luaguides

Playing Music and Sound Effects in LÖVE2D

Prerequisites

You need a working LÖVE2D project with at least a main.lua file. This tutorial builds on the basics covered in earlier entries in this series, but you can follow along with any LÖVE2D setup that can load audio files. Place your audio files (.ogg, .wav, .mp3, or .flac) in the same directory as main.lua or in a subdirectory where LÖVE2D can find them.

Overview

LÖVE2D provides a straightforward audio module (love.audio) for playing music and sound effects. The system distinguishes between two source types: static sources, which load entirely into RAM, and stream sources, which read from disk in chunks. Choosing the correct type for each use case matters for both performance and memory usage — static sources give you fast, instant playback at the cost of memory, while stream sources keep your RAM footprint small but add a small amount of I/O overhead during playback.

Supported audio formats

LÖVE2D supports the following audio formats:

FormatRecommended Use
.oggGeneral purpose — good compression, recommended for music and SFX
.wavUncompressed — larger files, fast loading, good for short SFX
.mp3Compressed — widely compatible, but .ogg is preferred in LÖVE
.flacLossless compression — larger than OGG, high quality

The .ogg format offers the best balance of file size and quality for most game audio needs. Place your audio files in the same directory as main.lua or in a subdirectory, then reference them by filename string.

Creating audio sources

Static sources (sound effects)

Use love.audio.newSource with the "static" type to load sound effects into RAM. Static sources load the entire file at creation time, which means faster playback on subsequent calls but uses more memory.

-- Load a sound effect into RAM
local jump_sound = love.audio.newSource("jump.ogg", "static")

Static sources are ideal for short, frequently-played sounds like jump noises, weapon shots, or UI clicks. You can clone a static source to play overlapping instances of the same sound. The key trade-off is memory: each static source holds the entire decoded audio buffer, so a one-second OGG file can easily consume several megabytes of RAM. For games with dozens of sound effects, the memory adds up quickly, but the payoff is that :play() triggers instantly with no disk I/O delay — exactly what you need for a tight-feeling action game.

Note that love.audio.newSource can return nil if the file is not found or the format is unsupported. This is a common source of silent failures in LÖVE2D games: if you misspell a filename and don’t check the return value, :play() will be called on nil and nothing happens — no error, no sound, just silence. Wrap loading in pcall or check the return value explicitly for safe error handling:

local jump_sound, err = love.audio.newSource("jump.ogg", "static")
if not jump_sound then
    print("Failed to load audio: " .. tostring(err))
end

Stream sources (music)

Use the "stream" type to stream audio from disk. The file is read in small chunks during playback, which keeps memory usage low even for long audio files.

-- Stream music from disk (does not load entire file into RAM)
local background_music = love.audio.newSource("music.ogg", "stream")

Streaming is the correct choice for background music, long dialogue tracks, or any audio longer than roughly 30 seconds. Attempting to stream very short files adds unnecessary overhead. The memory savings are significant: a 3-minute OGG track that would consume 30-50 MB of RAM as a static source stays at a few hundred KB of buffer memory when streamed. The trade-off is that :play() on a stream source may have a tiny initial delay while the first chunk is read from disk, though LÖVE2D’s audio backend handles this efficiently in the background thread, so your game loop won’t stall waiting for the read to complete.

Now that you can create both static and stream sources, the next step is controlling playback. LÖVE2D gives every Source object the same playback interface regardless of its type, so the same :play(), :pause(), and :stop() calls work identically on a streaming music track and a static sound effect.

Source playback controls

Basic Playback

The four fundamental functions on every audio Source — :play(), :pause(), :stop(), and :rewind() — give you straightforward playback control. Each has a specific effect on the source’s internal state, and combining them incorrectly can lead to surprising results. Understanding the state model before using these functions will save you debugging time later.

local sfx = love.audio.newSource("explosion.ogg", "static")

sfx:play()   -- Start playback
sfx:pause()  -- Pause playback (resume with :play() or :resume())
sfx:stop()   -- Stop playback and reset position to beginning
sfx:rewind() -- Rewind to the start without changing play/pause state

