Audio System
Implementation status: SoundEffect, SoundEffectInstance, DynamicSoundEffectInstance, MediaPlayer, and Song are fully implemented via SDL3_mixer. The XACT runtime (AudioEngine, SoundBank, WaveBank, Cue) is now real too — a genuine .xgs/.xsb/.xwb parser plays back through SDL3_mixer, with category/lifecycle/3D/instance-limit and fade handling and continuous RPC volume/pitch curves. Microphone is also implemented, using real SDL3 capture devices. The Audio namespace is approximately ~97% API surface implemented; documented deviations only (no HRTF/elevation, no AttackTime/ReleaseTime envelope tracking).
Overview
CNA's audio system is split into two tiers. Tier 1 covers the SDL3_mixer-backed classes that provide fully functional playback of sound effects and music. Tier 2 covers the XACT subsystem — cue-based audio driven by .xgs/.xsb/.xwb content, now genuinely parsed and played back rather than stubbed.
SDL3_mixer is vendored as a Git submodule and built alongside CNA. It uses the MIX_Mixer track-based model internally: each SoundEffectInstance and each Song played through MediaPlayer occupies its own mixer track, rather than writing to a global shared output. This avoids the global-state pitfalls present in older SDL_mixer versions and maps cleanly onto the XNA per-instance API.
All audio classes live in the Microsoft::Xna::Framework::Audio namespace, with MediaPlayer and Song in Microsoft::Xna::Framework::Media, matching XNA 4.0 exactly.
Tier 1 — Implemented (SDL3_mixer)
SoundEffect
SoundEffect represents a loaded audio asset — typically a short sound effect stored as WAV or OGG. It is an immutable handle to decoded audio data. To play a sound once, call Play() directly. To control playback parameters, call CreateInstance() and manipulate the resulting SoundEffectInstance.
| Member | Description |
|---|---|
Play() | Fire-and-forget one-shot playback at default volume, pitch, and pan |
Play(volume, pitch, pan) | One-shot playback with explicit volume (0.0–1.0), pitch (−1.0–1.0), and pan (−1.0–1.0) |
CreateInstance() | Returns a SoundEffectInstance for controlled playback |
static FromStream(stream) | Loads a SoundEffect from an open stream (WAV data) |
Duration | Total length of the audio clip as a TimeSpan |
Name | Asset name set by ContentManager at load time |
The preferred way to load a SoundEffect is through ContentManager, but FromStream() is provided for loading from arbitrary sources such as network streams or embedded resources.
// One-shot play — simplest usage
auto boom = content.Load<SoundEffect>("audio/explosion");
boom->Play();
SoundEffectInstance
SoundEffectInstance is a controllable playback handle obtained from SoundEffect::CreateInstance(). Unlike a fire-and-forget Play() call, an instance lets you adjust volume, pitch, and pan at any time, pause and resume playback, and query the current playback state. Each instance holds its own mixer track.
| Member | Type / Range | Description |
|---|---|---|
Volume | float — 0.0–1.0 | Playback amplitude; 1.0 is full volume |
Pitch | float — −1.0–1.0 | Pitch shift in octaves; 0.0 is unmodified |
Pan | float — −1.0–1.0 | Stereo pan; −1.0 is full left, 1.0 is full right |
IsLooped | bool | When true the clip loops until Stop() is called |
State | SoundState | Current state: Playing, Paused, or Stopped |
Play() | — | Starts or resumes playback |
Pause() | — | Pauses playback, retaining position |
Resume() | — | Resumes from the paused position |
Stop() | — | Stops playback and rewinds to the beginning |
// SoundEffectInstance with volume, pitch, and pan control
auto sfx = content.Load<SoundEffect>("audio/laser");
auto instance = sfx->CreateInstance();
instance->SetVolume(0.7f);
instance->SetPitch(-0.3f); // slightly lower pitch
instance->SetPan(0.5f); // panned right
instance->SetIsLooped(false);
instance->Play();
// Later, in response to a game event:
if (instance->GetState() == SoundState::Playing) {
instance->Pause();
}
DynamicSoundEffectInstance
DynamicSoundEffectInstance allows streaming audio data from application-managed buffers rather than from a preloaded file. The engine fires the BufferNeeded event whenever its internal buffer queue runs low, signalling the application to call SubmitBuffer() with the next chunk of PCM data. This is suitable for procedurally generated audio, network audio streams, or decoded-on-the-fly music.
| Member | Description |
|---|---|
DynamicSoundEffectInstance(sampleRate, channels) | Constructs an instance with the given sample rate (Hz) and channel count (1 or 2) |
SubmitBuffer(data) | Enqueues a block of 16-bit signed PCM samples for playback |
BufferNeeded | Event fired when the buffer queue needs more data; subscribe with a callback |
Play(), Pause(), Stop() | Playback control, same semantics as SoundEffectInstance |
PendingBufferCount | Number of buffers currently queued but not yet consumed |
// DynamicSoundEffectInstance with BufferNeeded callback
auto dynSfx = std::make_shared<DynamicSoundEffectInstance>(44100, AudioChannels::Stereo);
dynSfx->BufferNeeded += [&](auto* sender, auto& args) {
// Generate or decode the next chunk of PCM data
std::vector<int16_t> samples = GenerateNextAudioChunk();
dynSfx->SubmitBuffer(samples);
};
// Submit an initial buffer before calling Play()
std::vector<int16_t> initialChunk = GenerateNextAudioChunk();
dynSfx->SubmitBuffer(initialChunk);
dynSfx->Play();
Song
Song represents a music track loaded through the ContentManager. Unlike SoundEffect, a Song is played exclusively through the MediaPlayer static class; only one song plays at a time. SDL3_mixer decodes the audio file progressively, making Song suitable for large music files that would be impractical to load entirely into memory.
| Member | Description |
|---|---|
Duration | Total length of the track as a TimeSpan |
Name | Asset name set by ContentManager at load time |
MediaPlayer
MediaPlayer is a static class that manages playback of a single Song at a time. It mirrors the XNA 4.0 Microsoft.Xna.Framework.Media.MediaPlayer API exactly. Volume, muting, and looping are controlled through static properties, and the current playback state is available via State.
| Member | Type / Range | Description |
|---|---|---|
Play(song) | static void | Starts playing the given Song, stopping any currently playing track |
Pause() | static void | Pauses the currently playing song |
Resume() | static void | Resumes from the paused position |
Stop() | static void | Stops playback and rewinds |
Volume | float — 0.0–1.0 | Music volume; independent of SoundEffect volume |
IsMuted | bool | Silences output without altering Volume |
IsRepeating | bool | When true the song loops automatically when it ends |
State | MediaState | Current state: Playing, Paused, or Stopped |
// MediaPlayer playing a Song
auto theme = content.Load<Song>("music/main_theme");
MediaPlayer::SetIsRepeating(true);
MediaPlayer::SetVolume(0.8f);
MediaPlayer::Play(theme);
// Mute on focus loss, restore on focus gain
void OnFocusLost() { MediaPlayer::SetIsMuted(true); }
void OnFocusGained(){ MediaPlayer::SetIsMuted(false); }
Loading audio via ContentManager
Both SoundEffect and Song are loaded through the standard ContentManager pipeline. Place audio files in your content directory; the path passed to Load<T>() is relative to ContentManager::RootDirectory and should omit the file extension. CNA resolves common audio extensions automatically (.wav, .ogg).
// Loading audio assets via ContentManager
ContentManager content(services, "Content");
// Load a short sound effect (WAV or OGG)
auto jumpSfx = content.Load<SoundEffect>("audio/jump");
// Load a music track (OGG recommended for large files)
auto bgMusic = content.Load<Song>("music/level1");
// Alternatively, load a SoundEffect from a raw stream
std::ifstream file("assets/custom.wav", std::ios::binary);
auto custom = SoundEffect::FromStream(file);
Tier 2 — XACT (AudioEngine/SoundBank/WaveBank/Cue)
XACT is now real (~97% functional). AudioEngine, SoundBank, and WaveBank genuinely parse .xgs/.xsb/.xwb content, and Cue plays it back through SDL3_mixer, including category routing, natural-completion state reconciliation, weighted variation selection, category/cue instance-limit enforcement with fade in/out, and continuous RPC volume/pitch re-evaluation every AudioEngine::Update() tick. Documented, intentional deviations remain: no HRTF/elevation modelling, no AttackTime/ReleaseTime envelope tracking.
| Class | Header | Status | Notes |
|---|---|---|---|
AudioEngine |
Present | Implemented | Real .xgs parsing, categories, global variables, Update() lifecycle sweep |
SoundBank |
Present | Implemented | Real .xsb parsing (compact + non-compact), PlayCue, GetCue |
WaveBank |
Present | Implemented | Real .xwb parsing (incl. ADPCM), lazy streaming reads |
Cue |
Present | Implemented | Real playback, category-/cue-level instance-limit enforcement with fade, continuous RPC curves |
XACT-driven audio now works directly in CNA — you no longer need to migrate AudioEngine/SoundBank/Cue calls away. For new CNA-only projects without existing XACT content, SoundEffect and SoundEffectInstance remain the simpler Tier 1 API and integrate directly with ContentManager.
Microphone
Microphone is implemented using real SDL3 capture devices: device enumeration, Start()/Stop(), and BufferReady delivery all work against genuine hardware input.