Skip to content

Latest commit

 

History

History
334 lines (273 loc) · 15.4 KB

File metadata and controls

334 lines (273 loc) · 15.4 KB

Modern Audio Engine

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.

Design Goals

  • 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 the s_info/s_list/snd_restart workflow 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. .azb audio-zone sidecars refine environment rendering, but missing or invalid sidecars must never break a map.

Source Layout

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.

Layout Assessment

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.

Runtime Architecture

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:

  1. S_Init registers sound cvars and commands.
  2. If s_initsound is enabled, codecs are initialized and the requested backend is tried.
  3. s_backend openal calls S_OpenAL_Init, which fills soundInterface_t through AudioSystem::Init.
  4. If OpenAL cannot initialize, startup reports the failure and calls S_Base_Init for the legacy software mixer.
  5. s_backendActive is set to openal, legacy, or none based on the backend that actually started.
  6. Later callers keep using classic S_* functions without knowing which backend is active.

OpenAL Backend Slices

AudioSystem.cpp includes private implementation slices inside an anonymous namespace. They are not public headers.

  • AudioSystemShared.inl defines cvar pointers, math helpers, sample format classification, extension helpers, source-class/tone policy, environment state, audio-zone loading, and shared formatting.
  • AudioSystemOpenAL.inl owns 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.inl owns registered samples, world voices, looping sound state, listener state, positional updates, occlusion smoothing, tone application, source budgeting, and spatial debug snapshots.
  • AudioSystemStreams.inl owns OpenAL buffer queues for music and raw sample streams.
  • AudioSystemBackend.inl owns the soundInterface_t facade, 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.

Spatial Rendering

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.

Environment Layer

The OpenAL backend adds an environmental layer on top of the normal source selection path.

  • Reverb uses EFX when the device supports it. s_alReverb is latched because the effect slot is created at backend init. The backend prefers AL_EFFECT_EAXREVERB (low-frequency decay control, echo hints, underwater modulation) and falls back to AL_EFFECT_REVERB when 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_alReverbGain and s_alOcclusionStrength scale 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.

Device Handling

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_alAutoRecover enables automatic live recovery attempts when supported.
  • s_alRecoverDevice manually asks OpenAL Soft to reopen or reset the active device.
  • s_info reports connection/recovery state when the runtime exposes it.
  • snd_restart remains the deterministic full rebuild path when live recovery is unsupported or unsuccessful.

Diagnostics

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.

Validation

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-errorlogs

CMake:

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-failure

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

Maintainer Checklist

  • 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 .azb files optional, data-only, and compatible across sidecar versions.
  • Keep audio source ownership under code/client/audio/; avoid recreating a top-level code/audio/ unless audio becomes a true qcommon/server subsystem.
  • Update docs/AUDIO.md for player-facing controls and this file for architecture or source-layout changes.
  • Update docs/templates/README.md.in, then run python scripts/generate_docs.py, whenever README-facing audio text changes.