Calling :play() on a paused source resumes from the paused position. Calling :play() on a stopped or finished source starts from the beginning. The distinction between paused and stopped matters: a paused source still holds its audio resources and can be resumed instantly, while a stopped source is fully reset. If you plan to resume playback frequently, :pause() is more efficient than :stop() followed by :play().

Checking playback state

The state API — :isPlaying(), :isPaused(), and :isStopped() — lets you interrogate a source at runtime. These boolean methods are essential for building audio logic that responds to what is actually happening rather than what you assume is happening. For example, you might want to toggle music on and off while keeping the source alive, or avoid restarting a sound effect that is already audible. The following example demonstrates all three state checks in sequence, showing how each playback operation transitions the source between states:

local sfx = love.audio.newSource("beep.ogg", "static")

print(sfx:isPlaying())  -- false (hasn't started)
sfx:play()
print(sfx:isPlaying())  -- true
sfx:pause()
print(sfx:isPlaying())  -- false
print(sfx:isPaused())   -- true
print(sfx:isStopped())  -- false (stopped state has been reset by play())
sfx:stop()
print(sfx:isStopped())  -- true

Volume and Pitch

Volume Control

Volume ranges from 0.0 (silent) to 1.0 (maximum). Values outside this range are clamped.

local sfx = love.audio.newSource("coin.ogg", "static")

sfx:setVolume(0.5)  -- Set to 50% volume
print(sfx:getVolume())  -- 0.5

sfx:setVolume(1.5)  -- Clamped to 1.0
print(sfx:getVolume())  -- 1.0

The actual audible volume is source_volume × global_volume. You can also set a global volume that affects all sources:

love.audio.setVolume(0.8)  -- set global/master volume

Pitch Control

Pitch is measured as a multiplier where 1.0 is normal speed. Higher values play faster (higher pitch), lower values play slower (lower pitch). Changing pitch also changes playback duration, since the audio is effectively being played at a different speed. This can create interesting effects — doubling the pitch of a footstep sound makes it sound lighter and faster, while halving it produces a deep, slow rumble.

local sfx = love.audio.newSource("engine.ogg", "static")

sfx:setPitch(1.0)   -- Normal pitch
sfx:setPitch(2.0)   -- Double speed (one octave higher)
sfx:setPitch(0.5)   -- Half speed (one octave lower)

Pitch values must be positive. A pitch of 0 or negative causes undefined behavior.

Looping

Use :setLooping to control whether a source repeats. Looping is most commonly used for background music, but it can also work for ambient sounds like wind or engine hum that should play continuously without gaps. LÖVE2D handles loop transitions internally, stitching the end of the audio back to the beginning without a silent gap — unlike a naive manual seek-to-start approach.

local music = love.audio.newSource("bgm.ogg", "stream")
music:setLooping(true)   -- Enable looping
music:play()

-- Later, to stop looping:
music:setLooping(false)
music:stop()

A stream source with looping enabled plays indefinitely until you stop it. Stream sources handle looping seamlessly — do not implement your own loop logic by seeking.

Cloning sources for overlapping playback

A static source can only play one instance at a time via its own :play(). Calling :play() while already playing restarts from the beginning. To layer multiple copies of the same sound, clone the source:

local shoot_base = love.audio.newSource("shoot.ogg", "static")

-- Each clone is independent
local shoot1 = shoot_base:clone()
local shoot2 = shoot_base:clone()

shoot1:play()
shoot2:play()  -- Plays simultaneously with shoot1

Cloned sources inherit the original’s volume and pitch but are otherwise independent. Creating multiple clones of the same static source is cheap — they share the underlying audio buffer in memory. This shared-buffer design means you can create dozens of clones for a bullet-hell shooter without multiplying RAM usage, since only the playback state (position, volume, pitch) differs between clones, not the raw audio data. Common use cases include rapid gunfire, overlapping coin collection sounds, and footstep audio where multiple characters walk at once.

Playback Position

Seeking

Move the playback position with :seek(time), where time is in seconds.

local music = love.audio.newSource("song.ogg", "stream")
music:play()

-- Seek to 30 seconds
music:seek(30)

-- Seek to beginning
music:rewind()

:rewind() is equivalent to :seek(0), but it does not change whether the source is playing or paused. This distinction matters in practice: if you want to restart a track without interrupting playback, call :rewind() alone. If you want to fully stop and reposition, use :stop() or the explicit :seek(0) call. LÖVE2D handles seeking differently for static and stream sources — static sources can seek to any position instantly since the data is already in memory, while stream sources may need to re-read from disk if the seek target is far from the current playback point.

Telling the current position

You can query how far into the audio a source has progressed using :tell(). The returned value is in seconds, measured from the beginning of the audio file. This is useful for building progress bars in audio players, syncing visual effects to audio cues, or checking whether a voice line has finished before triggering the next dialogue event. Note that for stream sources with variable-bitrate encoding, the position may be approximate rather than frame-accurate.

local music = love.audio.newSource("song.ogg", "stream")
music:play()

local pos = music:tell()
print(pos)  -- e.g., 12.345 (seconds elapsed)

Getting Duration

The total length of an audio source is available through :getDuration(), which returns the duration in seconds. This is essential information for building any kind of audio UI, and the reliability of the returned value depends on what type of source you are working with.

local music = love.audio.newSource("song.ogg", "stream")
local sfx = love.audio.newSource("beep.ogg", "static")

print(music:getDuration())  -- e.g., 180.5 (seconds); -1 if unknown (some stream formats)
print(sfx:getDuration())   -- always known for static sources, e.g., 0.8

Static sources always know their duration. Stream sources return -1 if the decoder cannot determine the total length before fully parsing the file.

Combining position and duration

With both :tell() and :getDuration() available, you can compute playback progress as a percentage or display a formatted time string. This is the foundation for audio scrubbing UIs and progress indicators in game menus.

local music = love.audio.newSource("song.ogg", "stream")
music:play()

local current = music:tell()
local total = music:getDuration()
if total > 0 then
    print(string.format("%.1f / %.1f seconds", current, total))
end

Source type detection

Sometimes you need to know whether a source is static or stream at runtime — for example, when building a generic audio manager that handles both types differently. The :getType() method returns the string "static" or "stream", letting you branch on the type without maintaining your own type-tracking variables.

local sfx = love.audio.newSource("beep.ogg", "static")
local music = love.audio.newSource("song.ogg", "stream")

print(sfx:getType())   -- "static"
print(music:getType()) -- "stream"

Global audio controls

The love.audio module provides global functions that affect all sources. These operate at the module level, meaning they influence every active source simultaneously and are independent of any single source object. Global controls are particularly useful for implementing system-wide mute, pause-on-focus-loss for windowed games, or volume sliders in settings menus that the player expects to control everything at once.

love.audio.setVolume(0.8)  -- set master volume (0.0 to 1.0)
local vol = love.audio.getVolume()

love.audio.pause()       -- pause all currently playing sources
love.audio.pause(music)  -- pause a specific source

love.audio.stop()        -- stop all sources (resets all positions)
love.audio.stop(sfx)     -- stop a specific source

Spatial Audio

LÖVE2D supports 3D spatial audio using source positioning. Sounds farther from the listener are quieter, and the panning shifts based on the source’s position relative to the listener.

Setting source position

The listener is at the origin (0, 0, 0) by default. Position your sources in world coordinates:

local footstep = love.audio.newSource("footstep.ogg", "static")

-- x, y, z — z is usually 0 in a 2D game
footstep:setPosition(player_x, player_y, 0)
footstep:play()

Distance Attenuation

Control how quickly a sound fades with distance using setAttenuationDistances. The two-parameter model is straightforward: the first value sets the distance at which the sound plays at full volume, and the second sets the distance beyond which it is completely silent. Between these two points, LÖVE2D applies inverse-distance falloff, so sounds fade smoothly as they move away. This matches how real sound works and creates convincing spatial audio without complex configuration.

-- ref_distance: distance at which source is at full volume
-- max_distance: distance beyond which sound is inaudible
footstep:setAttenuationDistances(1.0, 50)

Values like 1.0 for the reference distance are typical in world coordinates. Adjust to match your game’s scale. The default attenuation model is inverse distance.

Common Gotchas

Playing a static source while it is already playing

Calling :play() on a static source that is already playing restarts it from the beginning — there is no queuing. Use :clone() for overlapping copies of the same sound.

Static sources use RAM regardless of looping

A static source holds the entire audio buffer in memory whether or not it is set to loop. A 1 MB OGG file becomes roughly 10–20 MB of RAM when decoded. For short SFX this is fine, but use "stream" for any looping music to keep memory usage low.

Pitch must be positive

Setting pitch to 0 or negative causes undefined behavior. Stick to values greater than 0.

Maximum active sources

LÖVE2D has a limit on the number of simultaneously active (playing or paused) sources. You can check the current count:

local count = love.audio.getActiveSourceCount()
print("Active sources: " .. count)

If you hit the limit, stop or release unused sources before playing new ones.

Example: complete sound manager

This is a practical, reusable manager that separates music and sound effect handling into a single table. The design loads all SFX into a lookup table keyed by name and wraps :play() behind a playSFX method that automatically clones the source, so every call produces an independent playback instance. Music gets its own dedicated source with looping enabled — a common pattern in real games where background music runs continuously while sound effects fire on demand.

local SoundManager = {}

function SoundManager:init()
    self.sounds = {}
    self.music = nil
    self.music_volume = 1.0
    self.sfx_volume = 1.0
end

function SoundManager:loadSFX(name, path)
    local sfx, err = love.audio.newSource(path, "static")
    if sfx then
        self.sounds[name] = sfx
    else
        print("Failed to load SFX '" .. name .. "': " .. tostring(err))
    end
end

function SoundManager:loadMusic(name, path)
    self.music = love.audio.newSource(path, "stream")
    self.music:setLooping(true)
end

function SoundManager:playSFX(name)
    local sfx = self.sounds[name]
    if sfx then
        local clone = sfx:clone()
        clone:setVolume(self.sfx_volume)
        clone:play()
    end
end

function SoundManager:playMusic()
    if self.music then
        self.music:setVolume(self.music_volume)
        self.music:play()
    end
end

function SoundManager:stopMusic()
    if self.music then
        self.music:stop()
    end
end

return SoundManager

Putting it together

Here is a minimal main.lua that puts audio into a real LÖVE2D game loop. This example demonstrates common patterns: streaming background music that loops, static sound effects for player actions, a clone-based approach for overlapping coin sounds, and music toggling via the pause/resume API. The key handler maps keyboard input to audio actions, showing how audio fits naturally into a standard game update loop.

local music
local sfx_jump
local sfx_coin

function love.load()
    -- Load background music (streamed)
    music = love.audio.newSource("bgm.ogg", "stream")
    music:setLooping(true)
    love.audio.play(music)

    -- Load sound effects (static)
    sfx_jump = love.audio.newSource("jump.ogg", "static")
    sfx_coin = love.audio.newSource("coin.ogg", "static")
end

function love.keypressed(key)
    if key == "up" or key == "w" then
        -- Play jump sound (each press restarts the static source)
        sfx_jump:play()
    elseif key == "space" then
        -- Collect coin — clone so overlapping coins play simultaneously
        local c = sfx_coin:clone()
        c:setVolume(0.8)
        c:play()
    elseif key == "m" then
        -- Toggle music pause
        if music:isPlaying() then
            love.audio.pause(music)
        else
            love.audio.resume(music)
        end
    elseif key == "escape" then
        love.event.quit()
    end
end

Summary

LÖVE2D’s audio system separates concerns cleanly: use "static" sources for short sound effects that need instant playback, and "stream" sources for music and long audio. Control individual sources with :play(), :pause(), :stop(), volume, pitch, and looping. Clone static sources when you need overlapping copies. Spatial audio is available through setPosition() and setAttenuationDistances(). Wrap audio loading in pcall to handle missing files gracefully, and be mindful of the active source count limit.

Next steps

Now that you can load and control audio in LÖVE2D, the next tutorial in this series explores tilemaps — building game worlds from reusable tiles with collision boundaries. You will learn how to use the audio manager from this tutorial alongside a tiled level system to create a complete game with sound, physics, and world design.

See also