Client Sound API

Controlling sounds started from Lua — volume, panning, looping, and the cleanup rules that stop sounds from playing forever.

The client global play_sound(sound_name, volume) (see lua-client-globals.md) starts a sound declared in a sound-config.md file and returns a sound object — a handle to that one playing instance. Keep the handle if you want to control or stop the sound later; each call to play_sound creates an independent instance.


Methods

MethodReturnsDescription
pause()—Pause playback (position is kept).
resume()—Resume after a pause.
stop()—Stop playback permanently. Safe to call more than once.
set_volume(volume)—Change volume. Same 0–100 percent scale as play_sound — 100 is full volume, 0 is silent.
set_panning(p)—Set stereo position: 0.0 = fully left, 0.5 = center, 1.0 = fully right.
set_looping(bool)—true loops the whole sound from the start; false clears looping.
set_looping_region(start_s, end_s)—Loop only between two time points, in seconds — e.g. skip an intro and loop the body.
get_position()numberCurrent playback position in seconds.
get_sound_state()stringOne of "Playing", "Pausing", "Paused", "WaitingToResume", "Resuming", "Stopping", "Stopped".

Cleanup — Don’t Leak Sounds

Losing the handle does not stop the sound. If you start a looping sound and drop the handle, it plays forever (until the session ends). This is the number one cause of stacked, ever-louder audio after mobs respawn.

The engine gives you one automatic cleanup path: any sound object stored as a top-level value of an entity’s lua_data table is stopped automatically when that entity disappears from the client (walks out of range, dies, despawns):

-- a mob's scripts.client-on-create
return function(entity)
    local music = play_sound("MyMod:boss_theme", 70)
    if music ~= nil then
        music:set_looping(true)
        entity.lua_data.boss_music = music   -- auto-stopped when the boss goes away
    end
end

Not covered by auto-cleanup — stop these yourself:

  • Sounds nested inside a sub-table of lua_data (e.g. entity.lua_data.sounds.music) — keep handles top-level instead.
  • Sounds stored on a static object’s lua_data — interactables don’t auto-stop their sounds; stop looping sounds in your own hooks (e.g. when the object is toggled off).
  • Sounds you keep in ordinary Lua variables.

One-shot (non-looping) sounds simply end on their own; leaking those handles is harmless.


Music Director

Background music and ambience are chosen by the music director from music-config.md, not started with play_sound. Scripts reach it through these calls:

CallReturnsDescription
music_override_start(id)handle, or nilPush a declared override — a boss theme — onto the stack. nil if no mod declares id.
handle:set_section(n)—Switch the override to section n, counting from 1. Out of range logs a warning and does nothing.
handle:stop()—Remove this override from the stack, wherever it sits. Safe to call more than once.
music_get_intensity()string"explore", "tension" or "combat".
music_raise_intensity(level, hold_ms)—Hold at least level for hold_ms milliseconds — a scripted ambush, an arena door closing. It never lowers a fight already happening.

An override handle follows the same cleanup rule as a sound object: stored as a top-level value of an entity’s lua_data, it is stopped when the entity goes away.

-- a boss's scripts.client-on-create
return function(entity)
    entity.lua_data.boss_music = music_override_start("MyMod:my_boss")
end
-- later, when the boss enters its second phase
if entity.lua_data.boss_music then
    entity.lua_data.boss_music:set_section(2)
end

Music is never the only sign of a boss’s phase change: switch sections alongside the boss’s visual tells, not instead of them.


See Also

Last updated