A game audio director: sound variations, voice limits, music crossfades, intensity levels and playlists.
Import blitz.audio As Audio gives your program these, with Audio. in front. Each name leads to its part of the reference.
LastError:Str()CreateAudio:AudioDirector(seed:Long, options:AudioOptions)ActiveVoices:Int(director:AudioDirector)CurrentMusic:Str(director:AudioDirector)MusicMode:Int(director:AudioDirector)AddCue:Int(director:AudioDirector, name:Str, sounds:Int List, options:CueOptions)PlayCue:Int(director:AudioDirector, name:Str, entity:Int = 0)AddMusic:Int(director:AudioDirector, name:Str, sound:Int, band:MusicBand, looping:Int = False)SetMusic:Int(director:AudioDirector, name:Str, fadeSeconds:Double = 1D)AudioIntensity(director:AudioDirector, value:Float)SetPlaylist:Int(director:AudioDirector, names:Str List, shuffle:Int = False)UpdateAudio(director:AudioDirector, seconds:Double)StopAudio(director:AudioDirector, fadeSeconds:Double = 0D)AudioOptionsmaxVoices:Int = 32, transitionSeconds:Double = 1DCueOptionsmaxVoices:Int = 4, priority:Int = 0, minimumRepeatSeconds:Double = 0D, minimumGain:Double = 1D, maximumGain:Double = 1D, pitchSemitones:Double = 0D, originalRate:Int = 0, shuffle:Int = FalseMusicBandminimum:Float = 0, maximum:Float = 1, hysteresis:Float = 0.05AudioDirectorAudioModeStopped, Manual, Bands, PlaylistAudio looks after your game's sound effects and music. Give a sound effect a few recordings, and each time it plays you hear a different one, a little higher or lower, so footsteps and sword swings never sound like a machine. It stops the same sound piling up when it's asked for too often, and keeps the number of sounds playing at once under control. Music fades smoothly from one track to another as the action heats up, or plays a list of tracks one after another.

