Audio System
Implementation status: SoundEffect, SoundEffectInstance, DynamicSoundEffectInstance, MediaPlayer, and Song have real playback paths. XACT is a real parser and player, not a facade: CNA parses XGS/XWB/XSB data, applies FACT's volume formula and RPC curves, and implements the named boundaries below. 3D audio uses FAudio's Doppler calculation and F3DAudio attenuation. Alpha.1 contains 31 audio test sources with 689 statically discoverable GoogleTest-family definitions; the executed subset depends on the chosen build. Honest partials are listed in Tier 2.
Overview
CNA's audio system is split into two tiers. Tier 1 covers playback of sound effects and music. Its device selection is independent through CNA_AUDIO_PLATFORM, but alpha.1's values are not feature-equivalent: SDL3 is the default and the only choice that defines SOUND_ENABLED and links the SDL3_mixer playback/decoding engine. SDL2 and NULL provide low-level IAudioDevice implementations and selection/conformance coverage, while the mixer-dependent XNA playback facade is compiled without its real engine. Tier 2 covers XACT — cue-based audio driven by .xgs/.xsb/.xwb content — and real playback likewise requires the SDL3 choice.
It is worth being precise about what "real XACT" means here, because most XNA reimplementations stop at the API shape. CNA ships a real parser for the three XACT binary formats, and the playback side reproduces FACT's actual volume formula and evaluates real RPC (runtime parameter control) curves rather than approximating them. The 3D positional path uses FAudio's exact Doppler calculation and F3DAudio's attenuation model. The honest partials below are the places where that fidelity currently stops.
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 playback (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 a real parser and player, not a facade. AudioEngine, SoundBank, and WaveBank genuinely parse the XGS/XWB/XSB binary formats, and Cue plays the result back through SDL3_mixer using FACT's actual volume formula and real RPC curves. 3D cues are positioned with FAudio's exact Doppler calculation and F3DAudio attenuation.
| Class | Header | Status | Notes |
|---|---|---|---|
AudioEngine |
Present | Implemented | Real XGS parsing; drives RPC curve evaluation |
SoundBank |
Present | Implemented | Real XSB parsing, PlayCue, GetCue |
WaveBank |
Present | Implemented | Real XWB parsing. XMA and WMA wave formats throw. |
Cue |
Present | Implemented | Real playback through SDL3_mixer with FACT's volume formula. Only the first PlayWave per track is honored. |
XACT-driven audio works directly in CNA — you do not 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.
Honest partials
Three limitations are worth knowing before you point a real XACT project at CNA:
- Only the first
PlayWaveper track is honored. Cues authored with multiple waves on a single track will not play them all. - XMA and WMA throw. Wave banks using either compressed format cannot be played; re-encode to a supported format.
- The reverb send is a no-op. The API accepts it, but no reverb is applied to the signal.
Microphone
Microphone is implemented using real SDL3 capture devices: device enumeration, Start()/Stop(), and BufferReady delivery all work against genuine hardware input.