New

dot-audio

A game audio system that ships no audio — a catalogue of ids to paths, pooled players with priority stealing, per-id concurrency caps and cooldowns, a distance cull, buses with a linear slider, music crossfades and ducking.

dot-audio decides what is heard, how loud, how many at once, and what happens on a machine with no sound card.

Requires dot-core.

A sound is an id and a path

Never a loaded AudioStream. A dedicated server validates the whole catalogue without a sound card and without the files, and a game delivered as a dot-cloud pack is mounted at run time, so a catalogue that preloads cannot describe delivered content at all.

validate() therefore checks everything except whether the file exists; missing_files() is a separate question, asked by the client about to need them.

Usage

var audio := DotAudioManager.new()
audio.catalogue = my_catalogue
add_child(audio)
audio.setup()                        # picks a sink by asking the driver's name

audio.listener_position = camera.global_position   # once a frame; culling needs it
audio.play(&"ui_click")
audio.play_at(&"rifle", muzzle.global_position)
audio.play_music(&"combat", 1.5)     # crossfade; asking for what is playing does nothing
audio.duck(&"radio")                 # reference-counted, so two ducks do not fight

Hand it a DotRandomStream and a shot’s pitch and variant are the same on the server and every client watching, which is what makes an audio bug reproducible:

audio.roll_source = rng.stream(&"audio")

That is why there is a DotAudioSink interface, and why DotAudioSinkNull is not a mock: the real catalogue, culling, cooldowns, caps and music state machine run above it, and only the lines that would create an AudioStreamPlayer are replaced. It is also what lets a game that generates its own audio keep doing so.

A volume slider is not decibels

Mapping a 0..1 slider onto volume_db, even scaled to -60..0, gives a control where everything below about 0.9 is inaudible. Hearing is roughly logarithmic, so the slider carries a linear amplitude and linear_to_db converts. Zero is a muted bus rather than -60 dB, because a slider at the bottom should be silence and not a number a later +3 dB turns back into sound.

DotAudioMixer is a DotConfig, so dot-ui generates the screen and dot-settings persists it, with nothing here knowing either exists.

Three limits, which are three different problems

max_concurrent Twelve identical rifles half a millisecond apart is not twelve gunshots. It is one gunshot twelve times as loud, with comb filtering. Three sounds like a crowd.
cooldown_ms A footstep triggered from a physics callback fires twice in a frame, and the second is inaudible and still costs a voice.
max_distance A cull, not a curve. A sound at 400 metres attenuated to inaudible still costs a stream load, a voice and a position update every frame.

The pool is fixed: the sink creates its players once and never frees them. Create-play-queue_free has no ceiling, and a firefight is a hundred nodes created and destroyed per second, which on the web is a visible hitch. When the pool is full the lowest-priority voice is stolen, and if nothing is lower the new sound is refused — a gunshot losing to a footstep because the footstep started first is the failure mode of every fixed pool without priority.