Buses, play options, snapshots, and entity sources.
Audio mixing
Sound in DemiEngine runs through a mixer with named buses, so music, effects, voice, and UI can be balanced separately. Scripts play one-shots and loops through demi.audio, or attach playback to an entity with demi.audio.source.
What a bus is
A bus is a volume lane. Every sound you play is routed into one, and the lanes sum upward. The default tree is master at the top with music, sfx, voice, and ui underneath. Turning down music leaves footsteps and dialogue alone; turning down master quiets everything.
local Audio = require("demi.audio")
Audio.play("asset://audio/skyforge_rune_collect", {
bus = "sfx",
volume = 0.8,
})
Audio.play(uri, options) starts a sound from an audio asset URI. The options table says how it plays (fields below). It returns an integer handle for the playing voice, or 0 when nothing could be started; pass a nonzero handle to Audio.stop when you need to end it early.
Play options
| Option | Type | Meaning |
|---|---|---|
bus | string | Mixer lane: music, sfx, voice, ui (under master) |
loop | boolean | Repeat until stopped |
streaming | boolean | Stream from disk instead of keeping the whole clip resident (music and ambience) |
volume | number | Per-sound loudness multiplier |
pitch | number | Playback rate multiplier (1 = normal) |
pan | number | Stereo placement for non-spatial sounds |
spatial | string | none, 2d, or 3d. Only 3d enables backend spatialization; use pan for non-spatial stereo placement. |
attenuation | string | Distance falloff: none, inverse, linear, or exponential |
x / y / z | numbers | World position for spatial sounds (scalars, like transforms) |
min_distance / max_distance | numbers | Near and far distance bounds for spatial attenuation; the sound is not necessarily silent at max_distance |
rolloff | number | Nonnegative distance-falloff factor |
doppler | boolean | Enable the backend Doppler factor; current Lua and entity-source requests submit zero source/listener velocity, so transform motion alone does not produce a shift |
delay | number | Seconds to wait before the sound starts |
fade_in | number | Seconds to ramp volume up at the start |
concurrency_group | string | Pool name limiting how many similar sounds play at once |
max_voices | number | Cap for that concurrency group |
voice_stealing | string | What to drop when the cap is hit: reject (refuse the new sound), oldest, or quietest |
pause_with_game | boolean | Whether Time.set_paused also pauses this sound |
Distances use world units and times use seconds. Omitted play options use runtime defaults (for example sfx, no loop, spatial = "none", inverse attenuation, and game-pause enabled); an asset's settings.streaming can additionally force streaming.
local Audio = require("demi.audio")
Audio.play("asset://audio/skyforge_wind", {
bus = "music",
loop = true,
streaming = true,
spatial = "3d",
x = 0, y = 0, z = -12,
min_distance = 4,
max_distance = 40,
fade_in = 2,
pause_with_game = true,
})
Audio.play("asset://audio/skyforge_step", {
bus = "sfx",
spatial = "2d",
concurrency_group = "steps",
max_voices = 3,
voice_stealing = "oldest",
})
The first call is looping streamed wind in 3D at a world position, fading in over two seconds and pausing with the game clock. The second is a non-spatial footstep limited to three simultaneous voices: a fourth step steals the oldest one. Both spatial = "none" and spatial = "2d" leave backend 3D positioning disabled.
Snapshots and transitions
A snapshot is a saved mix — bus volumes for a moment like “underwater” or “low health”. Switching between snapshots can be instant or gradual.
local Audio = require("demi.audio")
Audio.define_snapshot("skyforge_low_health", {
music = 0.4,
sfx = 0.9,
voice = 1.0,
ui = 1.0,
})
Audio.transition_snapshot("skyforge_low_health", 0.5) -- blend over 0.5 s
-- swap the music track under the player with a crossfade:
local music = Audio.play("asset://audio/skyforge_normal", { bus = "music" })
local next = Audio.crossfade(music, "asset://audio/skyforge_low_health", 1.2)
-- crossfade already fades out music; stop(next) later if needed
Audio.define_snapshot(name, buses)— saves a bus-volume table undername. Returns nothing.Audio.transition_snapshot(name, seconds)— blends the live mix toward that snapshot. Returnstruewhen the transition started.Audio.crossfade(from_handle, asset_id, duration, bus?, loop?, streaming?)— startsasset_idand fades it in while fading the voicefrom_handleout. The optional arguments default tomusic,true, andtrue. Returns the new voice handle, or0on failure.Audio.stop(handle)— stops one playing voice. Returnstruewhen stopped.
Snapshot tables map bus names to volume numbers. Define them once at startup (in on_start or on_create of an audio director script) and switch during play. The crossfade starts a new asset; its second argument is an asset:// ID, not a voice handle.
Entity-attached playback
An AudioSource component holds the clip and playback settings on the entity itself. Starting it makes the sound follow the entity and obey its position, which is what 3D world sounds need.
local AudioSource = require("demi.audio.source")
local handle = AudioSource.play("skyforge_rune") -- play the entity's attached source
AudioSource.stop("skyforge_rune") -- stop whatever this entity is playing
AudioSource.play(entity_id)— starts the entity'sAudioSourceclip at the entity's position, using the component's bus/volume/spatial settings. Returns an integer handle (or0when the entity has no source to play).AudioSource.stop(entity_id)— stops that entity's playback. Returnstruewhen stopped.
For the listening side of 3D audio, an AudioListener component marks the entity the mixer hears from — usually the player or the camera. Only one listener matters at a time.
local Entity = require("demi.entity")
Entity.add_component(Entity.find("skyforge_player"), "AudioListener", {})
-- from then on, 3D sounds are mixed relative to the player's position
Entity.add_component(id, "AudioListener", fields) attaches the listener; the fields table can be empty. Spatial distance attenuation uses the listener's position and the sound's min_distance, max_distance, attenuation, and rolloff settings. Doppler would require relative velocity, which these Lua and entity-source paths currently leave at zero.
Next steps
- State machines — animation markers that trigger footsteps.
- Importers — bringing WAV/OGG/MP3/FLAC sources into
asset://. - Assets, time & timers — loading long music tracks before you play them.