Open danger.bb and run it. Hold Up to raise the danger: past halfway, the calm music fades into the combat music, and Down brings the calm music back. Hold Space to walk, and listen to the footsteps.
; Up and Down change the danger, and the music follows it. Hold Space to walk:
; no two footsteps sound quite the same. Escape quits.
Dialect "modern"
Import blitz.audio As Audio
Graphics 640, 240, 0, 2
SetBuffer BackBuffer()
ClsColor 18, 52, 79
; The director plays everything. 42 seeds its random choices.
Local director:Audio.AudioDirector = Audio.CreateAudio(42L, Null)
; A cue is one name for several sounds. Each time it plays, the director picks
; one of them and nudges its pitch a little.
Local sounds:Int List = New List
sounds.Add(MakeBeep(180D, 0.1D))
sounds.Add(MakeBeep(240D, 0.1D))
sounds.Add(MakeBeep(320D, 0.1D))
Local cue:Audio.CueOptions = New Audio.CueOptions
cue.pitchSemitones = 2D ; Up to two semitones higher or lower,
cue.originalRate = 22050 ; for sounds made at 22,050 Hz.
cue.minimumRepeatSeconds = 0.3D ; One step at most every 0.3 seconds.
Audio.AddCue director, "step", sounds, cue
Delete cue ; The director copied the options and the list.
Delete sounds
; Calm music plays while the danger is up to a half, and combat music above
; that. Music that loops is set looping before it's added.
Local calm:Int = MakeBeep(220D, 2D)
Local combat:Int = MakeBeep(330D, 2D)
LoopSound calm
LoopSound combat
Local band:Audio.MusicBand = New Audio.MusicBand
band.maximum = 0.5
Audio.AddMusic director, "calm", calm, band, True
band.minimum = 0.5
band.maximum = 1
Audio.AddMusic director, "combat", combat, band, True
Delete band
Local danger:Float
Local steps:Int
Local previous:Int = MilliSecs()
While Not KeyDown(1)
Local now:Int = MilliSecs()
Local seconds:Double = Double(now - previous) / 1000D
previous = now
; Up raises the danger and Down lowers it.
danger = danger + (KeyDown(200) - KeyDown(208)) * Float(seconds) * 0.4
If danger < 0 Then danger = 0
If danger > 1 Then danger = 1
Audio.AudioIntensity director, danger ; Picks the music to match.
If KeyDown(57)
; Asking every frame is fine: the repeat limit spaces the steps out.
If Audio.PlayCue(director, "step") <> 0 Then steps = steps + 1
EndIf
; Tell the director how much time has passed, so it can fade the music.
Audio.UpdateAudio director, seconds
Cls
Color 40, 90, 140
Rect 20, 40, 600, 24, True
Color 255, 120, 60
Rect 20, 40, Int(600 * danger), 24, True
Color 255, 255, 255
Rect 319, 34, 2, 36, True
Text 20, 14, "Danger: Up and Down. Walk: hold Space."
Text 170, 72, "calm", True
Text 470, 72, "combat", True
Text 20, 104, "Music: " + Audio.CurrentMusic(director)
Text 20, 124, "Sounds playing: " + Audio.ActiveVoices(director)
Local foot:Int
For foot = 0 To steps Mod 20 - 1 ; A footprint for each step.
Oval 24 + foot * 30, 160 + (foot Mod 2) * 20, 14, 8, True
Next
Flip
Wend
Delete director ; This stops its sounds. Free sounds only after it.
End
; Makes a beep without a sound file of its own: it writes a WAV file, loads it
; and deletes the file. Your game loads its sounds with LoadSound.
Function MakeBeep:Int(hertz:Double, seconds:Double)
Local count:Int = Int(seconds * 22050D)
Local file:Int = WriteFile("beep.wav")
WriteInt file, $46464952 ; "RIFF"
WriteInt file, 36 + count * 2
WriteInt file, $45564157 ; "WAVE"
WriteInt file, $20746D66 ; "fmt "
WriteInt file, 16
WriteInt file, $00010001 ; One channel,
WriteInt file, 22050 ; 22,050 samples a second,
WriteInt file, 44100
WriteInt file, $00100002 ; 16 bits a sample.
WriteInt file, $61746164 ; "data"
WriteInt file, count * 2
Local index:Int
For index = 0 To count - 1
WriteShort file, Int(4000D * SinD(Double(index) * hertz * 360D / 22050D))
Next
CloseFile file
Local sound:Int = LoadSound("beep.wav")
DeleteFile "beep.wav"
Return sound
End Function
The program makes its own beeps, so that it needs no sound files: MakeBeep,
at the end, has nothing to do with the module. While Space is held, the program
asks for a footstep every frame, and the cue's repeat limit lets one through
every 0.3 seconds. The director never acts on its own. It plays a cue when you
ask, picks the music when you tell it the danger, and fades the music when you
tell it how much time has passed, so you are in charge of pausing and slow
motion.
Load them as usual, put them in a list, and add a cue. A cue can have one sound, or several for the director to choose between.
Local sounds:Int List = New List
sounds.Add(LoadSound("jump1.wav"))
sounds.Add(LoadSound("jump2.wav"))
If Not Audio.AddCue(director, "jump", sounds, Null) Then Print Audio.LastError()
Delete sounds ; The director copied the list.
Audio.PlayCue director, "jump"
Null in place of options means "the usual way": one of the sounds at random,
at full volume and its own pitch.
Shuffle the sounds, and vary their volume and pitch. To change the pitch, the director needs to know the rate the sounds were recorded at, and every sound in the cue must have the same one.
Local options:Audio.CueOptions = New Audio.CueOptions options.shuffle = True ; Play each sound once, in a random order, then start again. options.minimumGain = 0.7D ; Vary the volume from 70%... options.maximumGain = 1D ; ...to full. options.pitchSemitones = 1D ; Up to a semitone higher or lower, options.originalRate = 44100 ; for sounds recorded at 44,100 Hz. Audio.AddCue director, "hit", hitSounds, options Delete options
Give the cue a repeat limit, and a limit on how many copies can play at once. When a cue is at its limit, a new copy stops the oldest one.
Local options:Audio.CueOptions = New Audio.CueOptions options.minimumRepeatSeconds = 0.1D ; At most one every 0.1 seconds. options.maxVoices = 3 ; At most three at once. Audio.AddCue director, "coin", coinSounds, options Delete options
The director plays at most 32 sounds at once, and when they're all busy, a new
sound stops the oldest of the least important ones. Give sounds that must
always be heard a higher priority. A sound never stops one that matters more:
it is skipped instead.
Local important:Audio.CueOptions = New Audio.CueOptions important.priority = 10 ; The usual is 0. Bigger numbers matter more. Audio.AddCue director, "alarm", alarmSounds, important Delete important
Load its sounds with Load3DSound, give the camera a listener, and pass the
entity to PlayCue.
Local listener:Int = CreateListener(camera)
Local splashes:Int List = New List
splashes.Add(Load3DSound("splash.wav"))
Audio.AddCue director, "splash", splashes, Null
Delete splashes
Audio.PlayCue director, "splash", rock ; Plays from where the rock is.
Add the music without a band, so the danger never picks it, and choose it with
SetMusic. It takes over from the danger until you next call
AudioIntensity.
LoopSound bossMusic Audio.AddMusic director, "boss", bossMusic, Null, True Audio.SetMusic director, "boss", 0.5D ; Fade it in over half a second. ; When the boss is beaten, let the danger choose again: Audio.AudioIntensity director, danger
Add music that doesn't loop, and give SetPlaylist the names. When a track
ends, the next one fades in, and the list starts again after the last. True
shuffles it.
Audio.AddMusic director, "town1", townMusic1, Null
Audio.AddMusic director, "town2", townMusic2, Null
Local names:Str List = New List
names.Add("town1")
names.Add("town2")
If Not Audio.SetPlaylist(director, names, True) Then Print Audio.LastError()
Delete names
Audio.StopAudio director, 2D ; Fade out every sound and the music over two seconds.
The director then stops choosing music until you ask for some again, but you can carry on playing cues.
Blip's tip
Call
UpdateAudioevery frame, with the seconds that have passed. The director has no clock of its own, so without it, music that should fade in stays silent, and a cue with a repeat limit plays once and never again.
maxVoices in the
AudioOptions you create the director with, it plays at most 32 sounds at
once, and two of them are always kept for music. A cue plays at most 4
copies at once unless you change its own maxVoices.PlayCue returns 0 when a limit stops
a sound playing, and LastError() is then empty. For a name it doesn't know,
it returns 0 and LastError() says why.LoopSound before you add it,
and add it with True. The director never changes your sounds' own
settings. A playlist only takes music that doesn't loop.hysteresis
changes that distance. In the program above, the combat music plays on until
the danger drops below 0.45.Three more complete programs come with the module. Each waits for Space or a click before it starts.
The reference below has every function, option and rule.
A small game audio director: sound variations, priority/voice limits, music crossfades, intensity bands and repeating playlists. It uses decoded runtime sound channels. It does not replace the mixer, spatial audio or effects system.
Requires Extended with object-lifecycle-v1 and the completed blitz.random
package. Importing opens no window, reads no file and changes no audio setting.
Main-thread use only. No application callbacks, event bus or hidden clock.
Blitz3D+ comes with this module and blitz.random, so any Extended program can
Import blitz.audio. Open an example and run it.
examples/footsteps.bb: Space plays three shuffled, pitch-varied tones with
a 120ms repeat limit. This is the quick-start source to copy and modify.examples/calm_combat.bb: arrows select calm/combat bands, M selects manual
calm, 1/2 switch between single/stacked cue limits, Space plays a cue. Music
crossfades and the counter displays actual owned active sources.examples/playlist_space.bb: a shuffled playlist of three non-looping tracks,
rapid 1/2/3 interruptions, P to return to playlist, S to fade-stop. Arrows move
the visible emitter; Space emits a positional tone from it.Each generates quiet mono PCM WAVs in its own audio-<UUID> directory, loads
them normally, and stops channels, frees sounds and removes generated files.
Examples exit with Escape. --verify drives real finite interactions, rendering
and cleanup. In the browser click Start to activate audio; interactive
examples then ask for Space or a canvas click. Use a comfortable listening
volume.
Once your game has loaded sound handles, the common path is:
Dialect "modern" Import blitz.audio As Audio Local director:Audio.AudioDirector = Audio.CreateAudio(42L, Null) Local variants:Int List = New List variants.Add(footstepSound) Audio.AddCue director, "step", variants, Null Delete variants Local channel:Int = Audio.PlayCue(director, "step") Audio.UpdateAudio director, frameSeconds Delete director ; Now FreeSound footstepSound: sounds are borrowed, not owned by the director.
Check registration/selection return values and copy Audio.LastError() on
failure. Real applications keep the director and update it once per frame.
Input/game decisions come first; spatial scene update precedes audio update;
drawing follows. Pause/time scale belongs to the application. All deltas and
durations are finite nonnegative Double seconds. Zero is valid.
There is one function spelling for each operation. Resource representation, construction, registrations, voices and the backend interface are private.
| Operation | Contract and example |
|---|---|
CreateAudio(seed:Long, options:AudioOptions) | Caller-owned director. Explicit Null chooses defaults; Audio.CreateAudio(1L, Null). Invalid cap/time is a programming error. |
AddCue(director, name, sounds:Int List, options:CueOptions):Int | Copies list/options; borrows nonzero sounds. Nonempty case-sensitive unique cue name. False plus LastError for malformed/duplicate registration; candidate does not commit. Audio.AddCue d, "step", sounds, options. |
PlayCue(director, name, entity:Int=0):Int | Native owned channel, or zero. Entity 0 uses PlaySound; otherwise EmitSound with a caller-owned entity and 3D sound. Audio.PlayCue d, "step", player. |
AddMusic(director, name, sound:Int, band:MusicBand, looping:Int=False):Int | Copy optional band (Null for none), borrow sound. False plus LastError for bad metadata or duplicate name. Audio.AddMusic d, "calm", sound, band, True. Cue/music namespaces are separate. |
SetMusic(director, name, fadeSeconds:Double=1D):Int | Manual mode; false on unknown name/start failure. Re-selecting destination is a no-op, even mid-fade. Audio.SetMusic d, "combat", 0.8D. |
SetPlaylist(director, names:Str List, shuffle:Int=False):Int | Copies a nonempty repeating playlist; validates all names/non-looping declarations before changing it. Audio.SetPlaylist d, names, True. |
AudioIntensity(director, value:Float) | Explicitly selects band mode; finite value clamps to 0..1. Audio.AudioIntensity d, 0.8. No matching band fades to silence. |
UpdateAudio(director, seconds:Double) | Prune finished sources, decrement cooldowns, advance envelopes/playlist. Audio.UpdateAudio d, dt. Never polls input, sleeps or calls UpdateWorld. |
StopAudio(director, fadeSeconds:Double=0D) | Cancel automatic selection and pending transitions immediately; fade existing sources from their current levels. Director remains reusable. Audio.StopAudio d, 0.5D. |
ActiveVoices(director):Int | Prune then count owned playing/paused sources, including music. Does not count effect tails or runtime retained handle records. |
CurrentMusic(director):Str | Selected destination name, including a deferred third track; empty after stop/no selection. A completed manual track remains selected until a different request or stop. |
MusicMode(director):Int | AudioMode.Stopped, .Manual, .Bands or .Playlist; useful for UI. |
LastError():Str | Module-local main-thread diagnostic; copy before another fallible call/update. Suppression/priority drops return zero with empty error. |
Delete director | Stop owned playing/paused sources immediately; release only owned state. Null/deleted aliases are harmless. Delete before borrowed sounds/entities. |
AudioOptions is caller-owned: maxVoices=32 (at least 2),
transitionSeconds=1D for band/playlist changes. Two music slots are reserved
inside the total budget; cues use at most maxVoices-2. A cap of 2 disables
cues. Registration options do not grow the total budget. Example:
Local o:Audio.AudioOptions = New Audio.AudioOptions then o.maxVoices = 10.
CreateAudio copies it; delete/change it freely afterwards.
CueOptions is caller-owned, copied by AddCue:
| Field | Default / valid values |
|---|---|
maxVoices:Int | 4; positive local cap |
priority:Int | 0; larger is more important (any signed Int) |
minimumRepeatSeconds:Double | 0; finite, >=0; measured from successful playback using explicit deltas |
minimumGain, maximumGain:Double | 1, 1; finite ordered 0..1; replaces channel volume |
pitchSemitones:Double | 0; symmetric finite 0..48 variation |
originalRate:Int | 0; required for nonzero pitch variation; all variants share it |
shuffle:Int | False; True uses a small independent shuffle bag borrowing the director stream |
Nonzero pitch variation must keep the entire range within 100..192000 Hz. ChannelPitch takes absolute Hz, so a validated original sample rate is mandatory. The runtime has no public sample-rate query. Zero variation leaves copied sound pitch defaults untouched. Equal gain bounds are a constant (no random draw). At zero gain a cue remains an ordinary active source, not a completed stop.
MusicBand is caller-owned, copied by AddMusic: Float minimum=0, maximum=1,
hysteresis=0.05. All finite in 0..1, minimum <= maximum. Bands include both
endpoints. Registration order breaks overlapping matches. An incumbent in band
mode remains until intensity leaves its interval expanded by hysteresis. Manual
SetMusic suspends automatic selection until the next explicit AudioIntensity;
that request does a fresh match without retaining a manual selection's band.
SetPlaylist deliberately switches to playlist mode. Stop switches to Stopped.
Cue caps are checked before starting a channel. At the local cap only a voice of that cue is eligible for replacement; at the global cue cap, choose the oldest lowest-priority voice no more important than the new request. If every candidate is more important, drop the request. Pruning also runs before a play. Replacement stops first, so even transient overlap cannot exceed the cap. A start failure after eviction leaves the evicted voice stopped and reports an error. Suppression consumes no random values; a backend start attempt does.
Per-director randomness uses completed blitz.random PCG streams; no SeedRnd.
Each bag cycle is a permutation; boundary repeats are allowed. Playlist shuffle
uses its own bag on the same director stream. Call order determines draws.
Realtime completion depends on the runtime, so seeded selection is deterministic
given identical request/completion traces, not independently of the device clock.
Two-channel music uses linear amplitude interpolation, giving a gain sum <=1 for correlated tracks. It does not promise constant perceived loudness. Retargeting either playing track begins from current gains. A third distinct track during overlap drains both to zero over half the requested duration, then fades the latest destination in over the other half. This deliberate dip preserves continuity and two physical slots. Further requests restart from current levels. Zero duration stops obsolete channels and applies the new selection immediately. A delta spanning both phases applies the remainder to the incoming fade.
StopAudio cancels playlist/band/pending selection and fades current voices; new cue/music requests are legal during that stop. Repeating StopAudio starts from current levels. Delete is immediate and does not complete a playlist, save anything or wait. There is no public Cancel/Save/Commit operation with a separate meaning in this module, so none is invented as a disposal alias.
Music sounds are already decoded runtime-supported sounds. Prepare loops with
LoopSound sound before registration and declare looping=True. The director
never calls LoopSound or changes SoundVolume/Pitch/Pan/groups/effects. There is no
unloop or loop-state query: declarations must agree with caller preparation.
Playlists reject declared loops. They advance once after the selected source
finishes (including an external StopChannel). They cannot infer hypothetical
track completions from elapsed time because no duration/seek API is available.
Shuffled cycles may repeat at a cycle boundary; singleton playlists restart.
Malformed registrations/unknown names and runtime start failures use return status plus LastError; invalid director, Null required Lists, invalid time and nonfinite intensity are programming errors. Native handles must be valid; the module cannot validate arbitrary nonzero sound/entity integers. A delayed start failure is reported by UpdateAudio and stops automatic retry. Backend failure does not roll back audible stops or consumed randomness.
| Resource | Owner / borrowing |
|---|---|
| Director | Caller deletes it explicitly; leaving scope does not run cleanup |
| Options, bands, input lists | Caller; values copied at registration/construction |
| Sound handles, positional entities | Caller; keep valid until director deletion/use has ended |
| Returned cue channels | Director controls lifetime; caller may pause/resume/stop/change volume; only a later director fade (for example StopAudio with a fade) changes it again |
| Registrations, voices, bags, copied lists, random stream, private backend | Director; never exposed to importers |
No callbacks reach application code during mutation, so callback reentrancy and deletion guards are unnecessary. The private backend interface is not a public callback extension. Public mutation occurs only between calls on the main thread. Mutation never invalidates an active collection Each; owned voice lists are drained backwards by index. A paused runtime channel still counts as playing; module time nevertheless advances if the caller supplies it.
Extended native/web audio retains completed channel identities until runtime shutdown. The director relies on this real lifetime contract, never reuses a record's handle and prunes completed sources before setters/cleanup. Lightweight runtime handle tombstones accumulate over long playback sessions; the module cannot free them because there is no FreeChannel. PCM/nodes and module voice records are released, and unrelated sources/sounds are untouched.
Algorithms use stable Lists: registration lookup O(cues/music), scans O(registrations + active voices), each stable removal O(active voices), bag refill O(variants). Pruning many completed sources can cost O(active voices squared); the explicit voice budget bounds that work (default 32 total). Active voice records are bounded; definitions and input variant lists scale with registration. There is no general tween dependency and no dependent adapter.
Explicitly deferred: sample-accurate beat sync, gapless streaming, seeking, codec/mixer replacement, worker use, public backend plugins and automatic device recovery.