DEMIENGINEDOCUMENTATION Everything is free
Animation & audio · Game systems

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

OptionTypeMeaning
busstringMixer lane: music, sfx, voice, ui (under master)
loopbooleanRepeat until stopped
streamingbooleanStream from disk instead of keeping the whole clip resident (music and ambience)
volumenumberPer-sound loudness multiplier
pitchnumberPlayback rate multiplier (1 = normal)
pannumberStereo placement for non-spatial sounds
spatialstringnone, 2d, or 3d. Only 3d enables backend spatialization; use pan for non-spatial stereo placement.
attenuationstringDistance falloff: none, inverse, linear, or exponential
x / y / znumbersWorld position for spatial sounds (scalars, like transforms)
min_distance / max_distancenumbersNear and far distance bounds for spatial attenuation; the sound is not necessarily silent at max_distance
rolloffnumberNonnegative distance-falloff factor
dopplerbooleanEnable the backend Doppler factor; current Lua and entity-source requests submit zero source/listener velocity, so transform motion alone does not produce a shift
delaynumberSeconds to wait before the sound starts
fade_innumberSeconds to ramp volume up at the start
concurrency_groupstringPool name limiting how many similar sounds play at once
max_voicesnumberCap for that concurrency group
voice_stealingstringWhat to drop when the cap is hit: reject (refuse the new sound), oldest, or quietest
pause_with_gamebooleanWhether 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 under name. Returns nothing.
  • Audio.transition_snapshot(name, seconds) — blends the live mix toward that snapshot. Returns true when the transition started.
  • Audio.crossfade(from_handle, asset_id, duration, bus?, loop?, streaming?) — starts asset_id and fades it in while fading the voice from_handle out. The optional arguments default to music, true, and true. Returns the new voice handle, or 0 on failure.
  • Audio.stop(handle) — stops one playing voice. Returns true when 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's AudioSource clip at the entity's position, using the component's bus/volume/spatial settings. Returns an integer handle (or 0 when the entity has no source to play).
  • AudioSource.stop(entity_id) — stops that entity's playback. Returns true when 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