Playing Music and Sound Effects in LÖVE2D

· 8 min read · Updated March 20, 2026 · beginner
love2d audio sound-effects music lua gamedev

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:

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.

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.