Audio
The audio sample is a playable sound board: two short tones and a looping drone, with volume, pitch and stereo pan controls. Its original PCM sounds and the built wave bank are in the repository. Build it in the developer shell:
cmake --preset x64-debugcmake --build --preset x64-debug --target AudioSample& ./out/build/x64-debug/samples/audio/AudioSample.exePress 1 for a ping, 2 for an ascending chime and 3 to start the drone.
Space pauses or resumes the drone; S stops it. Hold Up/Down for volume,
Left/Right for stereo pan and Q/E for pitch. Escape quits. The screen
shows the current values and loop state. An active Windows audio output is
required to hear the normal XAudio2 build; -null presets record calls without
sound and label that mode on screen.
Declare a required bank
Section titled “Declare a required bank”A bank has two files: a JSON definition and the XAudio2 backend’s .xwb wave
container. The manifest names their shared stem, without either extension:
{ "assets": [ { "kind": "font", "directory": "./fonts/", "names": ["courier_new_bold_16"] }, { "kind": "sound_bank", "directory": "./sounds/", "names": ["tones"], "optional": false } ]}samples/audio/content/manifest.json at 862e08b
This bank is required. A missing or unreadable container stops loading with its
path in the error. optional: true is only for content a game can run without:
it substitutes a silent bank and cannot demonstrate playback. A required bank
with a misspelt wave also fails during loading.
The definition lists waves and persistent effect instances:
{ "waves": ["ping", "chime", "drone"], "create_effect_instance_for_each_wave": false, "sound_effect_instances": [ {"name": "drone_loop", "wave": "drone"} ]}samples/audio/content/sounds/tones.json at 862e08b
ping and chime need only waves. The named drone_loop instance retains a
voice so it can loop, pause and change levels. Several named instances may use
the same wave when a game needs separately controlled voices. Setting
create_effect_instance_for_each_wave to true instead creates an instance with
each wave’s own name.
The sample’s content target
copies the bank beside the executable on every build, including asset-only
edits. Application::load_manifest("./manifest.json") resolves the manifest
relative to the executable, and its directories relative to that manifest.
Resolve names once
Section titled “Resolve names once”After loading the manifest, the state borrows the bank from AudioResources:
this->board_ = std::make_unique<SoundBoard>(bank);samples/audio/audio_state.cpp, lines 21–22 at 862e08b
The sound board resolves its wave and effect names in its constructor:
{ if (this->bank_ == nullptr || !this->bank_->audible()) { throw std::runtime_error("AudioSample requires the tones wave bank."); } this->ping_ = this->bank_->resolve_wave("ping"); this->chime_ = this->bank_->resolve_wave("chime"); this->drone_ = this->bank_->resolve_effect("drone_loop");}samples/audio/sound_board.cpp, lines 9–18 at 862e08b
Keep the handles, and play through them during update(). Wave and effect
handles are different types: a one-shot wave is not a persistent voice. The
borrowed bank must outlive the board; Application keeps audio resources alive
until its states are destroyed.
audible() distinguishes a loaded bank from the missing-content substitute.
It is also true on the null backend and says nothing about speakers, mute
settings or whether the machine has an active audio output.
One-shots and a looping instance
Section titled “One-shots and a looping instance”play_wave starts a separate voice each time, useful for hits, jumps and menu
clicks. There is no handle for stopping or adjusting that individual playback:
void SoundBoard::play_ping() const{ this->bank_->play_wave(this->ping_, this->volume_, this->pitch_, this->pan_);}samples/audio/sound_board.cpp, lines 25–28 at 862e08b
Use an effect instance when the voice must persist. The loop flag belongs to the play call, not the bank definition:
void SoundBoard::start_loop() const{ this->bank_->play_effect(this->drone_, true, this->volume_, this->pitch_, this->pan_);}samples/audio/sound_board.cpp, lines 35–39 at 862e08b
The same instance can be paused and resumed without starting a second voice:
void SoundBoard::toggle_pause() const{ { this->bank_->pause_effect(this->drone_); } else if (this->loop_state() == labrador::SoundState::paused) { this->bank_->resume_effect(this->drone_); }}samples/audio/sound_board.cpp, lines 41–51 at 862e08b
stop_effect(effect, true) stops immediately. With the default false, a
loop exits its loop and the platform can finish its tails; the observed state
can remain playing briefly. The sample uses immediate stop and also stops the drone on
state destruction.
Levels and lifecycle
Section titled “Levels and lifecycle”Volume is in [0, 1], pitch in [-1, 1] (one octave down to one octave up),
and pan in [-1, 1] (left to right). SoundBank clamps values at the engine
boundary. The sample also clamps its own displayed settings:
void SoundBoard::set_volume(float volume){ this->bank_->set_effect_volume(this->drone_, this->volume_);}samples/audio/sound_board.cpp, lines 58–62 at 862e08b
Changing a retained voice takes effect while it plays. One-shots take their levels when launched, so new settings affect the next one-shot. The sample pauses a playing loop on deactivation and resumes only that loop when focus returns; a loop paused by the player stays paused.
Application updates the audio device every tick and owns its lifetime. A
client that uses AudioDevice directly must update it and keep it alive longer
than all the banks that borrow it; the sample’s
finite audio check shows
that order without creating a graphics device.
Make sound content and verify it
Section titled “Make sound content and verify it”The three checked-in WAV files were synthesized by a Python standard-library script; they use no third-party audio and carry the repository’s MIT licence. The sample README records the exact tool version, provenance, regeneration commands and content verifier. Neither Python nor an authoring tool is needed to run the existing example.
For your own bank, Microsoft’s XWBTool combines WAV files:
xwbtool -f -o tones.xwb ping.wav chime.wav drone.wavThe -f is required: it retains each file’s stem as a friendly name in the
bank, which is how Labrador checks and resolves waves. Use an in-memory bank
for this API; streaming banks are outside this example.
AudioSample.exe --smoke-test loads the packaged required bank, plays both
one-shots, and verifies loop play, pause, resume and immediate stop before
exiting. Run the normal x64-debug or x64-release executable for actual
XAudio2 coverage. A missing bank or unavailable output fails that check.
Under a null preset the same check records the calls and reports no playback;
it cannot validate the container or sound quality. AudioSampleTests also
checks the real definition, and under null audio asserts exactly which waves
the sound board requests and the levels it sends.
Development documentation (unreleased). Built from Labrador 862e08b of 2026-10-08.