Audio

A game audio director: sound variations, voice limits, music crossfades, intensity levels and playlists.

Commands

Import blitz.audio As Audio gives your program these, with Audio. in front. Each name leads to its part of the reference.

Functions

Types

Enums

Guide

Audio 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.

A danger meter two-thirds full, with the combat music playing and a trail of footprints

Try it

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.

How do I...

Play my own sounds?

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.

Make a sound less repetitive?

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

Keep sounds from piling up?

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

Play a sound from somewhere in the world?

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.

Change the music straight away, such as for a boss?

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

Play a list of tracks?

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

Fade everything out?

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 UpdateAudio every 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.

Good to know

More examples

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.

Reference

Needs
blitz.random
Examples
calm_combat.bb, footsteps.bb, playlist_space.bb

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.

Start with the working examples

Blitz3D+ comes with this module and blitz.random, so any Extended program can Import blitz.audio. Open an example and run 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.

API and defaults

There is one function spelling for each operation. Resource representation, construction, registrations, voices and the backend interface are private.

OperationContract 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):IntCopies 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):IntNative 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):IntCopy 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):IntManual 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):IntCopies 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):IntPrune then count owned playing/paused sources, including music. Does not count effect tails or runtime retained handle records.
CurrentMusic(director):StrSelected 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):IntAudioMode.Stopped, .Manual, .Bands or .Playlist; useful for UI.
LastError():StrModule-local main-thread diagnostic; copy before another fallible call/update. Suppression/priority drops return zero with empty error.
Delete directorStop 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:

FieldDefault / valid values
maxVoices:Int4; positive local cap
priority:Int0; larger is more important (any signed Int)
minimumRepeatSeconds:Double0; finite, >=0; measured from successful playback using explicit deltas
minimumGain, maximumGain:Double1, 1; finite ordered 0..1; replaces channel volume
pitchSemitones:Double0; symmetric finite 0..48 variation
originalRate:Int0; required for nonzero pitch variation; all variants share it
shuffle:IntFalse; 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.

Playback, fades and failure semantics

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.

Ownership and execution

ResourceOwner / borrowing
DirectorCaller deletes it explicitly; leaving scope does not run cleanup
Options, bands, input listsCaller; values copied at registration/construction
Sound handles, positional entitiesCaller; keep valid until director deletion/use has ended
Returned cue channelsDirector 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 backendDirector; 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.

Deferrals

Explicitly deferred: sample-accurate beat sync, gapless streaming, seeking, codec/mixer replacement, worker use, public backend plugins and automatic device recovery.