Playing Music and Sound Effects in LÖVE2D
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.
Supported Audio Formats
LÖVE2D supports the following audio formats:
| Format | Recommended Use |
|---|---|
.ogg | General purpose — good compression, recommended for music and SFX |
.wav | Uncompressed — larger files, fast loading, good for short SFX |
.mp3 | Compressed — widely compatible, but .ogg is preferred in LÖVE |
.flac | Lossless 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.
Note that love.audio.newSource can return nil if the file is not found or the format is unsupported. Wrap loading in pcall 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.
Source Playback Controls
Basic Playback
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.
Checking Playback State
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).
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.
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.
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.
Telling the Current Position
local music = love.audio.newSource("song.ogg", "stream")
music:play()
local pos = music:tell()
print(pos) -- e.g., 12.345 (seconds elapsed)
Getting Duration
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
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
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.
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:
-- 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
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:
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.