FnQuake3's modern audio engine is a client-side rendering upgrade for the existing Quake III sound contract. Game code, mods, demos, network protocol, filesystem behavior, and accepted asset formats stay on the classic surface; the modern path changes how the client renders sounds after the game has already chosen what to play.
The default backend is OpenAL. The original software mixer remains available as
s_backend legacy and is still the deterministic fallback when OpenAL cannot
start.
- Preserve retail Quake III behavior first. Spatial features must be additive, optional where appropriate, and observable through diagnostics.
- Keep player-facing controls stable.
s_backend,s_backendActive,s_volume,s_musicVolume,s_doppler, OpenAL device/HRTF/output cvars, and thes_info/s_list/snd_restartworkflow are compatibility surfaces. - Use OpenAL Soft capabilities when present, but treat HRTF, output mode, source counts, direct-channel routing, EFX, latency clocks, and live device recovery as runtime capabilities rather than guarantees.
- Keep server, protocol, VM, demo, and asset loading behavior out of scope for audio rendering changes.
- Make advanced map tuning data-only.
.azbaudio-zone sidecars refine environment rendering, but missing or invalid sidecars must never break a map.
Audio is now organized as one client-owned module:
code/client/audio/
snd_public.h public S_* sound API used by the client
snd_local.h private shared sound types and backend contract
snd_main.cpp cvars, commands, backend selection, fallback
legacy/
snd_dma.cpp original software mixer backend
snd_mix.cpp paint/mix path
snd_mem.cpp sample cache and memory path
snd_adpcm.cpp legacy ADPCM helper
snd_wavelet.cpp legacy wavelet helper
codecs/
snd_codec.h codec interface used by both backends
snd_codec.cpp codec registry and stream helpers
snd_codec_wav.cpp WAV loader (PCM 8/16/24-bit, IEEE float32,
WAVE_FORMAT_EXTENSIBLE; decodes to 16-bit)
snd_codec_ogg.cpp optional Ogg Vorbis loader
openal/
AudioSystem.cpp modern backend translation unit
AudioSystem*.inl private OpenAL implementation slices
shared/
AudioDeviceRecovery.h device-loss policy
AudioOcclusion.h occlusion smoothing policy
AudioZoneFormat.h .azb sidecar format
AudioZoneRuntime.h sidecar parser and lookup runtime
code/tools/audiozones/ optional .audiozones to .azb compiler/auditor
tests/audio/ deterministic policy, sidecar, and loopback tests
The high-level Quake sound API remains the S_* surface declared in
code/client/audio/snd_public.h. S_Init in
code/client/audio/snd_main.cpp owns player cvars, console commands, backend
selection, backend fallback, and soundInterface_t validation. Both the OpenAL
backend and the original mixer implement that same callback table, keeping
callers insulated from the rendering path.
The old split left modern OpenAL files under code/client/audio, reusable
policy headers under code/audio, and classic snd_* files directly in
code/client. That made the ownership boundary harder to read than the code
really is.
The best-practice shape for this tree is a feature module under the subsystem
that owns it. Audio rendering is a client feature, not a qcommon/server feature,
so code/client/audio/ is the natural home. Tools and tests still need the
sidecar format and deterministic policies, so those small headers live in
code/client/audio/shared/ rather than in a separate top-level code/audio/
island.
This also avoids two misleading alternatives:
- A top-level
code/audio/directory implies a reusable engine-wide subsystem, but the live backend depends on client state, client sound types, and client lifecycle. - A sibling
code/client/audio-legacy/directory overstates the split. The original mixer is a fallback backend, but the public sound facade, codecs, sample cache expectations, and mixer-era data types remain part of the active client sound contract.
The chosen layout keeps related files discoverable while keeping compatibility risk low:
code/client/audio/owns all client sound code.code/client/audio/openal/contains the modern backend.code/client/audio/legacy/contains the original mixer backend.code/client/audio/codecs/contains format loaders shared by both paths.code/client/audio/shared/contains small deterministic policies and sidecar formats used by the backend, tools, and tests.
The snd_* files were moved into this module, but they were not rewritten into
the OpenAL backend's single-translation-unit .inl style. That style is useful
for keeping OpenAL-private C++ types out of old C-facing headers; applying it to
the classic mixer would create a broad compatibility-sensitive rewrite without
changing the public boundary.
client game and engine callers
|
v
S_* functions in code/client/audio/snd_public.h
|
v
code/client/audio/snd_main.cpp
|
v
soundInterface_t backend table
|
+-- openal: code/client/audio/openal/AudioSystem.cpp
|
+-- legacy: code/client/audio/legacy/snd_dma.cpp
The OpenAL backend uses the same sound registration, start, loop, raw stream, music, respatialization, update, list, info, and shutdown semantics expected by the rest of the client. OpenAL does not create a second game-facing sound API.
Startup is request-oriented:
S_Initregisters sound cvars and commands.- If
s_initsoundis enabled, codecs are initialized and the requested backend is tried. s_backend openalcallsS_OpenAL_Init, which fillssoundInterface_tthroughAudioSystem::Init.- If OpenAL cannot initialize, startup reports the failure and calls
S_Base_Initfor the legacy software mixer. s_backendActiveis set toopenal,legacy, ornonebased on the backend that actually started.- Later callers keep using classic
S_*functions without knowing which backend is active.
AudioSystem.cpp includes private implementation slices inside an anonymous
namespace. They are not public headers.
AudioSystemShared.inldefines cvar pointers, math helpers, sample format classification, extension helpers, source-class/tone policy, environment state, audio-zone loading, and shared formatting.AudioSystemOpenAL.inlowns dynamic OpenAL loading, device/context creation, extension discovery, HRTF/output-mode requests, source and buffer helpers, EFX setup, direct-channel routing, timing queries, and recovery hooks.AudioSystemWorld.inlowns registered samples, world voices, looping sound state, listener state, positional updates, occlusion smoothing, tone application, source budgeting, and spatial debug snapshots.AudioSystemStreams.inlowns OpenAL buffer queues for music and raw sample streams.AudioSystemBackend.inlowns thesoundInterface_tfacade, backend lifetime, diagnostics, sample registration, music/raw handling, and per-frame service flow.
The .inl split keeps the OpenAL backend in one translation unit so private C++
types do not leak into the old C-facing client sound headers, while still
keeping device, world, stream, and facade code readable.
Mono world sounds are true OpenAL positional sources. Listener origin, orientation, source origin, source velocity, distance model, reference distance, max distance, and rolloff feed the OpenAL source model. This lets HRTF-capable output render source direction through head turns and movement.
Doppler uses OpenAL's native two-body model. The backend sets
alDopplerFactor/alSpeedOfSound from s_doppler, s_alDopplerFactor, and
s_alDopplerSpeed (live, no restart), feeds looping-source velocities from the
game, and derives listener velocity from respatialize deltas with teleport
rejection, speed clamping, and short exponential smoothing so respawns and
frame jitter cannot chirp the pitch.
The backend also enforces Quake III's audibility horizon on top of the clamped OpenAL distance models, which never reach true silence on their own. Positional voices fade over the final stretch of the legacy maximum range; loops beyond it become virtual voices (logical state kept, OpenAL source and filters returned to the pool) and one-shots that would start inaudible are skipped. One-shot eviction prefers the least audible voice, estimated from applied gain and distance, before falling back to age.
When EFX is present the listener is calibrated with AL_METERS_PER_UNIT
(1 unit = 1 inch) and positional sources get AL_AIR_ABSORPTION_FACTOR from
s_alAirAbsorption, giving distance-dependent high-frequency air absorption
with physically meaningful scaling.
Direct-path content stays direct by default:
- local UI and announcer-style sounds
- raw samples and background music
- authored stereo samples
- authored surround samples
- explicitly tagged UHJ or B-Format assets
When AL_SOFT_direct_channels is available, stereo/surround samples and streams
request direct channel routing so authored speaker channels are not image-shifted
by HRTF. When AL_SOFT_direct_channels_remix is also available, unmatched
speaker channels can be folded into narrower output layouts. Two-channel world
samples only enter positional routing through the opt-in
s_alSpatializeStereo compatibility switch and only on runtimes with
AL_SOFT_source_spatialize.
UHJ and B-Format are explicit filename-tag features, not automatic reinterpretations of ordinary WAV files. If the runtime does not accept the encoded OpenAL format, the backend falls back to a safe stereo-compatible path instead of failing the sound.
The OpenAL backend adds an environmental layer on top of the normal source selection path.
- Reverb uses EFX when the device supports it.
s_alReverbis latched because the effect slot is created at backend init. The backend prefersAL_EFFECT_EAXREVERB(low-frequency decay control, echo hints, underwater modulation) and falls back toAL_EFFECT_REVERBwhen the runtime rejects it; presets carry both parameter sets and zone sidecar preset indices are unchanged. - Occlusion uses conservative collision traces between listener and source, including a small source-side probe fan so edge cases can become partial occlusion instead of a binary mute. A liquid boundary between source and listener applies a partial-occlusion floor through the same smoothing and tone pipeline.
- Tone shaping uses low-pass, high-pass, and band-pass policies by source class, environment, and occlusion state.
- Environment transitions are smoothed so moving through thresholds does not zipper.
s_alReverbGainands_alOcclusionStrengthscale feature strengths without changing asset data.
Audio-zone sidecars can override generic environment heuristics for a map. The
runtime looks for maps/<mapname>.azb through normal filesystem search
semantics, parses the sidecar with code/client/audio/shared/AudioZoneRuntime.h,
and uses the current listener position to choose a zone. Zones may carry reverb,
occlusion, LF/HF tone multipliers, transition time, priority, material metadata,
outdoor/underwater flags, and bounded portal blend hints. Missing, disabled, or
invalid sidecars are harmless.
On Windows the OpenAL library search prefers a real OpenAL Soft runtime:
executable-directory OpenAL32.dll, executable-directory soft_oal.dll, the
packaged runtime path, a system soft_oal.dll, and only then the system
OpenAL32.dll, which is usually the legacy Creative router. When the loaded
runtime exposes no OpenAL Soft extensions, init prints a warning naming the
library and device so a degraded spatial layer is never silent. EFX filter
types are probed at init; runtimes that reject high-pass or band-pass filters
(the router's "Generic Software" driver) degrade those tones to low-pass so
occlusion and underwater muffling keep working.
The backend tries the requested OpenAL device first. If that device cannot open, it tries the system default before falling back to the legacy backend. Context creation is similarly layered: requested modern attributes first, simpler context attributes next, then the outer backend fallback.
Runtime device recovery is conservative:
s_alAutoRecoverenables automatic live recovery attempts when supported.s_alRecoverDevicemanually asks OpenAL Soft to reopen or reset the active device.s_inforeports connection/recovery state when the runtime exposes it.snd_restartremains the deterministic full rebuild path when live recovery is unsupported or unsuccessful.
The main inspection commands are:
s_info: active backend, device, requested-vs-active OpenAL settings, source counts, EFX state, audio-zone status, latency/clock data when available, and music/raw state.s_list: registered sample list and load state.s_alListDevices: OpenAL playback device list.s_alListHrtfs: HRTF specifier list for the active or requested device.s_alConfigHints: OpenAL Soft config-file guidance and live capability hints.s_alDebugDump: current spatial environment and selected voice details.s_alDebugOverlay: in-game summary and detailed spatial debug overlay.
These diagnostics are part of the support surface. Prefer expanding them over adding hidden behavior when new OpenAL features need to be explained.
For source-layout, backend, or documentation changes, run at least a normal client build. For policy or sidecar changes, run the deterministic tests that match the touched area.
Meson:
meson compile -C meson/build fnquake3.x64 fnq3-audiozonesc fnq3_audio_zone_tests fnq3_audio_recovery_tests fnq3_audio_occlusion_tests fnq3_audio_loopback_tests
meson test -C meson/build fnq3_audio_zones fnq3_audio_zone_authoring_audit fnq3_audio_zone_sweep_script fnq3_audio_zone_material_map fnq3_audio_recovery fnq3_audio_occlusion fnq3_audio_loopback --print-errorlogsCMake:
cmake --build <build-dir> --target fnq3-audiozonesc fnq3_audio_zone_tests fnq3_audio_recovery_tests fnq3_audio_occlusion_tests fnq3_audio_loopback_tests
ctest --test-dir <build-dir> -R "fnq3_audio" --output-on-failurefnq3_audio_loopback_tests may skip when OpenAL Soft loopback is unavailable.
Zone runtime, recovery policy, and occlusion policy tests do not need real audio
hardware.
- Keep OpenAL changes behind
soundInterface_t; do not add a second public sound API. - Keep dedicated-server builds free of OpenAL runtime requirements.
- Keep OpenAL startup cvars latched and request-oriented.
- Keep ordinary stereo and authored surround content direct unless the user explicitly opts into stereo world-source spatialization.
- Keep
.azbfiles optional, data-only, and compatible across sidecar versions. - Keep audio source ownership under
code/client/audio/; avoid recreating a top-levelcode/audio/unless audio becomes a true qcommon/server subsystem. - Update
docs/AUDIO.mdfor player-facing controls and this file for architecture or source-layout changes. - Update
docs/templates/README.md.in, then runpython scripts/generate_docs.py, whenever README-facing audio text changes.