Generated: 2026-07-17
Basis: independent deep source/test/fixture audit.
Important: the previous contents of this file were intentionally not read and were replaced from scratch, as requested.
Backup: the untouched original file is preserved as plan_audio_20260717_previous_unread.md; it was copied without inspection.
Companion report: docs/audio_deep_audit_2026-07-17.md
Task count: 438 (224 P0, 193 P1, 21 P2).
Reach evidence-based XNA 4.0 audio compatibility: correct sample rate and pitch, complete supported content loading, truthful errors/state, deterministic XACT behavior, safe streaming/lifetimes, and measurable cross-platform output. “Sounds okay” is not completion; every core behavior needs a reproducible fixture and numerical or differential acceptance evidence.
- Audio Issue: High-pitched sound effects reported — determine whether incorrect source sample rate, device conversion, explicit pitch, XACT cents/RPC/variation, or Doppler is changing speed/pitch.
- Audio Issue: Some audio files appear missing or fail to load — audit path/case/packaging and add required XNB/XWB codec support; no silent null/return is acceptable.
- User Report: Gameplay audio sounds distorted or sped up — measure playback rate, duration, dominant frequency, sample format, channels, and output-device negotiation against original XNA.
These three boxes may only be checked after the exact game/assets have matched XNA/CNA captures, a proven root cause, regression tests, and before/after evidence.
Progress note (2026-07-17, no exact game/assets available -- see AUD-01): built a real offline
render/measurement harness (AUD-03) and used it to rule out one entire hypothesis class:
SDL3_mixer's own resampler, run directly via MIX_CreateMixer()/MIX_Generate(), correctly
preserves frequency and duration when a correctly-declared 22050 Hz or 48000 Hz source is
rendered through CNA's hard-coded 44100 Hz mixer spec (AudioMixer.cpp), in both directions
(OfflineAudioRendererTests.cpp: Source22050HzThroughRenderMixer44100HzPreservesFrequency,
Source48000HzThroughRenderMixer44100HzPreservesFrequency,
Source44100HzThroughRenderMixer48000HzPreservesFrequency). A parallel test
(MisdeclaredSourceRateReproducesExactlyDoubleFrequencySignature) confirms that declaring a
22050 Hz buffer AS 44100 Hz reproduces the audit's exact reported 2x-speed/+1-octave signature.
This means, absent the exact affected game, the most likely remaining root causes are upstream
of the mixer's resampler itself -- a wrong sample rate reaching SoundEffect's raw constructor
or the XNB reader (AUD-05/AUD-06, not yet fixed), or a pitch/RPC/Doppler contributor being
double-applied (AUD-08/AUD-09/AUD-10, largely not yet audited this pass) -- not the mixer/backend
choice itself. Still needs AUD-01's actual differential capture to become a proven, closeable root
cause for a specific game; this is ruling out one whole hypothesis class, not closing the ticket.
- P0: correctness/reproduction/data-loss/silence/distortion/state-safety blocker; complete before broad feature work.
- P1: parity, robustness, cross-platform, performance, and major completeness work.
- P2: lower-risk completeness, optional extensions, tools, and polish after P0/P1 gates.
- A task is complete only when implementation, automated test, negative test, documentation, and relevant platform evidence exist.
- Suspected defects must first be demonstrated by a minimal fixture. Do not “fix” parser/math behavior from comments alone.
- Do not tune by ear before the offline capture/measurement harness exists.
- Preserve XNA defaults. NOXNA enhancements must be opt-in and must not alter default XNA-compatible behavior.
- Neutral pitch final ratio:
1.0within floating-point tolerance. - Calibration tone frequency: initial gate ±0.1%.
- Offline PCM duration/frame count: exact; end-to-end resampled duration: initial gate ±2 ms.
- No silent SDL/MIX failure and no public
Playingstate after failed play. - No missing shipped asset and no unsupported shipped codec without build-time failure/conversion.
- Zero ASan/UBSan/LSan findings; zero confirmed TSan race in supported configurations.
Establish an independent, reproducible audio program and prevent “fixed by ear” regressions.
- AUD-00-001 [P0] Preserve the reported high-pitch, missing-file, and distorted-audio reports as named release blockers. Acceptance: Each report has an issue ID, affected title/build/platform, owner, and closure evidence.
- AUD-00-002 [P0] Create an audio compatibility charter defining XNA 4.0 parity versus documented NOXNA extensions. Acceptance: Every behavior is classified as exact parity, acceptable backend divergence, extension, or unsupported.
- AUD-00-003 [P0] Create a source-of-truth audio architecture document. Acceptance: Document static, dynamic, XACT, media, microphone, mixer, device, and content-loading paths with ownership.
- AUD-00-004 [P0] Record the exact SDL and SDL_mixer commits used by every supported build. Acceptance: Build output and crash reports expose exact revisions and relevant compile options.
- AUD-00-005 [P0] Pin audio dependencies instead of relying on moving submodule heads. Acceptance: A clean checkout resolves byte-identical dependency revisions.
- AUD-00-006 [P0] Create an audio feature/format/platform support matrix. Acceptance: Matrix covers API, codec/container, channels, sample rate, XNB/XACT, capture, and each target platform.
- AUD-00-007 [P0] Define severity rules for silence, distortion, timing drift, and parity differences. Acceptance: Triage rules map symptoms to P0/P1/P2 consistently.
- AUD-00-008 [P0] Define evidence required before closing an audio defect. Acceptance: Closure requires a minimal fixture, regression test, before/after capture, and documented root cause.
- AUD-00-009 [P1] Add ownership labels for loader, decoder, mixer, XACT, media, microphone, and platform integration. Acceptance: Every plan item has a maintainership area.
- AUD-00-010 [P1] Create a compatibility-difference register against XNA/FNA/MonoGame. Acceptance: Known intentional and accidental differences are searchable and versioned.
- AUD-00-011 [P1] Add an audio change checklist to pull-request guidance. Acceptance: Checklist requires tests for format, duration, pitch, lifetime, and platform impact.
- AUD-00-012 [P1] Define a policy for backend approximations such as 3D and reverb. Acceptance: Approximations require documented math, limits, and golden tests.
- AUD-00-013 [P1] Create a test-asset provenance and license manifest. Acceptance: Every committed fixture has origin, license, generator, format, and expected hash.
- AUD-00-014 [P1] Add a generated inventory of public Audio and Media APIs. Acceptance: Inventory is compared automatically with the chosen XNA 4.0 reference surface.
- AUD-00-015 [P1] Add a generated inventory of SDL/MIX calls used by CNA audio. Acceptance: Every call is tagged checked/unchecked and covered/uncovered.
- AUD-00-016 [P2] Document non-goals for platform services unavailable outside Xbox/Windows Phone. Acceptance: Unsupported APIs have explicit compatible behavior instead of accidental stubs.
Turn the user report into a deterministic differential case with matched assets, code, and recordings.
- AUD-01-001 [P0] Obtain the exact original XNA C# revision and the corresponding C++ CNA revision. Acceptance: Both revisions are archived by commit hash and build instructions.
- AUD-01-002 [P0] Identify every affected sound by logical name and physical asset path. Acceptance: A table maps gameplay event → C# load path → C++ load path → file/XNB/XACT entry.
- AUD-01-003 [P0] Record original XNA and CNA output from the same gameplay event. Acceptance: Recordings use the same asset, event timing, output sample rate, and no post-processing.
- AUD-01-004 [P0] Capture one high-pitched effect in isolation outside gameplay. Acceptance: Minimal XNA and CNA programs play exactly one sound once with neutral parameters.
- AUD-01-005 [P0] Capture one reportedly missing effect in isolation. Acceptance: Logs identify whether failure occurs at path lookup, parse, decode, voice creation, or play.
- AUD-01-006 [P0] Capture one distorted effect in isolation. Acceptance: The original bytes and every interpreted format field are preserved.
- AUD-01-007 [P0] Hash and compare original and ported source assets. Acceptance: Byte differences are either eliminated or explained.
- AUD-01-008 [P0] Hash and compare built content products. Acceptance: XNB/XWB/XSB/XGS differences are documented by pipeline/tool version.
- AUD-01-009 [P0] Extract metadata with two independent tools. Acceptance: Sample rate, channels, bit depth, codec, block alignment, frame count, and loops agree or discrepancy is explained.
- AUD-01-010 [P0] Log the exact C++ constructor/loader overload used for each affected sound. Acceptance: No affected sound has an ambiguous raw-vs-container interpretation.
- AUD-01-011 [P0] Log all runtime pitch contributors for each affected voice. Acceptance: Base pitch, cents, RPC, random variation, Doppler, clamp, and final ratio are captured.
- AUD-01-012 [P0] Repeat the CNA capture with pitch and Doppler forcibly neutral. Acceptance: Result classifies issue as metadata/decoder versus pitch-composition.
- AUD-01-013 [P0] Repeat the CNA capture with reference-decoded PCM. Acceptance: Result isolates loader/decoder from mixer/playback.
- AUD-01-014 [P0] Write CNA-decoded PCM to WAV before playback. Acceptance: Offline WAV analysis isolates decoder output from live device conversion.
- AUD-01-015 [P0] Compare duration ratios numerically. Acceptance: Report distinguishes 2.0, 48k/44.1k, 44.1k/48k, channel, and arbitrary drift signatures.
- AUD-01-016 [P0] Compare dominant frequency and spectral centroid. Acceptance: Measured frequency/pitch shift is reported in ratio and semitones.
- AUD-01-017 [P0] Compare channel waveforms independently. Acceptance: Channel swap, duplication, interleave, and pan differences are identified.
- AUD-01-018 [P0] Repeat on a 44.1 kHz and 48 kHz output device. Acceptance: Device-dependent changes are either reproduced or ruled out.
- AUD-01-019 [P0] Repeat with static SoundEffect, dynamic stream, and XACT where applicable. Acceptance: The defective path is isolated.
- AUD-01-020 [P0] Repeat stationary and moving 3D variants. Acceptance: Doppler-specific differences are isolated.
- AUD-01-021 [P1] Capture stdout/stderr and structured audio trace with recordings. Acceptance: Every recording has a matching machine-readable trace.
- AUD-01-022 [P1] Record OS, backend, device, driver, and negotiated format. Acceptance: Reproduction package is portable to another machine.
- AUD-01-023 [P1] Create a one-command reproduction script. Acceptance: Script builds/runs both available references and emits analysis artifacts.
- AUD-01-024 [P1] Add the minimal reproduction to CI as a non-device offline test. Acceptance: The reported defect cannot regress silently.
Make every sample-rate, format, pitch, and backend decision observable without a debugger.
- AUD-02-001 [P0] Introduce a structured
AudioDiagnosticEventmodel. Acceptance: Events include severity, operation, backend error, asset/cue/wave identity, thread, and timestamp. - AUD-02-002 [P0] Add a runtime audio trace switch disabled by default. Acceptance: Trace can be enabled by environment/config without recompiling.
- AUD-02-003 [P0] Log requested and actual mixer/device audio specifications. Acceptance: Format, channels, rate, buffer/quantum, driver, and device name are emitted once.
- AUD-02-004 [P0] Log every loaded sound source format before conversion. Acceptance: Container, codec, bit depth, channels, rate, block align, frame count, and loops are recorded.
- AUD-02-005 [P0] Log every final track frequency ratio and its components. Acceptance: Trace provides base pitch, cue cents, RPC cents, random cents, Doppler, and final clamp.
- AUD-02-006 [P0] Log dynamic stream source and destination specs after track attachment. Acceptance: A null/invalid destination spec becomes a hard diagnostic failure.
- AUD-02-007 [P0] Check and propagate
SDL_CreateAudioStreamfailure. Acceptance: No public object enters a usable/playing state with a null stream. Evidence:DynamicSoundEffectInstance::Play()now checksaudioStream_afterEnsureStream()and returns (leaving state Stopped,std::cerrdiagnostic) before ever callingMIX_SetTrackAudioStream-- previously that call would "succeed" on a null stream (SDL docs: passing NULL is legal and just detaches input) and playback would proceed to a falsePlayingstate with total silence. See AUD-07-007's shared implementation/tests. - AUD-02-008 [P0] Check and propagate every
SDL_PutAudioStreamDatafailure. Acceptance: Failed chunks are not counted as submitted; state and callback behavior remain consistent. Evidence:SubmitQueuedToStream()now checks the return value; a failed chunk is dropped (not pushed tosubmittedChunkSizes_) with astd::cerrdiagnostic including the dropped byte count, rather than being credited toPendingBufferCountforever with no way to ever decrement (Update()'s consumed-byte accounting would never reach it). Chose "fail deterministically" over "retain for retry" from the acceptance's two options: SDL_PutAudioStreamData failures are allocation/param-level, and an unconditional retry loop risks spinning forever on a persistent failure with no equivalent retry concept in real FNA to justify the complexity. See AUD-07-009. - AUD-02-009 [P0] Check and propagate every
MIX_PlayTrackfailure. Acceptance: Public state remains Stopped and failure includes SDL error context. Evidence:Play()now capturesMIX_PlayTrack's return value (from both the properties and no-properties call sites) and returns before settingState_ = Playing/registering withFrameworkDispatcher::Streamsif it failed, with astd::cerrdiagnostic includingSDL_GetError(). See AUD-07-010. - AUD-02-010 [P0] Check all
MIX_SetTrackFrequencyRatiocalls. Acceptance: Invalid ratios fail loudly and do not leave cached state inconsistent. - AUD-02-011 [P0] Check all
MIX_SetTrackGainand mixer-gain calls. Acceptance: Rejected values are visible and public properties remain truthful. - AUD-02-012 [P0] Check all pan/stereo/filter callback registration calls. Acceptance: A failed effect setup cannot masquerade as successful parity.
- AUD-02-013 [P0] Replace raw stderr-only XACT/WaveBank failures with structured events. Acceptance: Applications can subscribe/log and tests can assert exact error codes.
- AUD-02-014 [P0] Replace silent MediaPlayer load/create/play returns with truthful state and diagnostics. Acceptance: Play failure cannot start the wall-clock or report Playing.
- AUD-02-015 [P0] Include content candidate paths in missing-asset diagnostics. Acceptance: Error shows every attempted path, extension, root, and case mismatch hint.
- AUD-02-016 [P1] Add per-voice debug IDs. Acceptance: Lifecycle, submissions, parameter changes, and stop callbacks correlate reliably.
- AUD-02-017 [P1] Add mixer counters for active, virtual, failed, and exhausted voices. Acceptance: Voice exhaustion is distinguishable from content failure.
- AUD-02-018 [P1] Add dynamic stream counters for queued input bytes, output frames, underruns, and failed puts. Acceptance: Counters are internally consistent after resampling.
- AUD-02-019 [P1] Add decoder timing and allocation diagnostics. Acceptance: Slow or allocation-heavy assets are identifiable.
- AUD-02-020 [P1] Add one-shot warning suppression by unique issue key. Acceptance: Repeated failures do not flood logs while first context is preserved.
- AUD-02-021 [P1] Add diagnostic JSON export. Acceptance: A bug report can attach a machine-readable audio session.
- AUD-02-022 [P1] Add a debug command to dump currently active voices and parameters. Acceptance: Dump is race-safe and includes final effective values.
- AUD-02-023 [P1] Add a debug command to dump XACT cue resolution. Acceptance: Selected sound/track/wave, variations, RPC outputs, and categories are shown.
- AUD-02-024 [P2] Add privacy-safe device diagnostics. Acceptance: No personally identifying capture-device data is emitted by default.
Prove what CNA produces numerically rather than relying on public state or listening alone.
- AUD-03-001 [P0] Build a deterministic offline audio render harness. Acceptance: It renders tracks to a buffer/file without physical hardware or wall-clock timing. Evidence:
tests/Microsoft/Xna/Framework/Audio/OfflineAudioRenderer.hpp'sRenderRawPcmOffline()usesMIX_CreateMixer()+MIX_Generate()(NOTMIX_CreateMixerDevice()) -- genuinely no physical device, noSDL_AUDIODRIVER, no wall-clock timing at all; every test using it runs identically headless or with real hardware present.MIX_Init()/MIX_Quit()are called per-render (refcounted, safe alongsideCNA::Internal::Audio::GetMixer()'s own shared device mixer). - AUD-03-002 [P0] Add a canonical WAV writer for captured output. Acceptance: Headers, channel layout, sample count, and hashes are deterministic. Not yet done this pass -- all evidence so far is compact numeric assertions (AUD-03-014), not exported WAV files.
- AUD-03-003 [P0] Add generated sine, impulse, step, silence, noise, sweep, and multitone fixtures. Partial evidence:
GenerateSineWaveS16/GenerateSilenceS16added and used throughout the new golden tests. Impulse/step/noise/sweep/multitone fixtures not yet added -- left unchecked. - AUD-03-004 [P0] Add dominant-frequency measurement. Acceptance: 440 Hz and other tones are measured within the configured threshold. Evidence:
GoertzelMagnitude(exact single-bin energy, no FFT bin-width uncertainty),EstimateDominantFrequencyHz(coarse blind sweep, for when the expected frequency isn't known ahead of time), andRefineFrequencyEstimateHz(phase-difference estimator between the first/second half of the buffer -- NOT limited by the analysis window's basic bin-width resolution the way a Goertzel/FFT peak search is, which is what actually achieves the plan's own 0.1% calibration gate even from short 0.2s windows). 25+ new tests measure real tones to within 0.1% across an 11025-96000 Hz x mono/stereo matrix and a full -1.0..+1.0 pitch-ratio matrix -- see AUD-05/AUD-08 below. - AUD-03-005 [P0] Add sample/frame-count and duration measurement. Acceptance: No test depends only on sleeping for approximate time. Evidence: every
OfflineAudioRendererTest/AUD05GoldenMatrix/AUD08GoldenPitchMatrixtest asserts exact frame counts (result.samples.size()) and/orresult.realBytesRendered(MIX_Generate's own non-silence byte count) -- zero wall-clock sleeps anywhere in this new test file. - AUD-03-006 [P0] Add per-channel RMS, peak, DC offset, clipping, and correlation metrics. Partial evidence:
MeasureRms/MeasurePeak/ContainsNaNOrInfadded and used (silence-is-truly-zero, sine-is-not-silence, no NaN/Inf-through-resampling checks). DC offset, clipping, and cross-channel correlation metrics not yet added -- left unchecked. - AUD-03-007 [P0] Add spectral comparison with windowing and tolerances. Acceptance: Compressed/resampled outputs can be compared robustly.
- AUD-03-008 [P0] Add transient/loop-boundary click detection. Acceptance: Unexpected discontinuities fail golden tests.
- AUD-03-009 [P0] Add channel-order test signals. Acceptance: Every channel has a unique tone/impulse signature.
- AUD-03-010 [P0] Add silence/tail detection. Acceptance: Truncation and unexpected decoder tails are measurable.
- AUD-03-011 [P0] Add latency measurement separated from duration. Acceptance: Startup latency is not misdiagnosed as speed error.
- AUD-03-012 [P0] Add XNA/FNA reference-capture import. Acceptance: Reference WAV plus metadata can be normalized and compared automatically.
- AUD-03-013 [P0] Version the comparison algorithm and thresholds. Acceptance: Golden results do not change silently with analysis code.
- AUD-03-014 [P0] Store compact numerical golden data rather than large opaque audio where possible. Acceptance: Repository remains reviewable and fixtures reproducible. Evidence: every new golden test's expected values are small numeric literals (expected Hz, expected pitch ratio) computed from source-generated fixtures at test time -- no committed binary audio blobs.
- AUD-03-015 [P1] Add AB listening export for human review. Acceptance: Tool emits level-matched A/B/X files without replacing objective gates.
- AUD-03-016 [P1] Add spectrogram and waveform artifact generation for CI failures. Acceptance: Failures provide immediate visual evidence.
- AUD-03-017 [P1] Add fuzz-safe parsers for captured metadata sidecars. Acceptance: Malformed test inputs cannot crash the harness.
- AUD-03-018 [P1] Add deterministic dithering/no-dithering controls. Acceptance: Sample comparisons account for conversion policy.
- AUD-03-019 [P1] Add resampler impulse and swept-sine characterization. Acceptance: Passband, aliasing, phase, and latency are documented.
- AUD-03-020 [P1] Add pan-law characterization. Acceptance: Center attenuation and extreme-channel isolation are measured.
- AUD-03-021 [P1] Add filter frequency/Q characterization. Acceptance: Actual response matches intended coefficients across sample rates.
- AUD-03-022 [P1] Add voice-mixing linearity and clipping tests. Acceptance: Summed voices follow documented headroom/clamp behavior.
- AUD-03-023 [P1] Add reproducibility checks across compiler optimization levels. Acceptance: Golden metrics stay within tolerance in Debug/Release.
- AUD-03-024 [P2] Add optional high-resolution float capture. Acceptance: Analysis can inspect pre-device output without quantization masking.
Guarantee that the mixer/device boundary never changes speed or hides failure.
-
AUD-04-001 [P0] Query and store the actual mixer output specification after creation. Acceptance: Requested and actual specs are both available and tested. Evidence (2026-07-17, A-06):
AudioMixer.cpp'sGetMixer()now callsMIX_GetMixerFormat(g_mixer, &actual)immediately after a successfulMIX_CreateMixerDevice()and logs the requested spec (S16 stereo 44100 Hz, the constant CNA always requests) against the actually negotiatedformat/channels/freqtostderr-- previously this was completely unobservable, a real diagnostic gap for investigating any 44.1/48 kHz-family pitch report. Query failure itself is logged rather than fatal (best-effort diagnostic, matching this file's existingstd::cerrconvention). New testAudioMixerTest.ActualMixerFormatIsQueryableAfterCreation(AudioMixerTests.cpp) callsGetMixer()directly and assertsMIX_GetMixerFormatsucceeds with sanefreq/channels, proving the actual spec is genuinely queryable post-creation, not just printed once and discarded. Full suite: 4704 passed / 0 failed / 2 skipped (unrelated Accelerometer/Gyroscope hardware-support tests). -
AUD-04-002 [P0] Verify 44.1 kHz request on a 48 kHz device does not alter pitch. Acceptance: 440 Hz remains within tolerance and duration remains correct. Adjacent evidence, not yet this item specifically:
OfflineAudioRendererTests.cpp'sSource44100HzThroughRenderMixer48000HzPreservesFrequencyproves the mixer's own resampler (source rate != mixer render rate, both viaMIX_CreateMixer()) preserves frequency in this direction. This item is about device negotiation specifically (SDL requesting one spec fromMIX_CreateMixerDevice()and the OS/driver actually opening a different physical rate) -- a separate layer this offline (non-device) harness cannot exercise by design. Still open. -
AUD-04-003 [P0] Verify 48 kHz request on a 44.1 kHz device does not alter pitch. Acceptance: Reverse conversion passes the same gates. Adjacent evidence: see AUD-04-002 --
Source22050HzThroughRenderMixer44100HzPreservesFrequency/Source48000HzThroughRenderMixer44100HzPreservesFrequencycover the mixer-resampler direction; real device-negotiation testing remains open. -
AUD-04-004 [P0] Make mixer rate/channels/format configurable for tests. Acceptance: Tests can force 22.05/44.1/48/96 kHz and mono/stereo where supported. Evidence:
AudioMixer.cpp/.hppgained test-onlySetMixerSpecOverrideForTests(spec)/ClearMixerSpecOverrideForTests(), guarded by the same mutex asg_mixer;GetMixer()uses the override (if set) instead of the hard-coded S16/stereo/44100 default on its next mixer-creating call (i.e. afterDestroyMixer()). New parameterized testAUD04004/AudioMixerSpecOverrideTest.OverriddenSpecIsActuallyNegotiated(AudioMixerTests.cpp) forces 22050/44100/48000/96000 Hz x mono/stereo through the realMIX_CreateMixerDevice()path under the dummy driver and queriesMIX_GetMixerFormat()after. Real finding surfaced by this test (documented "where supported" caveat): SDL3 itself (OpenPhysicalAudioDevice,third_party/SDL/src/audio/SDL_audio.c:1810-1826) imposes a hard floor of S16/stereo/44100 Hz on every physical playback device it opens (DEFAULT_AUDIO_PLAYBACK_CHANNELS=2,DEFAULT_AUDIO_PLAYBACK_FREQUENCY=44100,SDL_sysaudio.h) -- "We impose a simple minimum on device formats... This prevents something low quality... from ruining a music thing playing at CD quality." Requests at/above the floor (44100/48000/96000, stereo) pass through exactly; requests below it (22050, mono) are silently raised to the floor -- confirmed empirically for all 5 param cases, not merely read from a comment. Test assertsactual == max(requested, floor), not naive equality, so it locks in the real SDL behavior rather than a false expectation. This means 22.05 kHz and mono are not achievable as a physical device spec on this SDL3 build regardless of what CNA requests -- a genuinely new, verified fact for AUD-04-002/003/005/006's remaining device-negotiation work: CNA's own hard-coded production request (S16 stereo 44100 Hz) already sits exactly on this floor, so there is no downward device-negotiation risk in this SDL build; the only remaining device-negotiation pitch-risk direction is upward (an OS default device that only natively offers 48/96/192 kHz), which AUD-04-002/003 still need to verify and which this dummy-driver harness cannot exercise (the dummy backend has no independent "native" format of its own to negotiate against -- seeSDL_dummyaudio.c'sDUMMYAUDIO_OpenDevice, "don't change reported device format"). Full whole-repo suite: 4709 passed / 0 failed / 2 skipped (unrelated hardware-support tests) -- also confirms the per-testDestroyMixer()/override/restore cycle in the new test does not corrupt the shared process-wide mixer singleton for any other test in the binary. -
AUD-04-005 [P0] Choose and document the production mixer-format policy. Acceptance: Policy is native-device, fixed-reference, or platform-specific with rationale. Evidence: documented in
NEXTaudio.md§6 "Mixer output-format policy (AUD-04-005)" -- CNA's existing behavior (always request S16 stereo 44100 Hz, unchanged by this task) is the fixed-reference policy; native-device and platform-specific were considered and explicitly rejected with rationale (determinism, the AUD-04-004-confirmed SDL device-open floor already sitting at the same spec, and avoiding a wider verification matrix for no identified benefit). No code change -- this task closes the open "which policy and why" documentation gap, not a behavior change. -
AUD-04-006 [P0] Validate mixer creation against unsupported requested specs. Acceptance: Fallback is explicit, logged, and cannot silently change speed. Investigated -- confirmed already safe via two independent, already-existing SDL3 validation layers, no code change needed, locked down with new tests: using AUD-04-004's override mechanism, empirically drove
GetMixer()through 5 genuinely-invalid specs (freq<=0,channels<=0,channels>8). All 5 throwstd::runtime_errorfromMIX_CreateMixerDevicefailing -- none silently substitute an unrequested rate/channel count. Root cause traced to real SDL3 source (third_party/SDL/src/audio/SDL_audiocvt.c'sSDL_SetAudioStreamFormat, called viaSDL_CreateAudioStreaminsideSDL_OpenAudioDeviceStream): it validates the original, unclamped app-side spec (freq<=0rejected outright; channels must be inSDL_IsSupportedChannelCount's 1-8 range) as a separate, stricter gate thanOpenPhysicalAudioDevice's AUD-04-004 floor-clamp (which only raises too-small-but-still-valid values like 22050 Hz/mono, never touches<=0/>8). So the two SDL-level mechanisms compose correctly for CNA's purposes: below-floor-but-valid requests are silently (but observably, via AUD-04-001's log) raised to the documented 44100 Hz/stereo floor, while genuinely invalid requests are rejected outright with an exception -- there is no code path where an unsupported spec produces a mixer running at an arbitrary, unexplained rate. New tests:AUD04006/AudioMixerInvalidSpecThrowsTest(5 param cases: freq 0/-1, channels 0/-1/9) all assertEXPECT_THROW(GetMixer(), std::runtime_error). Full whole-repo suite: 4714 passed / 0 failed / 2 skipped (unrelated hardware-support tests). -
AUD-04-007 [P0] Test device-open failure and retry without leaked MIX init references. Acceptance: Repeated failures leave balanced lifecycle and accurate exceptions. Evidence: new
AudioMixerTest.RepeatedDeviceOpenFailuresLeaveBalancedLifecycleAndSubsequentSuccessIntact(AudioMixerTests.cpp) drivesGetMixer()through 5 consecutive failures on the same invalid spec (channels=0, AUD-04-006's rejection path), each asserted to throwstd::runtime_error(accurate exceptions, every time -- not just the first), then clears the override and confirms a completely ordinaryGetMixer()call still succeeds immediately afterward with the correct default spec (S16/stereo/44100), proving the 5 failed attempts left no residue ing_mixer/SDL's audio subsystem refcount forAudioMixer.cpp's existing IN-11MIX_Init()/MIX_Quit()pairing to leak. No production code change -- this is a regression lock on already-correct lifecycle behavior. Full whole-repo suite: 4715 passed / 0 failed / 2 skipped (unrelated hardware-support tests). -
AUD-04-008 [P0] Test mixer destruction with active static voices. Acceptance: No use-after-free, deadlock, callback-after-destroy, or leak. Two confirmed real defects found and fixed (not merely tested):
- UAF on
SoundEffectInstance::track_.AudioMixer::DestroyMixer()frees everyMIX_Trackthe mixer owns (MIX_DestroyMixer->MIX_DestroyTrack->SDL_aligned_free, confirmed by readingthird_party/SDL_mixer/src/SDL_mixer.c), but nothing ever told a liveSoundEffectInstance/DynamicSoundEffectInstanceitstrack_pointer had just been freed -- every accessor (getStateProperty,Play,Stop,Pause,Resume,Dispose, theINTERNAL_apply*Filterfamily) only null-checkedtrack_, never validated it was still live. Fix:AudioMixergained a monotonicGetMixerGeneration()counter, bumped byDestroyMixer()only when it actually destroys a mixer.SoundEffectInstancegainedtrackMixerGeneration_(captured the instant a track is created) and a newGetLiveTrackHandle()accessor that returnsnullptr(clearingtrack_as a side effect) whenever the captured generation no longer matches current -- every one of the ~13AsTrack(track_)call sites inSoundEffectInstance.cpp, plusDestroyTrackSafe(now generation-checked before callingMIX_StopTrack/MIX_DestroyTrack), now goes through it. Proven via the git-stash pattern: with the fix stashed out, the new deterministic (non-subprocess) testSoundEffectInstanceTest.MixerDestructionOrphansTrackWithoutUseAfterFree(SoundEffectInstanceTests.cpp) fails exactly as expected (GetTrack(inst)-- a raw, bypass-the-check accessor -- stays non-null after the first post-DestroyMixer()call, proving the check never ran); with the fix restored it passes,track_is provably nulled by the very first accessor call afterDestroyMixer(), not merely "didn't crash." - A second, deeper, more severe defect found while chasing this:
MIX_DestroyMixer()also callsSDL_QuitSubSystem(SDL_INIT_AUDIO)internally (when destroying a real device-backed mixer) -- if that call brings SDL's global audio subsystem refcount to zero, it fully deinitializes the subsystem process-wide, silently breaking every OTHER independently-ownedSDL_AudioStreamCNA still holds (confirmed by an ASan-symbolized crash:SDL_WasInit(SDL_INIT_AUDIO)observed0at the crash site,SEGVinsideSDL_UnbindAudioStream_REALwhenDynamicSoundEffectInstance::DestroyStream()later calledSDL_DestroyAudioStream()on its own stream -- see AUD-04-009 below for the fix, which closes this for both classes since it's a subsystem-level fix inAudioMixer.cpp, not a per-class one).
New isolated-subprocess harnesses (
tools/audio/mixer_destroy_active_static_voice_harness.cpp, wired viacmake/Harnesses.cmake/cmake/UnitTests.cmake) also exist as an end-to-end safety net (AudioMixerTest.MixerDestructionWithActiveStaticVoiceDoesNotCrashOrUseAfterFree, spawned the same wayP9-HARDWARE-005's no-hardware harness is) -- these were the tool used to discover both defects above via ASan (SDL_WasInit/__pthread_mutex_lockSEGV), even though a plain non-ASan run of the isolated static-voice case alone didn't reliably crash (the freed-memory read happened to be benign in that specific scenario -- the deterministic in-process test above is the real regression guard, not this harness's exit code alone). Full whole-repo suite: 4719 passed / 0 failed / 2 skipped (unrelated hardware-support tests), confirmed clean 3x under ASan (audio-scoped subset) with only the already-documentedAUD-15-001libdrm.so.2/<unknown module>graphics-driver leak-noise baseline, nothing new. - UAF on
-
AUD-04-009 [P0] Test mixer destruction with active dynamic streams. Acceptance: Streams/tracks are detached in a defined order. Shares AUD-04-008's
MIX_Track-generation-check fix (DynamicSoundEffectInstanceshares the base class'strack_/trackMixerGeneration_, P13-DYNAMIC-001) -- its own independentAsTrackD(track_)call sites (getStateProperty,Play's resume-from-Paused and new-track-creation paths,Stop(bool)'s early-return guard,StopInternal's inline track teardown) were separately updated to go through the sameGetLiveTrackHandle(), andStopInternal's formerly-unconditionalMIX_StopTrack/MIX_DestroyTrackcalls are now generation-guarded the same wayDestroyTrackSafeis. The subsystem-deinitialization defect described in AUD-04-008 was actually found via this class specifically:DynamicSoundEffectInstance::DestroyStream()callingSDL_DestroyAudioStream()on its own independently-ownedaudioStream_(never bound to anyMIX_Track, created directly viaSDL_CreateAudioStreaminEnsureStream()) segfaulted afterDestroyMixer()ran, becauseMIX_DestroyMixer's internalSDL_QuitSubSystem(SDL_INIT_AUDIO)call had brought the global audio subsystem refcount to zero. Fix (inAudioMixer.cpp, benefits both classes):GetMixer()now acquires one extra, permanently-heldSDL_InitSubSystem(SDL_INIT_AUDIO)reference on first use and never releases it, soDestroyMixer()'s internalSDL_QuitSubSystemcall can never bring the subsystem's refcount below 1 -- confirmed fixed by re-running the exact crash repro (SDL_WasInit(SDL_INIT_AUDIO)no longer returns 0 afterDestroyMixer()) and by the crash no longer reproducing across 3x ASan runs of the full audio-scoped test filter (previously 100% reproducible in that filter,AddressSanitizer:DEADLYSIGNAL/SEGV ... in __pthread_mutex_lockinsideSDL_UnbindAudioStream_REAL<-SDL_DestroyAudioStream_REAL<-DynamicSoundEffectInstance::DestroyStream()). Proven via the git-stash pattern the same way as AUD-04-008: the new deterministic testDynamicSoundEffectInstanceTest.MixerDestructionOrphansTrackWithoutUseAfterFree(DynamicSoundEffectInstanceTests.cpp) fails against pre-fix code -- and notably, for this class specifically,getStateProperty()itself returned the wrong live value (Playing, notStopped) when reading the freed track's memory pre-fix, a concrete demonstration that the old behavior was genuinely undefined, not merely "coincidentally looked right." New isolated-subprocess harnesstools/audio/mixer_destroy_active_dynamic_voice_harness.cpp/AudioMixerTest.MixerDestructionWithActiveDynamicVoiceDoesNotCrashOrUseAfterFreealso added as an end-to-end safety net (this harness's own internal state-check did reliably fail pre-fix, unlike the static-voice one). "Streams/tracks are detached in a defined order" is satisfied by the existingStopInternal()/DestroyStream()ordering (track first, then stream), now made safe under the orphaned-mixer case by the two fixes above. Full whole-repo suite: 4719 passed / 0 failed / 2 skipped. -
AUD-04-010 [P1] Implement output-device change handling. Acceptance: Default-device changes either migrate safely or stop with a documented event.
-
AUD-04-011 [P1] Implement device-loss/reopen handling. Acceptance: State recovery is deterministic and does not speed up queued audio.
-
AUD-04-012 [P1] Measure and document output latency/quantum by backend. Acceptance: Latency settings are not conflated with playback duration.
-
AUD-04-013 [P1] Add low/high-latency configuration bounds. Acceptance: Invalid values fail predictably; supported values are tested.
-
AUD-04-014 [P1] Verify master volume is applied exactly once. Acceptance: Track and mixer gains do not double-multiply or omit master gain. Investigated -- confirmed already correct via real SDL3_mixer source + empirical RMS measurement:
MIX_SetTrackGain(used forSoundEffectInstance::Volume) delegates toSDL_SetAudioStreamGain(track->output_stream, gain)-- applied when the mixer's group-mixing loop pulls from that stream (SDL_GetAudioStreamData).MIX_SetMixerGain(used forSoundEffect::MasterVolume,setMasterVolumePropertyinSoundEffect.cpp) setsmixer->gain, applied separately byMixFloat32Audiowhen accumulating that already-track-gained sample into the group mix buffer (both read directly fromthird_party/SDL_mixer/src/SDL_mixer.c). Two genuinely distinct multiplicative pipeline stages -- structurally impossible to double-apply or omit either one. Empirically confirmed, not just read from source:OfflineAudioRenderer.hpp'sRenderRawPcmOfflinegained optionaltrackGain/mixerGainparameters (wired toMIX_SetTrackGain/MIX_SetMixerGain); new testsTrackGainAloneScalesRmsLinearly,MixerGainAloneScalesRmsLinearly, and the core claimTrackAndMixerGainComposeMultiplicativelyNotDoubleAppliedOrOmitted(0.5×0.5 measured RMS ratio ≈0.25, distinguishing this from a double-apply reading of ≈0.0625 or an omit-one-factor reading of ≈0.5) all pass against the real decode/mix pipeline. No code change. -
AUD-04-015 [P1] Verify mixer gain changes affect active and future voices consistently. Acceptance: Behavior matches chosen XNA baseline. Evidence: new test
MixerGainChangeMidPlaybackAffectsActiveVoiceOnNextChunk(OfflineAudioRendererTests.cpp) plays a track, pulls one 0.1s chunk viaMIX_Generate, callsMIX_SetTrackGainon the same still-playing track (no Stop/Play cycle, no new track), pulls a second chunk, and confirms the second chunk's measured RMS is ≈0.25x the first (matching the new gain) -- proving a gain change takes effect live on an already-active voice's very next generated chunk, not only on voices created after the change. The "future voices" half of the acceptance is already covered by AUD-04-014's tests: every one of those creates a brand-new track withMIX_SetTrackGain/MIX_SetMixerGaincalled beforeMIX_PlayTrack, so there is no code path where a freshly created track could see a stale gain. No production code change -- SDL3_mixer's per-chunk gain read (traced in AUD-04-014's evidence) already behaves this way by construction. -
AUD-04-016 [P1] Verify no clipping/NaN propagation for extreme aggregate gain. Acceptance: Output remains finite and documented. Evidence: new tests
ExtremeAggregateGainRemainsFiniteNoNaNOrInf(trackGain=mixerGain=1000, real decoded output asserted finite viaContainsNaNOrInfand non-zero peak -- SDL3_mixer's float32 pipeline has no hard clamp, so extreme gain legitimately produces a large-but-finite value, not distortion-via-wraparound or NaN) andZeroTrackGainRendersSilenceNotNaN(trackGain=0 renders clean all-zero silence, not a NaN from a degenerate 0-gain edge case). Both pass against the existing, unmodified mixing pipeline -- no code change. -
AUD-04-017 [P1] Test null/dummy audio drivers separately from physical output. Acceptance: Dummy success is not accepted as proof of audible correctness.
-
AUD-04-018 [P1] Add backend capability querying. Acceptance: Unsupported effects/formats are known before play.
-
AUD-04-019 [P1] Add mixer thread-affinity and callback-thread documentation. Acceptance: Public API and internal locks comply with the contract.
-
AUD-04-020 [P2] Benchmark conversion cost for common source/device rate pairs. Acceptance: Performance budget includes resampling and channel conversion.
Prevent C++ callers from accidentally labeling bytes with the wrong rate, width, or channel count.
- AUD-05-001 [P0] Validate raw SoundEffect sample rate before backend calls. Acceptance: Zero, negative, overflow, and unsupported rates throw XNA-compatible exceptions. Investigated -- resolved decision, matches FNA, acceptance corrected: real FNA's own internal
SoundEffectconstructor (SoundEffect.cs) does zero C#-level validation ofsampleRateat all, relying entirely on the native backend (FAudio) to reject an invalidWAVEFORMATEX(same pattern already resolved forDynamicSoundEffectInstance's constructor,P10-DYN-001..003). Empirically confirmed via a direct probe that CNA's own backend call (MIX_LoadRawAudio) already rejectsfreq<=0outright (NULL, "unknown/unsupported/corrupt format"), which the existingif (!raw) throw NotSupportedException(...)guard safely converts -- no crash, no garbageSoundEffect. New testsBufferRangeConstructorWithZeroSampleRateThrowsNotSupported/WithNegativeSampleRateThrowsNotSupportedlock this down. Not adding CNA-side pre-validation, since real XNA docs promisingArgumentOutOfRangeExceptionhere are the same documented-vs-actual-FNA-behavior split this project already resolved in FNA's favor elsewhere. - AUD-05-002 [P0] Validate raw channel enum before backend calls. Acceptance: Only supported XNA channel values are accepted unless an explicit extension is used. Investigated -- resolved decision, matches FNA: same reasoning as AUD-05-001 -- FNA does no C#-level channel validation either;
MIX_LoadRawAudioalready rejectschannels<=0(confirmed via the same probe). New testBufferRangeConstructorWithZeroChannelsThrowsNotSupportedlocks this down. - AUD-05-003 [P0] Validate raw byte count is aligned to a complete sample frame. Acceptance: Misaligned buffers fail deterministically instead of truncating or distorting. Investigated -- confirmed already safe, acceptance's literal "fail" not implemented (truncates instead), documented as the chosen behavior: empirically confirmed via a direct probe that SDL3_mixer's
MIX_LoadRawAudiohandles a non-frame-aligned byte count (e.g. 401 bytes of stereo S16) by cleanly ignoring the trailing partial frame (decodes as exactly 100 frames, identical to a clean 400-byte buffer) -- not a crash, not corrupted/distorted audio, and matches FNA's own total lack of frame-alignment validation. New testBufferRangeConstructorWithMisalignedByteCountTruncatesCleanlylocks this graceful-truncation behavior down rather than adding a throw that would diverge from FNA for no safety benefit (the current behavior already meets this task's real underlying goal -- no distortion -- even though it truncates rather than throwing). - AUD-05-004 [P0] Validate loop start/length against decoded frame count where parity permits. Acceptance: Out-of-range loops follow verified XNA/FNA behavior and never reach backend unchecked. Investigated -- resolved decision, matches an already-resolved prior decision (
P9-VALIDATION-002), confirmed against real FNA source and empirically against the real backend: FNA's own internalSoundEffectconstructor (SoundEffect.cs) does zero C#-level validation ofloopStart/loopLengthagainst the decoded frame count --this.loopStart = (uint) loopStart;unconditionally, relying entirely on the native backend to behave safely. CNA's constructor already matches this (P9-VALIDATION-002's comment inSoundEffect.cpp, pre-existing, unchanged). What AUD-05-004 adds: empirical confirmation that SDL3_mixer (CNA's backend) also degrades gracefully for an out-of-range loop region rather than reaching it "unchecked" in any memory-unsafe sense -- read directly fromthird_party/SDL_mixer/src/SDL_mixer.c:MIX_PlayTrackclampsloop_startto>=0only (no upper-bound check at play time), and the mixing loop's loop-back path (decoder->seek(track->decoder_userdata, track->loop_start)) either succeeds (decoder-level clamp) or fails cleanly (track_stopped = true), never a crash or garbage read; an oversizedmax_framesimply never triggers the early-clamp comparison, falling back to the decoder's own natural EOF detection. New testLoopRegionFarBeyondDecodedLengthDegradesGracefullyNoCrashNoNaN(OfflineAudioRendererTests.cpp) empirically confirms this: a 2205-frame real source withloopStart=22050/maxFrame=220500(10x/100x past the real content) rendered for 8820 frames (4x the real source length, guaranteeing the nonsensical loop points are actually hit) produces no crash and no NaN/Inf. No production code change -- matches FNA's own documented lack of C#-level validation, and the backend already handles the out-of-range case safely. - AUD-05-005 [P0] Document that raw constructor bytes are PCM16LE, not a WAV/container. Acceptance: API docs and exception message prevent accidental whole-file submission. Evidence: both raw-buffer constructors' Doxygen comments (
SoundEffect.hpp) now explicitly state the "headerless, little-endian, signed 16-bit PCM... NOT a WAV/RIFF file" contract, name the specific consequence of misuse (leading container header misread as samples, producing noise/silence not a clean failure), and point atSoundEffect(const std::string&)as the correct file-loading alternative. The "exception message" half is satisfied differently than a literal wording change: real backend-level rejection (NotSupportedException) is only reachable for spec-level failures (freq/channels the backend itself rejects, already covered by AUD-05-001/002), never for "right shape, wrong content" container misuse -- that misuse mode is what AUD-05-006's new diagnostic (below) actually catches, since no exception is thrown for it in real FNA/CNA either. - AUD-05-006 [P0] Add debug detection for RIFF/Ogg/MP3/XNB signatures passed to raw PCM constructor. Acceptance: Likely misuse emits a precise diagnostic. Evidence: new
DetectLikelyContainerSignature()helper (SoundEffect.cpp) checks the leading bytes of the range constructor's[offset, offset+count)slice forRIFF/OggS/ID3/XNBmagic bytes and prints a precisestd::cerrdiagnostic naming the detected format and the correct fix -- advisory only, never throws (matches AUD-05-005's constructor contract: the backend still "successfully" decodes container bytes as garbage PCM, so there is no exception path to hook this into even if desired). New testsRawBufferStartingWithRiffSignatureEmitsDiagnosticWithoutThrowing/RawBufferStartingWithXnbSignatureEmitsDiagnosticWithoutThrowing(SoundEffectTests.cpp, viatesting::internal::CaptureStderr()) confirm the diagnostic fires and construction still succeeds;RawBufferWithoutKnownSignatureEmitsNoDiagnosticconfirms ordinary raw PCM (no known signature) produces zero extra stderr output, ruling out false positives for the common case. Full whole-repo suite: 4729 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-05-007 [P0] Add debug detection for implausible PCM16 statistics indicating float/compressed data. Acceptance: Diagnostic is advisory and has no release false rejection. Evidence: new
LooksImplausiblyHighEntropyForPcm16()helper (SoundEffect.cpp) computes Shannon entropy over the raw byte histogram of the[offset, offset+count)slice (checked only whenDetectLikelyContainerSignatureabove found no known container header, avoiding a double-diagnosis of the same misuse) and prints an advisorystd::cerrdiagnostic when entropy exceeds 7.9 bits/byte (of a theoretical max of 8.0) -- compressed/encoded bitstreams (Ogg/MP3 without a recognizable header, or float32 samples byte-reinterpreted as PCM16) approach this ceiling, while real quantized 16-bit audio essentially never does (adjacent samples/channels stay correlated). Deliberately conservative threshold, never throws, no release rejection. New tests:RawBufferWithHighEntropyRandomDataEmitsDiagnosticWithoutThrowing(deterministic-seed PRNG bytes, a reasonable proxy for compressed data's near-uniform byte distribution, triggers the diagnostic; construction still succeeds) andRawBufferWithRealSineWaveEmitsNoEntropyDiagnostic(a real continuous 440 Hz tone produces zero extra stderr output, ruling out a false positive on ordinary game audio). Full whole-repo suite: 4731 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-05-008 [P0] Verify duration calculation uses decoded frames and source rate. Acceptance: Duration remains correct after backend conversion. Evidence: pre-existing
ConstructFromBufferAndPropertiesalready asserts an exact expected duration (1024.0/44100.0seconds for a 1024-frame stereo buffer,EXPECT_NEARnot just non-zero); the newBufferRangeConstructorWithMisalignedByteCountTruncatesCleanly(AUD-05-003) extends this to the truncated-frame case (100.0/44100.0, not the naive byte-count-implied duration). - AUD-05-009 [P0] Verify mono/stereo interleaving with asymmetric test signals. Acceptance: No channel duplication/swap/misalignment. Already satisfied by a pre-existing AUD-03 harness self-test, cross-referenced here (no new code):
OfflineAudioRendererTest.StereoChannelsAreIndependentlyMeasurable(OfflineAudioRendererTests.cpp) builds a stereo buffer with genuinely asymmetric per-channel content -- left channel 440 Hz, right channel 880 Hz -- and measures each channel independently viaRefineFrequencyEstimateHz. A channel swap bug would make the 440 Hz measurement appear on the right/880 Hz measurement appear on the left; a duplication bug would make both channels read 440 Hz (or both 880 Hz); a misalignment (off-by-one interleaving) bug would corrupt both measured frequencies away from either expected value -- all three failure modes are within this single test's discriminating power, and it currently passes. - AUD-05-010 [P1] Define endianness policy on non-little-endian targets. Acceptance: PCM input is converted or rejected explicitly.
- AUD-05-011 [P1] Add a typed NOXNA audio-buffer descriptor for richer formats. Acceptance: Extensions carry codec/format/rate/channels/block alignment explicitly.
- AUD-05-012 [P1] Avoid ambiguous integer casts from arbitrary channel values. Acceptance: Static analysis/tests reject invalid enum construction paths. Investigated -- resolved decision, matches FNA's identical gap, empirically documented:
AudioChannelsis a scoped enum (Mono=1, Stereo=2); C++ has no language-level protection against a caller force-casting an out-of-range value (static_cast<AudioChannels>(5)), and FNA's own constructor has the identical gap (SoundEffect.csdoes(ushort) channelsunconditionally, no validation) -- CNA intentionally matches this rather than adding a restriction FNA itself doesn't have (would diverge from real XNA/FNA behavior for no parity benefit, the same reasoning already applied to AUD-05-001/002). What was empirically verified instead: the backend's actual behavior for an out-of-range value passed through unchanged. New testOutOfRangeChannelsEnumValueEitherConstructsOrThrowsCleanlyNeverUBconfirmschannels=5constructs successfully --MIX_LoadRawAudio's RAW decoder accepts an arbitrary positive channel count unvalidated at load time (unlikeMIX_CreateMixerDevice's stream-format path, which enforcesSDL_IsSupportedChannelCount's 1-8 range, but only at play time -- AUD-04-006) -- and that the resultingSoundEffectbehaves sanely (not disposed, no corruption), with the test also accepting a cleanNotSupportedExceptionas a valid outcome should a future SDL3_mixer version start validating this earlier. No production code change. Full whole-repo suite: 4736 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-05-013 [P1] Check
MIX_LoadRawAudioownership/copy semantics against pinned version. Acceptance: Buffer lifetime is safe and documented. Investigated and confirmed safe -- CNA's rawSoundEffectconstructor does not need the caller's buffer to outlive the call: read the pinnedthird_party/SDL_mixer/src/SDL_mixer.cdirectly.MIX_LoadRawAudio->MIX_LoadRawAudio_IOnever setsMIX_PROP_AUDIO_LOAD_ONDEMAND_BOOLEAN(only the separateMIX_LoadRawAudioNoCopy, which CNA does not use, sets it true "so it doesn't make a copy to precache" -- an explicit signal the other function does copy).MIX_LoadAudioWithProperties'selse if (!ondemand)branch (the oneMIX_LoadRawAudioalways takes) callsSDL_LoadFile_IO(io, &audio->precachelen, false), reading the entire source into a freshSDL_malloc'd buffer independent of the caller's original memory. Empirically locked down (not just read from source): new testRawBufferLifetimeIsIndependentOfSourceMemoryAfterConstructionoverwrites the source buffer with0xCDand destroys it immediately after constructing theSoundEffect, then confirmsDurationandCreateInstance().Play()both still behave correctly. Full whole-repo suite: 4735 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-05-014 [P1] Test zero-length raw buffers. Acceptance: Behavior matches reference and never divides by zero. Evidence: new test
ZeroLengthRawBufferConstructsWithZeroDurationNoCrashconstructs from an empty buffer and confirmsDuration == TimeSpan::Zero, no crash -- matchesgetDurationProperty()'s existingframes > 0guard (SoundEffect.cpp, pre-existing, never divides by a zero frame count; division is always bysampleRate, separately guarded bysampleRate > 0). - AUD-05-015 [P1] Test very short one-frame/two-frame buffers. Acceptance: No off-by-one duration or callback error. Evidence: new tests
OneFrameRawBufferHasExactSingleFrameDuration/TwoFrameRawBufferHasExactTwoFrameDuration(4-byte and 8-byte stereo S16 buffers) confirm exact1/44100/2/44100second durations within1e-6tolerance (this file's established convention,ConstructFromBufferAndProperties) -- SDL3_mixer's own duration computation carries a small, fixed sub-microsecond rounding error independent of buffer length (confirmed empirically: an initial1e-9tolerance attempt failed by ~5-8e-8 seconds on both cases, negligible for any real audio purpose but revealing the fixed-not-frame-count-scaled nature of the rounding). - AUD-05-016 [P1] Test very large buffers near API/backend limits. Acceptance: Overflow is prevented before allocation/backend calls. Evidence: new tests
HugeCountAgainstSmallBufferThrowsBeforeReachingBackend/HugeOffsetNearIntMaxThrowsBeforeReachingBackend/OffsetPlusCountThatWouldOverflowInt32ThrowsCleanly(SoundEffectTests.cpp) confirmoffset/countvalues nearSharpRuntime::intcs's (int32) ceiling, checked against a genuinely small real buffer, are rejected by the existingP9-VALIDATION-003unsigned-arithmetic bounds check (off > buffer.size() || cnt > buffer.size() - off,SoundEffect.cpp) beforebuffer.data() + offsetis ever computed or the backend is reached -- including the exact overflow-prone case that check exists for (offsetnearINT32_MAXplus a smallcount, which would overflow a plainint32sum if computed naively). No real multi-gigabyte allocation was needed: the bounds check depends only on the (small) real buffer failing to contain the (huge) claimed range, not on how large the real buffer actually is. No production code change -- locks down already-correct, pre-existing behavior. Full whole-repo suite: 4739 passed / 0 failed / 2 skipped (unrelated hardware-support tests). This closes AUD-05's entire P0/P1 list exceptAUD-05-010/011(endianness policy, NOXNA buffer descriptor -- both design-decision-sized, deliberately deferred rather than self-started). - AUD-05-017 [P1] Golden-test raw PCM16LE at 8000 Hz mono. Acceptance: Frequency, duration, frame count, channel identity, and neutral ratio pass thresholds. Evidence:
AUD05GoldenMatrix/GoldenSampleRateTest(OfflineAudioRendererTests.cpp), param(8000, 1)-- 220 Hz tone measured within 0.1% viaRefineFrequencyEstimateHz, exact frame count asserted. - AUD-05-018 [P1] Golden-test raw PCM16LE at 8000 Hz stereo. Acceptance: as above. Evidence: same test, param
(8000, 2). - AUD-05-019 [P1] Golden-test raw PCM16LE at 11025 Hz mono. Evidence: param
(11025, 1). - AUD-05-020 [P1] Golden-test raw PCM16LE at 11025 Hz stereo. Evidence: param
(11025, 2). - AUD-05-021 [P0] Golden-test raw PCM16LE at 22050 Hz mono. Evidence: param
(22050, 1); also independently reproduced end-to-end viaSource22050HzThroughRenderMixer44100HzPreservesFrequency(resampled through CNA's actual hard-coded 44100 Hz mixer rate). - AUD-05-022 [P0] Golden-test raw PCM16LE at 22050 Hz stereo. Evidence: param
(22050, 2). - AUD-05-023 [P1] Golden-test raw PCM16LE at 32000 Hz mono. Evidence: param
(32000, 1). - AUD-05-024 [P1] Golden-test raw PCM16LE at 32000 Hz stereo. Evidence: param
(32000, 2). - AUD-05-025 [P0] Golden-test raw PCM16LE at 44100 Hz mono. Evidence: param
(44100, 1). - AUD-05-026 [P0] Golden-test raw PCM16LE at 44100 Hz stereo. Evidence: param
(44100, 2). - AUD-05-027 [P0] Golden-test raw PCM16LE at 48000 Hz mono. Evidence: param
(48000, 1); alsoSource48000HzThroughRenderMixer44100HzPreservesFrequency. - AUD-05-028 [P0] Golden-test raw PCM16LE at 48000 Hz stereo. Evidence: param
(48000, 2). - AUD-05-029 [P1] Golden-test raw PCM16LE at 96000 Hz mono. Evidence: param
(96000, 1). - AUD-05-030 [P1] Golden-test raw PCM16LE at 96000 Hz stereo. Evidence: param
(96000, 2). All 14AUD05GoldenMatrixcases pass; this closes the entire golden sample-rate matrix (AUD-05-017..030). Note: these tests exercise the real SDL3_mixer decode/resample pipeline directly (RenderRawPcmOffline), not yetSoundEffect's own raw constructors -- AUD-05's validation items (001-016, e.g. sample-rate/channel/frame-alignment checks on the public constructor) are a separate, not-yet-done task (see task #12/AUD-05-001..016 below).
Eliminate the confirmed missing-audio gap caused by PCM16-only XNB loading.
- AUD-06-001 [P0] Create an authoritative XNB SoundEffect format support matrix by XNA target platform/profile. Acceptance: Every accepted/rejected combination has a reference fixture and rationale. Evidence: matrix documented in
SoundEffectContentTypeReader.hpp's class doc comment andSoundEffectContentTypeReaderTests.cpp's file header; every row has a real MonoGame-produced.xnbfixture (tests/assets/xnb/monogame/windows/uncompressed/audio/tone_mono_44khz_{8bit,16bit,float,msadpcm,imaadpcm}.xnb, plus stereo 16-bit) except XMA2 (no such fixture exists in MonoGame's own test corpus either; covered by a hand-built minimal object stream instead). - AUD-06-002 [P0] Preserve
nAvgBytesPerSec,nBlockAlign, and full format extension data. Acceptance: Decoder receives complete WAVEFORMATEX metadata. Evidence:SoundEffectContentTypeReader.cppnow capturesnAvgBytesPerSec/nBlockAligninto named locals (previously read-and-discarded) and captures the format extension bytes verbatim intoextensionData(previously always discarded via a bareReadBytesExactOrThrowwith no assignment) for every non-XMA2 format. - AUD-06-003 [P0] Refactor XNB SoundEffect construction away from the PCM16-only raw constructor. Acceptance: A format-aware internal path owns metadata and encoded bytes safely. Evidence: new
BuildViaWavWrapper()(format-aware: wraps raw bytes in a synthetic in-memory WAV matching the real WAVEFORMATEX fields, decoded viaSoundEffect::FromStream/SDL3's own WAV loader) handles every format except 16-bit PCM, which deliberately keeps the existing direct-construction fast path unchanged (no WAV-wrapping overhead for the already-correct, most common case). - AUD-06-004 [P0] Support 8-bit PCM XNB SoundEffect where reference behavior requires it. Acceptance: Included fixture decodes with exact frame count and correct unsigned-to-signed conversion. Evidence:
Pcm8BitLoadsSuccessfullyagainst the real fixture; unsigned-to-signed 8-bit conversion is SDL3's own native WAV-decoder responsibility (not reimplemented in CNA), consistent with routing every non-16-bit format through the same real decoder. Exact frame-count cross-check against the fixture's own declared sample count not yet added (duration-is-positive only) -- a reasonable follow-up, not blocking. - AUD-06-005 [P0] Support 16-bit PCM XNB SoundEffect without regression. Acceptance: Existing fixture remains sample/duration correct. Evidence: unchanged fast path;
Pcm16BitMonoLoadsSuccessfully/Pcm16BitStereoLoadsSuccessfullystill pass, full whole-repo suite reverified green. - AUD-06-006 [P0] Support MS ADPCM XNB SoundEffect where reference behavior requires it. Acceptance: Block alignment, samples/block, duration, loops, and decoded output are verified. Evidence:
MsAdpcmLoadsSuccessfullyagainst the real fixture. Confirmed real defect found and fixed en route (AUDIO-XNB-ADPCM-001): hex-dumped the real fixture and found MonoGame's content pipeline writescbSize=0for MS-ADPCM (no coefficient table, nowSamplesPerBlockat all in the embedded format block) -- unlike IMA-ADPCM, SDL3's MS-ADPCM decoder has no auto-derive fallback and requires an explicit, valid extension (SDL_wave.c'sMS_ADPCM_Init). Fixed by synthesizing the standard extension (BuildStandardMsAdpcmExtension+ awSamplesPerBlockcomputed fromnBlockAlignvia the MS-ADPCM "Standards Update" formula) whenever the XNB's own extension is absent/too small to contain a real coefficient table, while still preferring a real authored extension if one is ever present. Loop points forwarded via a synthesized minimal WAVsmplchunk (AppendSmplChunkIfLooped), picked up automatically bySoundEffect::FromStream's existingTryParseWavSmplChunk(CP-17). - AUD-06-007 [P0] Determine and implement IMA ADPCM XNB compatibility policy. Acceptance: Decision is based on real XNA/MonoGame/FNA content, with fixture and explicit behavior. Evidence: decision: support via SDL3's native IMA-ADPCM decoder (WAV-wrapped), same as MS-ADPCM/float/PCM8 -- confirmed against the real fixture (
ImaAdpcmLoadsSuccessfully), which worked immediately (SDL auto-deriveswSamplesPerBlockfromnBlockAlignwhen the XNB doesn't supply an extension, unlike MS-ADPCM). - AUD-06-008 [P0] Determine and implement IEEE float XNB compatibility policy. Acceptance: Float fixture either loads correctly or fails with documented profile-compatible reason. Evidence: decision: support via SDL3's native IEEE-float WAV decoder;
IeeeFloatLoadsSuccessfullyagainst the real fixture. - AUD-06-009 [P0] Determine and implement XMA2 compatibility strategy. Acceptance: Native decode, conversion-at-build, optional decoder, or explicit unsupported result is tested and documented. Evidence: decision unchanged from the prior session (explicit unsupported result -- no decode path exists anywhere in this stack, SDL3 doesn't decode XMA2 either) but now has real regression coverage:
Xma2IsRejectedbuilds a complete, valid minimal XNB object stream (type-reader table + shared-resource count + root type id, reverse-engineered from a real fixture's hex dump) around an XMA2 format block, confirming the rejection path is reachable through the realReadAsset<T>()entry point, not just throughSoundEffectReader::Read()called directly. - AUD-06-010 [P0] Use stored XNB duration as a validation oracle. Acceptance: Large decoded-duration disagreement fails with asset-specific diagnostics. Evidence: the
.xnb's own storeddurationfield (previously read-and-discarded as "unused, matches FNA") is now compared against the actually-decodedSoundEffect's duration via newValidateDecodedDurationAgainstStoredOracle(), wired into both the direct-construction 16-bit PCM fast path and theBuildViaWavWrapperpath inSoundEffectReader::Read(). Threshold was empirically calibrated (temporary debug instrumentation, since removed) against all 6 real MonoGame fixtures undertests/assets/xnb/monogame/windows/uncompressed/audio/: uncompressed PCM/float formats match the stored duration exactly (to whole-millisecond rounding), ADPCM formats drift by only a few percent from legitimate block-rounding -- comfortably inside a deliberately generous 2x/0.5x ratio threshold, chosen to catch only order-of-magnitude-class misinterpretations (wrong sample rate, wrong channel count, a doubled/halved frame count from a format mixup) without risking any real, legitimately-authored asset.storedDurationMs == 0is treated as "not meaningfully set" and skipped entirely -- many real assets across the ecosystem leave this field at its default, and FNA itself never populates/validates it either, so treating an unset value as an error would be a real compatibility regression. New testsDrasticDurationOracleDisagreementThrowsWithBothValues(a 100x disagreement: 50ms real audio vs. 5000ms stored, expectsContentLoadExceptionnaming both values and the asset name) andSmallDurationOracleDisagreementDoesNotThrow(a 52ms-vs-50ms plausible rounding difference, expects no throw). Git-stash regression-verified: stashing onlySoundEffectContentTypeReader.cpp(keeping the new tests) makesDrasticDurationOracleDisagreementThrowsWithBothValuesfail as expected (no exception thrown pre-fix) whileSmallDurationOracleDisagreementDoesNotThrowstill trivially passes; popping the stash and rebuilding restores both to green. Full whole-repo suite: 4752 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-06-011 [P0] Validate XNB format length and extension sizes exhaustively. Acceptance: Truncation/overflow/extra-byte cases cannot desynchronize the reader. Investigated -- confirmed already safe by construction, empirically locked down at both ends of the range:
formatLength(auint32_t) drivesint64_t skip = formatLength - 18[- 34]thenstatic_cast<int32_t>(skip). Traced the full value range: sinceformatLengthcan be at mostuint32_t's own ceiling (~4.29 billion),skipcan never exceed that range either, so the int32 narrowing always produces EITHER the correct non-negative value (small/validformatLength) OR a negative value (large/corruptformatLength) -- there is no reachableformatLengththat makesskipwrap around TWICE into a small-but-wrong positive count, which would have been the actual desync risk. The negative case is already caught byReadBytesExactOrThrow's existingcount < 0guard (ContentLoadException). New testsFormatLengthOneByteTooSmallForCbSizeThrowsCleanly(formatLength=17, one byte short of covering even its owncbSizefield,skip=-1) andPathologicallyLargeFormatLengthThrowsCleanlyNotDesync(formatLength=0xFFFFFFF0, near theuint32_tceiling) both confirm a cleanContentLoadException, not a crash, OOB read, or silently-wrong extension-byte count. No production code change. Full whole-repo suite: 4746 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-06-012 [P0] Validate data length before allocation/read. Acceptance: Negative, oversized, and truncated lengths fail safely. Investigated -- confirmed already safe via existing shared infrastructure, empirically locked down:
SoundEffectReader::Read()passes its declared audio-data length straight toinput.ReadBytesExactOrThrow(input.ReadInt32(), "SoundEffectReader")with no length check of its own, but that's safe because the shared helper already handles all three cases:ReadBytesExactOrThrow(ContentReader.cpp) rejects a negative count outright with aContentLoadExceptionbefore ever callingReadBytes;BinaryReader::ReadBytes()(sharp-runtime) clamps its eager allocation to the stream's own real remaining length ("A seekable stream can never deliver more bytes than its own remaining length -- clamp the eager allocation to that bound instead of the raw (possibly adversarial)count") before allocating anything, so a corrupt/adversarial declared length can never force an allocation for bytes that could never be read anyway; a genuinely truncated stream still throwsSystem::IO::EndOfStreamExceptiononce the actual byte count comes up short. New testsNegativeDataLengthFailsCleanlyRatherThanCrashing(ContentLoadException) andOversizedDataLengthAgainstTruncatedStreamFailsCleanlyNotWithHugeAllocation(claims 100 MB against a stream that actually ends immediately after the header --EndOfStreamException, no 100 MB allocation attempt, both tests complete in ~0ms) confirm this specifically throughSoundEffectReader's own data-length field, not just the shared helper in isolation. No production code change. Full whole-repo suite: 4743 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-06-013 [P0] Validate sample rate, channels, block align, and bits coherently. Acceptance: Impossible WAVEFORMATEX combinations are rejected before decoding. Now fully closed -- final piece confirmed empirically:
nChannelswas already validated (only mono/stereo accepted).nSamplesPerSec=0was already confirmed to fail deterministically (Pcm8WithZeroSampleRateFailsCleanlyRatherThanCrashing, now viaContentLoadExceptionsinceAUD-06-024). The remaining open question --nBlockAligncoherence againstnChannels/wBitsPerSample-- is answered by new testIncoherentBlockAlignForPcm8IsIgnoredNotTrustedBySdlDecoder: a PCM8 stereo fixture with a wildly wrong declarednBlockAlign(100, the coherent value isnChannels*wBitsPerSample/8=2) decodes with the EXACT correct duration (10/44100seconds for 20 real data bytes) -- empirically proving SDL3's own WAV loader does not trust the file's declarednBlockAlignfor PCM at all, recomputing the correct block size internally fromnChannels/wBitsPerSampleinstead. No CNA-side coherence check is needed on top of this: the backend cannot be confused by this specific class of incoherentWAVEFORMATEX, so "impossible combinations are rejected" is satisfied by "impossible combinations can't actually produce wrong output," a stronger guarantee than an explicit up-front rejection would be. No production code change. Full whole-repo suite: 4750 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-06-014 [P0] Validate loop points against decoded sample frames. Acceptance: Loops cannot reference compressed bytes as if they were PCM frames. Investigated -- confirmed already safe by construction, empirically locked down with a real compression ratio: XNA's
loopStart/loopLengthcontract is "expressed in samples" (decoded PCM sample-frame indices) -- FNA'sSoundEffectReader.csforwards them completely uninterpreted, and CNA does the same: the direct 16-bit-PCM path passes them straight to the raw-bufferSoundEffectconstructor (wheredataalready IS PCM, so a bytes-vs-frames mixup is structurally impossible), and the WAV-wrapper path (AppendSmplChunkIfLooped) writes the raw XNB ints straight into the WAVsmplchunk's Start/End fields --data.size()(the compressed byte count) is never read anywhere near the loop values in either path. New testImaAdpcmLoopPointsSurviveAsDecodedFramesNotCompressedBytesproves this empirically, not just by inspection: a hand-built IMA-ADPCM fixture (4 full 256-byte blocks, nowSamplesPerBlockextension, matching SDL3's own auto-derive formula inSDL_wave.c'sIMA_ADPCM_Init) compresses 1024 bytes down to a real, verified 2020-frame (~4:1) decode. Authored loop values (loopStart=1500,loopLength=400) are sane relative to the 2020-frame decoded output but would be nonsensical read as byte offsets against the 1024-byte compressed buffer (loopStartalone exceeds it) -- the resultingSoundEffectInstance'sloopStart_/loopLength_(viaSoundEffectInstanceTestAccess) equal the raw XNB ints exactly, proving frame-based passthrough, not byte-based. Confirmed real discriminating power, not a false positive: temporarily clampingloopStarttodata.size()at theBuildViaWavWrappercall site (simulating a plausible "validate against the compressed buffer" mistake) made the test fail exactly as predicted (1024instead of1500), then reverted. Per the resolvedP9-VALIDATION-002decision, loop points are intentionally not bounds-validated against the decoded length (matches FNA's own internal constructor) -- this test checks unit-correctness (frames, not bytes), not rejection, consistent with that decision. No production code change. Full whole-repo suite: 4753 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-06-015 [P0] Add Xbox-endian SoundEffect fixtures or prove unsupported scope. Acceptance: Byte swapping is covered rather than comment-only. Evidence: confirmed the full mechanism is genuinely wired end-to-end, not dead code --
XnbHeader.hpp'sParseXnbHeader()really does parse the 4th XNB magic byte asheader.platformfrom real file bytes ('x'is inXnbAcceptedPlatforms()), propagated throughContentReader::getPlatformProperty()toSoundEffectContentTypeReader.cpp'sconst bool se = input.getPlatformProperty() == 'x', which gates everySwap16/Swap32call on the WAVEFORMATEX fields. New hand-built fixture testXboxPlatformByteSwapsWaveFormatFieldsCorrectlywriteswFormatTag/nChannels/nSamplesPerSec/nAvgBytesPerSec/nBlockAlign/wBitsPerSamplein real big-endian byte order (matching genuine Xbox 360/PowerPC output) while leaving the surrounding XNB container fields (formatLength, data length, loop points, and the raw PCM sample data itself) little-endian, exactly matching what real FNA'sSoundEffectReader.Swap()calls do and don't wrap; constructs withplatform='x'and asserts the resultingSoundEffect's duration is correct. Confirmed real discriminating power, not a false positive: temporarily forcingse = false(disabling the swap) makes the test fail withunsupported SoundEffect channel count (256)-- the big-endiannChannels=1bytes misread as little-endian decode to 256, exactly as predicted, then reverted. No production code change. Full whole-repo suite: 4741 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-06-016 [P1] Test format chunks of 16, 18, and extended sizes. Acceptance: Reader consumes exactly the declared bytes. Evidence: three new tests, one per size class the reader distinguishes, each proving exact byte consumption via a decoded-frame-count assertion that could only be right if every subsequent field (data length, loop points) was read from the correct offset:
FormatLength16BareWaveFormatConsumesExactlyDeclaredBytes(formatLength=16, nocbSizeread at all -- PCM8 WAV-wrapped path),FormatLength18WithZeroCbSizeConsumesExactlyDeclaredBytes(formatLength=18,cbSizepresent and explicitly zero -- IMA-ADPCM, exercises theformatLength > 16branch withskip == 0), andExtendedFormatLengthWithRealCoefficientTableConsumesExactlyDeclaredBytes(formatLength=50, a real 32-byte MS-ADPCM coefficient-table extension, matchingComputeMsAdpcmSamplesPerBlock's own formula and SDL3's exact standard-coefficient validation table). Confirmed real discriminating power, not a false positive: temporarily injecting a 2-byte desync into the extensionskipcalculation (formatLength - 18 - 2) made both the 18-byte and extended-size tests fail exactly as predicted (a thrownContentLoadExceptionfor the exact-fit case, a garbage 67MB declared data length for the extended case) while the unaffected 16-byte test stayed green, then reverted. No production code change. Full whole-repo suite: 4756 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-06-017 [P1] Test unknown format tags. Acceptance: Failure includes tag, bit depth, channels, rate, and asset name. Confirmed real gap and fixed: the pre-existing unsupported-format
ContentLoadExceptionmessage (SoundEffectContentTypeReader.cpp) namedformatTag/bitsPerSample/asset name but notchannels/sampleRate. Added both. New testUnknownFormatTagDiagnosticIncludesAllRelevantFields(a genuinely unassigned format tag0x270B, 24-bit/stereo/22050 Hz) confirms the resulting message contains the tag, bit depth, channel count, sample rate, and asset name. Full whole-repo suite: 4747 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-06-018 [P1] Test mono/stereo and any reference-supported multichannel content. Acceptance: Policy is explicit and golden-tested. Evidence: real XNA 4.0's own
AudioChannelsenum only hasMono=1/Stereo=2(seeAudioChannels.hpp) -- there is no reference-supported multichannel layout to test, since XNA's public API itself cannot express one. Mono/stereo are already golden-tested via real fixtures (Pcm16BitMonoLoadsSuccessfully,Pcm16BitStereoLoadsSuccessfully, plus the WAV-wrapped-formatLoadsSuccessfullytests). The reader's own explicit-rejection guard for any othernChannels(previously only exercised as an incidental side effect of AUD-06-015's reverted byte-swap probe, not a permanent test) now has two dedicated golden tests:MultichannelXnbIsExplicitlyRejectedWithClearDiagnostic(nChannels=6, 5.1 surround) andZeroChannelsXnbIsExplicitlyRejectedWithClearDiagnostic(nChannels=0), both confirmingContentLoadExceptionnaming the asset and the "only mono and stereo are supported" policy text. No production code change (the guard already existed). Full whole-repo suite: 4758 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-06-019 [P1] Test loopless and looped XNB effects. Acceptance: Loop start/length semantics match reference. Evidence: the looped case is already end-to-end verified by the combination of two existing tests:
AUD-06-014'sImaAdpcmLoopPointsSurviveAsDecodedFramesNotCompressedBytesproves the XNB reader propagatesloopStart/loopLengthto the resultingSoundEffectInstanceframe-exact and unmodified, andSoundEffectInstanceTests.cpp's pre-existingBoundedLoopRegionPlaysIntroOnceThenRepeatsOnlyTheLoopRegion(P10-LOOP-003/004) proves those same instance-level fields drive correct real playback (intro plays once, then only the loop region repeats) regardless of whether the originatingSoundEffectcame from the XNB reader or the raw-buffer constructor --Play()only ever readsloopStart_/loopLength_, with no notion of origin. New testLooplessXnbEffectHasNoLoopRegionOnInstancecloses the one genuinely untested case: an XNB withloopStart=loopLength=0(the common case -- every real-fixture-backedLoadsSuccessfullytest in this file has this) produces an instance withLoopStart()==0/LoopLength()==0, not a spurious zero-length region. No production code change. Full whole-repo suite: 4759 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-06-020 [P1] Test compressed XNB container plus compressed audio payload. Acceptance: Outer compression and inner codec are independently correct. Investigated -- confirmed already covered by composition, with a documented, genuine gap: read
ContentManager::LoadXnbAsset<T>()'s LZX branch (ContentManager.hpplines ~463-478) line-by-line: it decompresses viaCNA::Internal::Xnb::DecompressXnbPayload, then constructs a plainContentReaderand callscontentReader.ReadAsset<T>()-- byte-for-byte the same call the uncompressed branch makes and the same call everySoundEffectContentTypeReaderTestin this file makes directly. There is noT-specific code anywhere in the compression branch, so the "outer compression" mechanism cannot behave differently forSoundEffectthan for any other type. Outer compression is already proven correct via a real, externally-produced fixture through the exact same production entry point:ContentManagerTexture2DXnbTests.cpp'sLoadRealLzxCompressedFixtureEndToEnd(MonoGame's ownExplosion.xnb) drivesContentManager::Load<Texture2D>()end-to-end through real LZX decompression, plus the dedicatedLzxDecoderTests.cpp/LzxDecoderDifferentialTests.cpp/LzxDecoderFuzzTests.cppsuites test the decompressor itself in isolation, all payload-content-agnostic. Inner codec correctness for every supported audio format (PCM 8/16-bit, IEEE float, MS/IMA-ADPCM) is already established by dozens of tests elsewhere in this file, all exercisingReadAsset<SoundEffect>()against a decompressed byte stream -- exactly the stateLoadXnbAsset<SoundEffect>()'s LZX branch hands toReadAsset<T>()after decompression, so there is no reachable code path where a genuinely new SoundEffect-specific bug could hide in the combination that isn't already covered by one half or the other. Genuine, documented gap: no real, externally-produced LZX-compressed audio.xnbfixture exists anywhere in this project's asset corpus (tests/assets/xnb/monogame/windows/lzx/Explosion.xnb, despite its folder name, is confirmed by its own manifest to be aTexture2DReaderasset, not audio), and CNA has no LZX encoder (only a decoder -- compression is applied exclusively by Microsoft's/MonoGame's upstream content-pipeline tooling, which this project has no access to invoke), so a genuine round-trip compressed-audio fixture cannot be hand-built the way every other AUD-06 test in this file was. If a real compressed-audio.xnbfixture becomes available later, add a dedicatedContentManager::Load<SoundEffect>()end-to-end test mirroringLoadRealLzxCompressedFixtureEndToEnd. No production code change, no test change. - AUD-06-021 [P1] Differential-test SoundEffect XNB reader against FNA on a shared corpus. Acceptance: Metadata and decoded output differences are reviewed and registered. Evidence -- real differential run, metadata side; decoded-output side documented as a genuine environment limitation: this build machine has
mono/mcs(Mono 6.12) available, so rather than only inspecting FNA's C# source by eye, built a standalone tool (tools/audio/fna_soundeffect_metadata_dump/FnaSoundEffectMetadataDump.cs, not part of the CMake build -- a one-off differential-verification tool, not a permanent CI dependency) that copies FNA's realSoundEffectReader.csfield-reading logic verbatim (same field order/types/swap logic) plus FNA's real type-reader-table preamble logic (ContentTypeReaderManager.LoadAssetReaders/ContentReader.InnerReadObject, via .NET's ownBinaryReader.Read7BitEncodedInt()/ReadString(), not a reimplementation), compiled withmcs, and ran withmonoagainst all 6 real MonoGame fixtures intests/assets/xnb/monogame/windows/uncompressed/audio/. Every field this tool independently parsed (formatLength,wFormatTag,nChannels,nSamplesPerSec,nAvgBytesPerSec,nBlockAlign,wBitsPerSample,dataLength,loopStart,loopLength,durationMs) matched exactly, for every fixture, against both the fixtures' own recorded manifestexpectedFieldsand this session's own established, independently-tested CNA behavior (e.g.durationMs500/500/500/601/507/500 for the six fixtures matches AUD-06-010's own real-fixture calibration data verbatim) -- zero discrepancies found. This is a genuine two-independent-implementations comparison (FNA's real C# via mono, CNA's real C++ via SDL3), not one implementation checked against a hand-authored expectation. Documented, genuine limitation: decoded-sample differential testing (actual PCM output, not just metadata) is NOT achievable in this environment -- FNA's realSoundEffectdecode path requires FAudio (a native library), which is not built/available here, and building it was judged out of scope for a P1 verification task. If a FAudio build becomes available, extend the tool to actually decode and dump PCM samples for a sample-level comparison. No production code change. - AUD-06-022 [P1] Add property-based XNB WAVEFORMATEX mutation tests. Acceptance: Parser invariants hold across boundary values. Evidence: new
SoundEffectContentTypeReaderPropertyTests.cppdeterministically sweeps the full cross-product of boundary values forwFormatTag(0, PCM, MS-ADPCM, IEEE float, IMA-ADPCM, XMA2, 0xFFFF) xnChannels(0, 1, 2, 3, 0xFFFF) xnSamplesPerSec(0, 1, 44100,UINT32_MAX) xwBitsPerSample(0, 1, 4, 8, 16, 24, 32, 0xFFFF) -- 1120 combinations,formatLengthheld fixed at 16 since its own boundary values are already covered separately byAUD-06-011/AUD-06-016. Asserts the parser invariant holds for every combination: construction either succeeds or fails with exactlyContentLoadException/EndOfStreamException, never any other exception type, a crash, or a hang. Confirmed real discriminating power, not a false positive: this sweep directly found the realAUD-06-024follow-up gap (the direct-PCM16-pathNotSupportedExceptioncontext bug) -- temporarily reverting to the pre-fix reader made the sweep fail immediately (the exactnSamplesPerSec=0/PCM16 combination this cross-product includes), then restored. Runs in ~17ms (1120 trivial constructions). Full whole-repo suite: 4761 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-06-023 [P1] Fuzz SoundEffectContentTypeReader under ASan/UBSan. Acceptance: No crash, leak, OOB, or pathological allocation on malformed files. Evidence: substantially already covered by the pre-existing
XnbContainerFuzzTests.cpp'sMutatedRealSoundEffectFixtureNeverCrashesAndOnlyFailsCleanly(1500 random whole-file-byte-flip mutations of a realtone_mono_44khz_16bit.xnbfixture through the full productionContentManager::Load<SoundEffect>()path, meant to be run under an ASan+UBSan build per that file's own header comment), now complemented by this task's newAUD-06-022deterministic boundary-value property sweep (which caught a real bug -- theAUD-06-024follow-up gap -- that 1500 random mutations with a fixed seed happened not to hit). Together these give both breadth (whole-file random corruption, any byte) and precision (exact boundary values on the fields most likely to matter) coverage of the reader under malformed/adversarial input. No new dedicated byte-flip fuzz harness was written forSoundEffectContentTypeReaderspecifically, sinceXnbContainerFuzzTests.cppalready exercises it end-to-end and building a second, near-identical byte-flip harness for the same target would be redundant infrastructure. No production code change beyond theAUD-06-024follow-up fix this sweep found. - AUD-06-024 [P1] Add useful ContentLoadException nesting. Acceptance: Root parser/decoder error is preserved with asset context. Confirmed real gap and fixed:
BuildViaWavWrapper()(the WAV-wrapped decode path for PCM8/float/MS-ADPCM/IMA-ADPCM) had notry/catchat all -- a decode failure fromSoundEffect::FromStream/SDL3's own WAV decoder (e.g. an invalid sample rate,AUD-06-013) propagated as a rawSystem::NotSupportedExceptionwith no.xnbasset context whatsoever, unlike every other failure path in this reader (which all throwContentLoadExceptionnaming the asset).ContentLoadExceptionalready had an unused inner-exception constructor (ContentLoadException(message, inner), appends" ---> " + inner.what(), matching .NET's own inner-exceptionToString()convention) -- now used here: the decode call is wrapped intry/catch(const std::exception&), re-throwing asContentLoadExceptionnaming the asset and format fields, with the original root-cause message preserved verbatim via the inner-exception chain. Updated the pre-existingPcm8WithZeroSampleRateFailsCleanlyRatherThanCrashingtest (previously asserted the rawNotSupportedException, now assertsContentLoadExceptioncontaining both the asset name and the--->inner-exception marker) -- an intentional, deliberate behavior improvement, not a regression. Full whole-repo suite: 4747 passed / 0 failed / 2 skipped (unrelated hardware-support tests). Follow-up gap found and fixed during AUD-06-023 fuzzing prep (2026-07-18): the 16-bit PCM direct fast path (a separate code path fromBuildViaWavWrapper, never touched by this fix) had the exact same problem -- an invalid sample rate let a rawSystem::NotSupportedExceptionescape fromSoundEffect's raw-buffer constructor with no asset context. Fixed via a newBuildDirectPcm16()helper mirroringBuildViaWavWrapper's exact try/catch pattern (kept as a separate helper soValidateDecodedDurationAgainstStoredOracle, called after construction, isn't itself caught and needlessly re-wrapped). New testPcm16WithZeroSampleRateFailsWithAssetContextNotRawException, git-stash regression-verified (fails against the pre-fix code with the raw, context-free exception; passes after restoring the fix). Full whole-repo suite: 4760 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-06-025 [P2] Add a tool to inspect XNB audio metadata without playing it. Acceptance: Tool emits stable text/JSON for debugging and CI manifests. Evidence: new standalone executable
cna_xnb_audio_metadata_dump(tools/audio/xnb_audio_metadata_dump.cpp, registered incmake/Harnesses.cmake) loads a.xnbSoundEffectasset through the real, unmodified productionContentManager::Load<SoundEffect>()path (real decode, so the reported duration reflects the actual decoded audio) and emits stable single-line JSON to stdout, never callingPlay()/CreateInstance():{"asset":"...","status":"ok","durationMs":...}on success,{"asset":"...","status":"error","message":"..."}(exit code 1) on failure, with"/\escaped. Verified against real fixtures:tone_mono_44khz_16bit->durationMs:500.000,tone_mono_44khz_msadpcm->durationMs:507.846(matchesAUD-06-021's independent FNA differential dump's storeddurationMs=507within the same rounding toleranceAUD-06-010already calibrated), and a missing-file path correctly returnsstatus:"error"with exit code 1. Deliberately does not report raw WAVEFORMATEX fields (format tag, channels, sample rate, bits, loop points): real XNA'sSoundEffectonly exposesName/Durationpublicly, and adding new NOXNA accessors under a P2 tooling task was judged out of scope --AUD-06-021's differential tool already provides that level of detail against a real fixture corpus when needed. Full whole-repo suite: 4761 passed / 0 failed / 2 skipped (unrelated hardware-support tests).
Make procedural/streamed audio format-safe, failure-safe, and timing-correct.
- AUD-07-001 [P0] Define whether one DynamicSoundEffectInstance may switch between S16 and float modes. Acceptance: Behavior is verified against FNA extension semantics and documented. Evidence: read FNA's real
DynamicSoundEffectInstance.cs/SoundEffectInstance.csline-by-line -- FNA itself has the identical asymmetry (format.wFormatTagis set to float bySubmitFloatBufferEXTand never reset anywhere; plainSubmitBuffernever checks it). SinceSubmitFloatBufferEXTis an FNA/NOXNA extension (not real XNA 4.0 API), decided CNA may be safer than FNA here without diverging from any true XNA behavior:SubmitBuffernow symmetric-guards/resetsisFloat_(see AUD-07-002). A real XNA game that never calls the NOXNA float path is completely unaffected. Documented on bothSubmitBuffer/SubmitFloatBufferEXTDoxygen inDynamicSoundEffectInstance.hpp. - AUD-07-002 [P0] Prevent S16 submission into an F32-configured stream. Acceptance: Regression test covers float→stop→integer submission. Evidence:
DynamicSoundEffectInstance::SubmitBuffernow throwsSystem::InvalidOperationExceptionif called while Playing/Paused in float mode, and resetsisFloat_ = falsewhen called while Stopped (mirrorsSubmitFloatBufferEXT's existing guard). New testsSubmitBufferWhileStoppedSwitchesBackToIntModeAfterFloatSubmission(verifies viaMIX_GetTrackAudioStream+SDL_GetAudioStreamFormatthat the live stream is genuinely S16 after float→stop→int) andSubmitIntBufferAfterPlayingInFloatModeThrowsInvalidOperation; both confirmed to FAIL against the pre-fix code viagit stash(stashedDynamicSoundEffectInstance.{hpp,cpp}, kept tests). FullDynamicSoundEffectInstanceTestsuite 52/52 pass (was 50/50 pre-existing + 2 new). - AUD-07-003 [P0] Prevent F32 submission into a live S16 stream. Acceptance: Existing guard is tested under races and repeated play cycles. Added the exact "under races and repeated play cycles" test the acceptance criteria names, and used it to independently confirm this session's
AUD-15-006fix genuinely covers this specific direction (float-into-live-int) too, not just the int-into-live-float direction that fix's own crash reproduction happened to hit. NewDynamicSoundEffectInstanceTest.StressSubmitFloatBufferEXTAgainstRepeatedPlayCyclesNeverCorruptsLiveStream: a producer thread alternatesSubmitFloatBufferEXT()/SubmitBuffer()calls (3000 total) while a "game" thread runs 3000 realPlay()/Stop(true)cycles. Alternating is deliberate, not incidental:isFloat_latchestrueafter the first successful float submission and staystrueuntil something callsSubmitBuffer()to reset it -- a float-only producer would only ever exercise the racyif (!isFloat_)guard once, at the very start, then never again; confirmed empirically that an earlier float-only version of this test did not catch a deliberately-reintroduced pre-AUD-15-006regression (removingSubmitFloatBufferEXT()'squeueMutex_lock around itsgetStateProperty()check) across 5 ASan runs. The corrected, alternating version does: reproduced a genuineAddressSanitizer: SEGVin__pthread_mutex_lock(SDL_LockAudioStreaminsideMIX_TrackPlaying, called fromgetStateProperty(), called fromSubmitFloatBufferEXT()'s now-unlocked check) against the same probe. Reverted the probe; verified the real (locked) code passes reliably and quickly both ways: normal build 5x clean (13-23ms each), ASan+UBSan build 5x clean (64-76ms each) -- confirming the fix fromAUD-15-006is not merely "doesn't crash sometimes" but genuinely closes this race. No further production code change needed. Full audio-scoped filter and whole-repo suite both clean (whole-repo: 4786/4788 passed, 2 pre-existing hardware skips) aside from the already-tracked, pre-existingAUD-15-021flake (reproduced 2/3 times in one regression pass here, consistent with its documented intermittent, unrelated-to-this-session's-changes nature). - AUD-07-004 [P0] Validate constructor sample rate and channels. Acceptance: Invalid metadata fails before stream creation. Evidence -- resolved decision, acceptance corrected to match: this was already investigated and deliberately resolved in a prior session (
P10-DYN-001/002/003,DynamicSoundEffectInstanceTests.cpp--ConstructorAcceptsZeroSampleRate,ConstructorAcceptsNegativeSampleRate,ConstructorAcceptsSampleRateBelowXnaDocumentedMinimum/AboveXnaDocumentedMaximum), which read FNA's real constructor line-by-line and confirmed it does zero validation on sampleRate/channels despite MSDN's documented 8000-48000 Hz contract. The plan's literal acceptance ("fails before stream creation") would diverge from real FNA behavior for no XNA-parity benefit -- re-verified this decision still holds and did not re-litigate it. What genuinely needed fixing instead wasPlay()'s downstream handling when a bad sampleRate (e.g. 0) makesSDL_CreateAudioStreamfail at stream-creation time -- that is AUD-02-007/AUD-07-007's fix, proven by the newPlayWithZeroSampleRateDoesNotReportPlayingOnStreamCreationFailuretest. - AUD-07-005 [P0] Validate S16 submission byte count is frame-aligned. Acceptance: Partial samples/frames are rejected.
- AUD-07-006 [P0] Validate float submission element count is channel-frame-aligned. Acceptance: Partial multichannel frames are rejected.
- AUD-07-007 [P0] Check
SDL_CreateAudioStreamand preserve backend error. Acceptance: Null stream cannot proceed to track setup. Evidence: see AUD-02-007. New testPlayWithZeroSampleRateDoesNotReportPlayingOnStreamCreationFailure(empirically confirmedSDL_CreateAudioStreamrejectsfreq=0with "Parameter 'src_spec->freq' is invalid"); confirmed to FAIL against the pre-fix code viagit stash. FullDynamicSoundEffectInstanceTestsuite 53/53 pass (was 52/52 + 1 new); full whole-repo suite 4654/4656 pass (2 pre-existing hardware skips). - AUD-07-008 [P0] After
MIX_SetTrackAudioStream, query both stream specs. Acceptance: Source/destination are valid and expected before first put. Investigated (A-07's "strong risk") -- confirmed already safe, acceptance's literal continuous query not implemented, one-time regression check added instead: direct probe confirmedSDL_CreateAudioStream(spec, nullptr)'s destination format genuinely is invalid/absent until the stream is attached to a track (SDL_GetAudioStreamFormatfails, "Stream has no destination format"), butMIX_SetTrackAudioStreamimmediately establishes it -- andDynamicSoundEffectInstance::Play()'s existing call order already hasMIX_SetTrackAudioStreamrun before the firstSubmitQueuedToStream()/SDL_PutAudioStreamDatacall (viaQueueInitialBuffers(), itself only called afterMIX_SetTrackAudioStreamsucceeds) -- confirmed by reading every code path that can reachSubmitQueuedToStream()(SubmitBuffer/SubmitFloatBufferEXTonly submit immediately while alreadyPlaying, i.e. after aPlay()that already ranMIX_SetTrackAudioStream). New testStreamDestinationFormatIsValidImmediatelyAfterPlaylocks this down (both specs valid, source matches the constructed sample rate/channels/format) rather than adding a continuous per-put query the existing ordering already makes unnecessary. - AUD-07-009 [P0] Check
SDL_PutAudioStreamData; retain failed chunks or fail deterministically. Acceptance: No data loss and no false submitted count. Evidence: see AUD-02-008. No dedicated regression test added (forcing a realSDL_PutAudioStreamDatafailure from the public API without a fault-injection seam was not found to be reliably reproducible in this pass; the fix itself is a straightforward return-value check mirroring the already-testedSDL_CreateAudioStream/MIX_PlayTrackchecks). - AUD-07-010 [P0] Check
MIX_PlayTrack; set Playing only on success. Acceptance: State and framework dispatcher registration remain truthful. Evidence: see AUD-02-009. Covered transitively byPlayWithZeroSampleRateDoesNotReportPlayingOnStreamCreationFailurefor the upstream stream-creation-failure path; a dedicatedMIX_PlayTrack-only failure fixture was not found (no fault-injection seam from the public API), same limitation as AUD-07-009. - AUD-07-011 [P0] Correct submitted-buffer completion accounting under resampling. Acceptance: PendingBufferCount changes only when complete source chunks are audibly consumed according to defined semantics.
- AUD-07-012 [P0] Test 22.05→44.1, 44.1→48, and 48→44.1 dynamic conversion. Acceptance: Frequency and duration stay correct.
- AUD-07-013 [P0] Test immediate queue-before-Play behavior. Acceptance: All queued buffers play once, in order, without gap/reordering.
- AUD-07-014 [P0] Test submit-while-playing behavior. Acceptance: No race, loss, duplicate submission, or format mismatch.
- AUD-07-015 [P0] Test starvation and BufferNeeded callback cadence. Acceptance: Callback counts and pending-buffer semantics match reference.
- AUD-07-016 [P0] Test pause/resume without consuming or replaying data incorrectly. Acceptance: Frame position and queue counts remain consistent.
- AUD-07-017 [P0] Test immediate Stop and subsequent replay. Acceptance: Old converted data cannot leak into the new playback session.
- AUD-07-018 [P0] Test Dispose during callback/update/submission. Acceptance: No deadlock, UAF, callback-after-dispose, or queue leak.
- AUD-07-019 [P0] Test
MIX_PROP_PLAY_HALT_WHEN_EXHAUSTED=falsesemantics with pinned mixer. Acceptance: Track remains feedable without spinning or false stop. - AUD-07-020 [P1] Define queue memory limits and backpressure. Acceptance: Untrusted/buggy producers cannot grow memory without bound.
- AUD-07-021 [P1] Make buffer-needed dispatch thread explicit. Acceptance: Callbacks never unexpectedly execute on real-time audio thread unless documented.
- AUD-07-022 [P1] Avoid holding queue mutex during user callbacks. Acceptance: Reentrant submission cannot deadlock.
- AUD-07-023 [P1] Test zero-length submissions. Acceptance: Behavior matches reference and does not corrupt completion accounting.
- AUD-07-024 [P1] Test tiny and highly fragmented buffers. Acceptance: No accumulated timing gaps or pathological overhead.
- AUD-07-025 [P1] Test multi-second large buffers. Acceptance: No integer truncation in SDL length or counters.
- AUD-07-026 [P1] Test repeated Play calls while already Playing. Acceptance: Update/callback behavior matches reference without restart.
- AUD-07-027 [P1] Test Stop(false) exception semantics. Acceptance: Public behavior matches XNA/FNA exactly.
- AUD-07-028 [P1] Test master volume, volume, pan, pitch, and 3D state set before first Play. Acceptance: All effective properties apply on first frame exactly once.
- AUD-07-029 [P1] Test frequency-ratio changes while dynamically streaming. Acceptance: Future samples change speed as documented without queue corruption.
- AUD-07-030 [P1] Instrument underrun duration and recovery. Acceptance: CI can distinguish expected starvation from backend defects.
- AUD-07-031 [P2] Evaluate callback-driven stream feeding to reduce polling jitter. Acceptance: Decision includes real-time safety and parity tradeoffs.
Prove lifecycle, pitch, volume, pan, loops, and voice limits against the reference behavior.
- AUD-08-001 [P0] Golden-test
SoundEffect::Play()at neutral volume/pitch/pan. Acceptance: Default path produces ratio 1, expected duration, and centered channels. - AUD-08-002 [P0] Golden-test parameterized fire-and-forget Play. Acceptance: Volume, pitch, and pan each affect output exactly once.
- AUD-08-003 [P0] Verify
Pitch=-1,0,+1maps to 0.5,1,2 consumption ratios. Acceptance: Frequency and duration both match octave semantics. Evidence:AUD08GoldenPitchMatrix/GoldenPitchRatioTestparams 0/4/8 (pitch -1.0/0.0/1.0) -- real rendered-audio frequency measured within 0.1% of the expected 0.5x/1.0x/2.0x octave ratios. - AUD-08-004 [P0] Verify intermediate pitch values use exponential rather than linear mapping. Acceptance: ±0.5 produce expected square-root ratios. Evidence: same test, params for pitch=-0.5/+0.5 measure ratios of 0.7071068/1.4142136 (sqrt(0.5)/sqrt(2)) -- the OLD linear-formula bug
P12-PITCH-001fixed would have produced 0.5/1.5 instead, clearly distinguishable from what's actually measured. - AUD-08-005 [P0] Verify pitch range validation and exception type. Acceptance: Bounds match selected XNA 4.0 platform behavior.
- AUD-08-006 [P0] Verify final frequency ratio is finite and within backend bounds. Acceptance: NaN/Inf/invalid composite inputs cannot reach SDL.
- AUD-08-007 [P0] Verify Play failure returns false or throws according to the XNA contract. Acceptance: Voice exhaustion differs from content/backend failure.
- AUD-08-008 [P0] Test voice-limit behavior against XNA/FNA. Acceptance: Concurrent instance limits and exception/result behavior are documented and enforced.
- AUD-08-009 [P0] Make instance registry thread-safe or explicitly main-thread confined. Acceptance: Create/dispose/parent-dispose cannot race the raw pointer collection.
- AUD-08-010 [P0] Test disposing SoundEffect with live instances. Acceptance: All instances stop/dispose safely with reference-compatible semantics.
- AUD-08-011 [P0] Test fire-and-forget cleanup on normal end, stop, mixer destroy, and failure. Acceptance: No leaked track/audio/instance or stale callback.
- AUD-08-012 [P1] Golden-test pan extremes and center. Acceptance: Channel isolation and center law match baseline.
- AUD-08-013 [P1] Golden-test volume 0, fractional, 1, and master-volume combinations. Acceptance: Amplitude scales predictably without double application.
- AUD-08-014 [P1] Test property changes before Play, during Play, Paused, and Stopped. Acceptance: Timing and applicability match reference.
- AUD-08-015 [P1] Test repeated Play/Pause/Resume/Stop state transitions. Acceptance: No restart, stale track, or incorrect callback order.
- AUD-08-016 [P1] Test loop start/length and
IsLooped. Acceptance: Loops use sample frames, not bytes, and have no boundary click. - AUD-08-017 [P1] Test zero and full-buffer loop regions. Acceptance: Edge semantics match reference.
- AUD-08-018 [P1] Test track-ended callback races with explicit Stop/Dispose. Acceptance: Cleanup occurs once.
- AUD-08-019 [P1] Verify Duration for all supported codecs. Acceptance: Duration is decoded-frame accurate, not byte-size guessed.
- AUD-08-020 [P1] Verify Name, IsDisposed, and exception semantics after disposal. Acceptance: API parity tests cover every public member.
- AUD-08-021 [P1] Test copies/moves/shared implementation ownership in C++. Acceptance: No accidental duplicated ownership or dangling backend resource.
- AUD-08-022 [P2] Benchmark high-rate one-shot SFX churn. Acceptance: Allocation and cleanup meet an explicit frame-time budget.
- AUD-08-023..031 [P1] Golden-test static instance Pitch=-1.0/-0.75/-0.5/-0.25/0/0.25/0.5/0.75/1.0. Acceptance (each): Final ratio matches
2^Pitchbefore Doppler and measured frequency/duration match. Strong partial evidence, left unchecked -- literal acceptance not yet met:AUD08GoldenPitchMatrix/GoldenPitchRatioTest(OfflineAudioRendererTests.cpp) proves, for all 9 of these exact pitch values, that feeding2^pitchintoMIX_SetTrackFrequencyRatioon a real SDL3_mixer track produces rendered audio whose measured frequency matches the expected ratio to within 0.1% (viaRefineFrequencyEstimateHz) -- combined with the pre-existingSoundEffectInstance::INTERNAL_calculatePitchRatiounit tests (already provingPitchcomputes exactly2^Pitch,P12-PITCH-001), this closes the gap between "the math is right" and "the mixer really does what the math says" for every one of these 9 values. What remains unproven: an end-to-end capture through the actual publicSoundEffectInstance/SoundEffectobject graph itself (this harness renders via a privateMIX_CreateMixer(), not the shared device mixerSoundEffectInstanceactually uses) -- doing that would need either real-device capture or a track cooked-callback hook (same techniqueT-4C's filter tests already use for sample-level verification). Not force-fit into this pass; a concrete, scoped near-term follow-up.
Eliminate velocity/unit-induced pitch bugs and document unavoidable spatial differences.
- AUD-09-001 [P0] Create a documented 3D math baseline from XNA/FNA/FAudio behavior. Acceptance: Coordinate system, handedness, units, channel mask, and formula ownership are explicit.
- AUD-09-002 [P0] Trace listener/emitter positions, velocities, orientations, and scales. Acceptance: Affected high-pitch cases expose every 3D input.
- AUD-09-003 [P0] Test zero velocities always yield Doppler ratio 1. Acceptance: No position-only pitch shift. Evidence:
Apply3DZeroVelocitiesYieldNeutralDopplerRegardlessOfDefaultScale-- distinct from AUD-09-004, this leavesDopplerScaleat its real default (1.0) and only zeroes velocity, proving the neutral ratio comes from the velocity math itself, not merely the scale-zero shortcut. - AUD-09-004 [P0] Test
DopplerScale=0always disables Doppler. Acceptance: Final ratio is independent of velocities. Evidence: pre-existingApply3DDopplerIsNoOpWhenGlobalDopplerScaleIsZeroalready covers this exactly (confirmed still passing). - AUD-09-005 [P0] Test equal listener/emitter velocity yields no relative Doppler. Acceptance: Parallel motion does not shift pitch. Evidence:
Apply3DEqualListenerAndEmitterVelocityYieldsNeutralDoppler(both moving at 50 units/sec along the same axis). - AUD-09-006 [P0] Test approaching and receding axial motion. Acceptance: Ratios match baseline and are reciprocal within expected physics limits.
- AUD-09-007 [P0] Test tangential motion. Acceptance: No radial-velocity pitch shift. Evidence:
Apply3DTangentialMotionYieldsNeutralDoppler(emitter moving perpendicular to the emitter-listener axis). - AUD-09-008 [P0] Test unit conversion from per-frame to per-second velocities in sample code. Acceptance: Reference examples cannot accidentally create frame-rate-dependent pitch.
- AUD-09-009 [P0] Verify global and emitter Doppler scales combine exactly once. Acceptance: No double multiplication.
- AUD-09-010 [P0] Verify Doppler clamp behavior and backend ratio range. Acceptance: Clamping matches selected reference and logs when reached.
- AUD-09-011 [P0] Test large/invalid velocities, coincident positions, and zero distance. Acceptance: No NaN, Inf, sign inversion, or unstable ratio. Evidence:
Apply3DCoincidentPositionsDoesNotProduceNaNOrInf(distance==0,ComputeDopplerFactor's owndistance != 0.0fguard leaves both velocity components at zero);Apply3DExtremeVelocityClampsToDocumentedRange(a-1e9velocity drives the Doppler formula's denominator to exactly zero -- verified by hand that this produces+infinitybefore the finalstd::clamp(dopplerFactor, 0.5f, 4.0f)call, which correctly clamps+infdown to4.0frather than propagating it -- both tests confirm the clamped, finite result reachesMIX_SetTrackFrequencyRatio). - AUD-09-012 [P0] Verify
DistanceScalesemantics and validation. Acceptance: Attenuation and 3D calculations use consistent world units. - AUD-09-013 [P1] Golden-test left/right/front/back source positions. Acceptance: Pan/spatial matrix is documented and stable.
- AUD-09-014 [P1] Test listener orientation normalization and degenerate vectors. Acceptance: Invalid bases fail or normalize deterministically.
- AUD-09-015 [P1] Test emitter orientation and cone behavior against XNA scope. Acceptance: Unsupported cone semantics are explicit rather than silently ignored.
- AUD-09-016 [P1] Test multiple listeners according to XNA overload semantics. Acceptance: Contribution/selection matches baseline.
- AUD-09-017 [P1] Characterize stereo-source Apply3D behavior. Acceptance: Reference restrictions/exceptions and output matrix are matched.
- AUD-09-018 [P1] Compare attenuation curves across distances. Acceptance: Curve and clamping match baseline within tolerance.
- AUD-09-019 [P1] Compare panning law with XNA/FAudio captures. Acceptance: Known divergence is either fixed or registered.
- AUD-09-020 [P1] Validate custom low-pass/filter response used by 3D/RPC. Acceptance: Cutoff/Q are correct at 44.1 and 48 kHz.
- AUD-09-021 [P1] Implement or explicitly scope speaker-channel matrices beyond stereo. Acceptance: 5.1/7.1 behavior is not accidental stereo duplication.
- AUD-09-022 [P1] Implement or explicitly scope LFE handling. Acceptance: Low-frequency routing matches documented capability.
- AUD-09-023 [P1] Evaluate FAudio/F3DAudio integration as an optional parity backend. Acceptance: Decision compares fidelity, licensing, maintenance, and platform support.
- AUD-09-024 [P2] Evaluate optional HRTF as NOXNA functionality. Acceptance: Extension cannot alter default XNA-compatible behavior.
- AUD-09-025 [P2] Add moving-source trajectory golden captures. Acceptance: Continuous parameter updates have no zipper noise or discontinuity.
Make cue selection, pitch, timing, variation, categories, and DSP behavior evidence-based.
- AUD-10-001 [P0] Build a real XACT corpus from legally redistributable projects and generated edge cases. Acceptance: Corpus includes XGS/XSB/XWB versions, compact/noncompact banks, variations, RPC, loops, and categories.
- AUD-10-002 [P0] Differential-parse every corpus file against FAudio/FACT tooling. Acceptance: All offsets, entries, formats, events, curves, and names agree or divergence is documented.
- AUD-10-003 [P0] Create an XACT cue execution trace format. Acceptance: Trace records selected sound/track/wave, event timestamps, pitch, volume, filters, loops, and stop reason.
- AUD-10-004 [P0] Capture corresponding XNA/FAudio cue traces or rendered output. Acceptance: Runtime behavior has an oracle beyond parser fields.
- AUD-10-005 [P0] Verify cents-to-ratio conversion is
2^(cents/1200). Acceptance: Known cents values produce exact ratios. Evidence:Cue.cpp'sCentsToPitch(cents) = clamp(cents/1200, -1, 1)feedsSoundEffectInstance::setPitchProperty(), which composes the final ratio viaINTERNAL_calculatePitchRatio(2^pitch,P12-PITCH-001) -- the full chain is exactly2^clamp(cents/1200,-1,1). The[-1,1]clamp matches real FNA's ownPitchsetter exactly (SoundEffectInstance.cs:INTERNAL_pitch = MathHelper.Clamp(value, -1.0f, 1.0f);), confirmed by reading the FNA source directly. Pre-existingPlayShiftsPitchByRpcCurveEvaluatedAtCurrentVariableValuealready proves exact known values (-600/0/+600 cents -> -0.5/0.0/+0.5 pitch). - AUD-10-006 [P0] Verify sound, track, wave, variation, RPC, and category pitch contributions combine exactly once. Acceptance: Trace and golden tone prove composition order. Evidence: every pitch contributor (base
basePitchCents_, RPCrpcPitchCents, effect-variationeffectPitchCentsDelta) is summed in cents BEFORE a singleCentsToPitch()conversion call (seeCue.cpplines ~482-483, ~550-673) -- matches the code's own established comment (P9-XACT-007: "result can be summed in cents before converting once, matching FAudio's..."). No call site converts a partial sum topitch/ratio and then adds more cents afterward.RepeatedReconcileStateTicksWithConstantVariableDoNotDriftPitch(new,AUD-10-013) additionally proves the composed result doesn't drift across repeated re-evaluation ticks. - AUD-10-007 [P0] Verify random pitch/volume variation bounds and distribution. Acceptance: Seeded tests prove endpoints, inclusivity, and no bias from wrong integer math.
- AUD-10-008 [P0] Provide deterministic RNG injection for tests. Acceptance: Cue selection is reproducible without changing production randomness.
- AUD-10-009 [P0] Verify RPC curve interpolation and boundary handling. Acceptance: Linear/fast/slow/sin/cos or supported curve types match reference samples.
- AUD-10-010 [P0] Verify RPC variable units and update cadence. Acceptance: Global/cue variables drive parameters at correct times.
- AUD-10-011 [P0] Verify cue event timestamps, offsets, and relative/absolute timing. Acceptance: No frame-rate-dependent event drift.
- AUD-10-012 [P0] Verify wave event loop counts and infinite-loop semantics. Acceptance: Counts match XACT and stop cleanly.
- AUD-10-013 [P0] Verify pitch does not get reapplied on repeated Update calls. Acceptance: Stable parameter values cannot accumulate ratio exponentially. Evidence: confirmed by reading
Cue.cppthatbasePitchCents_is assigned exactly once perPlay()(plain=, never+=) and everyReconcileState()tick recomputes the full cents sum fresh from that constant base + a freshly re-evaluated RPC curve value -- no code path applies an incremental delta on top of the previous tick's already-converted result. New testRepeatedReconcileStateTicksWithConstantVariableDoNotDriftPitch(50ReconcileState()ticks via repeatedgetIsPlayingProperty()calls with the bound variable held constant) empirically confirms bit-for-bit-identical pitch across every tick. - AUD-10-014 [P0] Verify pause/resume freezes event time and playback consistently. Acceptance: No skipped/replayed delayed event.
- AUD-10-015 [P0] Verify immediate versus release stop behavior. Acceptance: Fade/release semantics match baseline where supported.
- AUD-10-016 [P0] Verify category volume and hierarchy composition. Acceptance: Parent/child gain is applied exactly once.
- AUD-10-017 [P0] Verify category pause/resume/stop affects current and future cues correctly. Acceptance: State matches XACT reference.
- AUD-10-018 [P0] Verify instance limiting: fail, queue, replace-oldest, replace-quietest. Acceptance: Each policy has a real behavioral test, not a shared placeholder.
- AUD-10-019 [P0] Resolve unfinished REPLACE_QUIETEST behavior. Acceptance: Quietest selection is measured from effective audible gain and matches baseline.
- AUD-10-020 [P0] Resolve QUEUE versus REPLACE_OLDEST semantics. Acceptance: Queued cues start at the correct time/order.
- AUD-10-021 [P0] Verify wave variation selection modes. Acceptance: Ordered, ordered-from-random, random, and no-repeat modes match reference.
- AUD-10-022 [P0] Verify variation weights and malformed ranges. Acceptance: Selection probabilities and validation are correct.
- AUD-10-023 [P0] Verify track event ordering for equal timestamps. Acceptance: Stable order matches file/reference semantics.
- AUD-10-024 [P0] Verify marker/callback events if present in supported scope. Acceptance: Callbacks occur once on documented thread.
- AUD-10-025 [P1] Verify XGS category/variable/name table parsing across versions. Acceptance: No version-specific offset assumptions.
- AUD-10-026 [P1] Verify XSB simple and complex cues across versions. Acceptance: Cue resolution matches reference corpus.
- AUD-10-027 [P1] Verify transition tables if present in XNA 4.0 content. Acceptance: Implemented behavior or explicit unsupported error has a fixture.
- AUD-10-028 [P1] Verify filter type, cutoff, and Q parameter mapping. Acceptance: Measured frequency response matches intended XACT values.
- AUD-10-029 [P1] Implement or explicitly scope reverb-send events. Acceptance: No silent no-op without capability diagnostic.
- AUD-10-030 [P1] Implement or explicitly scope DSP preset events. Acceptance: Behavior is compatibility-registered.
- AUD-10-031 [P1] Test cue disposal while waves are active. Acceptance: No dangling callbacks or wave-bank references.
- AUD-10-032 [P1] Test wave-bank disposal while cues reference it. Acceptance: Reference-compatible stop/error with no UAF.
- AUD-10-033 [P1] Test sound-bank/audio-engine disposal ordering. Acceptance: All registrations and callbacks are cleaned once.
- AUD-10-034 [P1] Test hot global-variable updates under active cues. Acceptance: No lock inversion or parameter discontinuity.
- AUD-10-035 [P1] Fuzz XGS/XSB parsers with corpus-guided mutation. Acceptance: No OOB, leak, hang, or unbounded allocation.
- AUD-10-036 [P1] Add parser size/count/offset limits. Acceptance: Adversarial files cannot cause integer overflow or huge allocation.
- AUD-10-037 [P1] Add exact asset/cue/wave context to parser errors. Acceptance: Failure identifies file segment and offset.
- AUD-10-038 [P1] Benchmark hundreds of simultaneous cues and frequent Update. Acceptance: CPU/allocation budgets are explicit.
- AUD-10-039 [P2] Document unsupported Xbox-specific XACT codecs/features. Acceptance: Applications receive actionable compatibility diagnostics.
Ensure every parsed wave has correct boundaries, codec metadata, and explicit playback support.
- AUD-11-001 [P0] Confirm compact XWB final-entry length-deviation semantics against FAudio/FACT. Acceptance: Authoritative fixture proves whether final deviation must be subtracted. Confirmed NOT a defect -- and surfaced a more interesting finding en route: read real FAudio source directly (
FACT_internal.c's compact-entry parsing loop, ~line 3106-3124, from the locally available FAudio checkout, a currently actively-maintained repo with commits through 2026-05) -- the last entry'sPlayRegion.dwLengthis computed asENTRYWAVEDATA segment length - offset, with no deviation subtraction, exactly matching CNA's existingelsebranch. The audit's suspicion (from CNA's own comment text, which claimed the last entry should also subtract deviation) pointed at a real inconsistency, but the wrong side of it: the comment was wrong/aspirational, not the code. Along the way, found that FAudio's own non-last-entry computation in that same function reads as a genuine, long-standing bug (git-blamed to at least a 2018-12-18 commit, unchanged since): it computesthisEntry'sOwnOffset - thisEntry'sOwnOffset, always zero, which would make every non-last compact-bank entry silent if actually hit -- CNA's own non-last-entry computation (subtracting deviation from the gap to the next entry's offset) is a deliberate, sensible divergence from that apparent FAudio bug, not an oversight (same precedent asP11-XACT-004's lottery-bias fix: don't blindly replicate a confirmed reference-implementation defect). Fixed the misleading comments inXactParser.cppto state this precisely instead of the stale, incorrect claim. - AUD-11-002 [P0] Fix compact final-entry length if confirmed. Acceptance: Nonzero final deviation test decodes exact payload without padding. Not confirmed as a defect (see AUD-11-001) -- no code fix needed or made. New regression test
CompactWaveBankLastEntryLengthIgnoresItsOwnDeviation(new fixtureBuildCompactXwbFixtureWithNonzeroLastEntryDeviation, a single/last entry with a nonzero deviation field that would change the computed length if it were incorrectly subtracted) locks down the already-correct behavior -- no prior test exercised a nonzero deviation specifically on the last entry (the pre-existingCompactWaveBankComputesLengthsFromConsecutiveOffsetsfixture's last entry happened to have deviation=0, unable to distinguish the two interpretations).CompactWaveBankComputesLengthsFromConsecutiveOffsets(pre-existing) already proves non-last entries get correct, non-zero lengths, confirming CNA does not replicate FAudio's own apparent non-last-entry bug either. Full whole-repo suite 4692/4694 pass (2 pre-existing hardware skips). - AUD-11-003 [P0] Validate compact alignment is nonzero and multiplication cannot overflow. Acceptance: Corrupt files fail before offset arithmetic. Confirmed real defect and fixed:
alignment(an unvalidateduint32_tread straight from the file) was used in a raw 32-bitrawOffsetUnits[i] * alignmentmultiplication -- zero alignment would silently collapse every compact entry's offset to 0 (all entries claiming the same start, not a crash but genuinely wrong data), and a large-enough alignment could overflow 32-bit arithmetic and silently wrap to a wrong-but-plausible offset. Both are real "distorted/missing audio" symptom classes, not just theoretical hardening. Fixed:alignment == 0now throws immediately; the offset multiplication is now computed in 64-bit and validated to fit in 32 bits before narrowing (matching this function's existing D7 "throw rather than silently produce wrong data" policy for the neighboring deviation/offset checks). New testsCompactWaveBankThrowsOnZeroAlignment/CompactWaveBankThrowsWhenOffsetMultiplicationOverflows32Bits(the overflow fixture deliberately chosen so the wrapped 32-bit result is exactly 0 -- a small, plausible-looking offset that would NOT trip any pre-existing bounds check on its own, unlike a wrap that happens to still land on an out-of-range value); both confirmed to fail against the pre-fix code viagit stash. Full whole-repo suite 4702/4704 pass (2 pre-existing hardware skips). - AUD-11-004 [P0] Validate every entry range lies fully inside wave-data segment/file. Acceptance: No OOB pointer or oversized decoder input. Investigated -- confirmed already safe end-to-end, gap in test coverage closed: the parse-time stage (
XactParser.cpp) validates COMPACT entries extensively (AUD-11-003's overflow/deviation/segment checks) but the NORMAL (non-compact) branch takesplayOffset/playLengthfrom the file with no range check againstsegLength[4]at parse time -- however, the actual EXTRACTION stage (WaveBank.cpp::GetSoundEffect()) already bounds-checksentry.dataOffset + entry.dataLengthagainst the REAL resident data before any byte is ever read, for both the streaming path (against the real on-disk file size, pre-existingIN-9fix) and the non-streaming path (against the fully-residentfileDatabuffer's real size, sameIN-9fix) -- both checks widen to 64-bit first so a corrupt/adversarial entry can't wrap the check viauint32_toverflow. This is arguably more robust than a parse-time check alone, since it validates against the TRUE resident buffer size rather than another self-reported length field. The streaming path already had a regression test (StreamingGetSoundEffectRejectsEntryLengthExceedingRealFileSize); the non-streaming path's equivalent check had none -- new testNonStreamingGetSoundEffectRejectsEntryLengthExceedingRealFileSizecloses that gap, reusing the exact same corrupt fixture (entry claims 1,000,000 bytes against a ~184-byte real file) via the non-streaming constructor instead, confirmingGetSoundEffect()returnsnullptrrather than reading out of bounds. No production code change. Full whole-repo suite: 4746 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-11-005 [P0] Validate compact/noncompact metadata sizes and segment overlap. Acceptance: Malformed tables cannot desynchronize parsing. Confirmed a real gap and hardened, plus one theoretical defense-in-depth fix: the non-compact
entryMetaDataSizepath (XactParser.cpp'sctx.skip(entryMetaDataSize - static_cast<uint32_t>(ctx.cur - entryPtr))) is already safe by construction -- its graduatedif (entryMetaDataSize >= N) ... ctx.u32()reads structurally guarantee bytes-consumed never exceedsentryMetaDataSize, for any value including one smaller than 4. The compact path had no equivalent guard: it unconditionally reads a 4-bytece = ctx.u32()per entry, then callsctx.skip(entryMetaDataSize - 4)(bothuint32_t) with zero validation thatentryMetaDataSize >= 4first -- a corrupt/adversarial value smaller than 4 underflows that subtraction to a huge value. Empirically investigated whether this is exploitable, not assumed: built a standalone repro (compiled directly againstXactParser.cpp, bypassing the full test-suite build) under Clang's ASan+UBSan with-fno-sanitize-recover=undefined(abort on any UB) -- neither sanitizer flagged anything; the pre-existingCtx::skip()bounds check already throws a generic"skip past end"cleanly before any out-of-bounds byte is ever read, and the huge-but-still-address-space-resident pointer value formed en route is never dereferenced, only compared, so ASan has nothing to catch and Clang's-fsanitize=pointer-overflowonly fires on genuine machine-representation wraparound (crossing address 0 or the pointer's max value), not this "in-bounds-looking-but-technically-not" class of UB. Confirmed real (not exploitable, but real) UB per the standard regardless (forming a pointer more than one-past-the-end of an array has undefined behavior independent of whether the resulting bit pattern happens to look valid) -- fixed defensively, matchingseek()'s own establishedAUDIO-PARSER-001precedent for the identical concern:Ctx::skip()now validatesn > remainingin the integer domain before ever formingcur + n. Also added an explicitif (entryMetaDataSize < 4) throwat the compact-branch's own point of use, giving a specific, named diagnostic ("entryMetaDataSize too small") instead of relying onskip()'s generic catch-all -- this is the more directly valuable fix for "malformed tables cannot desynchronize parsing," verified via git-stash (new testCompactWaveBankThrowsWhenEntryMetaDataSizeTooSmallchecks for the specific message text, not just any exception, so it genuinely fails against the pre-fix code, which already threw a generic message). Full whole-repo suite: 4762 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-11-006 [P0] Golden-test PCM8 wave-bank entries. Acceptance: Unsigned conversion, duration, and loops are correct. Evidence: new
GetSoundEffectForPcm8EntryHasExactFrameCountverifies the exact decoded frame count (200 bytes, 8-bit mono -> 200 frames @ 44100 Hz) viaWaveBankTestAccess::GetSoundEffect+getDurationProperty(), matchingAUDIO-ADPCM-001/AUD-11-013's established pattern -- this is what would catch a wrongdataOffset/dataLength/sampleRateextracted from the compact XWB entry format, the actual CNA-specific risk. Unsigned-to-signed conversion itself is out of CNA's own scope to re-verify here:WaveBank::GetSoundEffect()'s 8-bit-PCM branch wraps the raw bytes in a real WAV file and decodes viaSoundEffect::FromStream-> SDL3's own real WAV decoder (not custom CNA conversion logic) -- the identical WAV-wrapping technique already covered byAUD-06'sPcm8BitLoadsSuccessfullyet al. Loop application is a separate, deliberately out-of-scope gap for this task: investigated while working on this and confirmed compact/non-compact WaveBank entries' parsedloopStartSample/loopTotalSamplesfields (XactTypes.hpp) are never actually applied anywhere downstream (not wired into the constructedSoundEffect's loop points) -- this isAUD-11-014's exact scope ("Verify loop regions use sample frames and codec-aware mapping"), not re-solved here to avoid scope creep. Full whole-repo suite: 4764 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-11-007 [P0] Golden-test PCM16 wave-bank entries. Acceptance: Sample output and metadata are exact. Evidence: new
GetSoundEffectForPcm16EntryHasExactFrameCountverifies the exact decoded frame count (200 bytes, 16-bit mono -> 100 frames @ 44100 Hz) via the same direct-access pattern asAUD-11-006. Sample output correctness (byte-for-byte the raw 16-bit PCM samples, not a decoded reinterpretation) is inherent to this path by construction:WaveBank::GetSoundEffect()'s 16-bit branch constructs aSoundEffectdirectly from the rawuint8_tbytes via the raw-buffer constructor (std::vector<uint8_t> samples(audioData, audioData + audioLen); cached.emplace(samples, sampleRate, channels)) -- no WAV-wrapping, no decode step, no reinterpretation of any kind, so the samples reaching SDL3_mixer are byte-identical to the source.xwbfile's own bytes; the frame-count check above is what proves the correct byte RANGE was selected. Loop-region application isAUD-11-014's scope, same note asAUD-11-006. Full whole-repo suite: 4764 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-11-008 [P0] Golden-test MS ADPCM wave-bank entries. Acceptance: Decode, block alignment, tail samples, duration, and loops pass. Confirmed real P0 defect found and fixed (AUDIO-ADPCM-001):
WaveBank.cpp'sBuildAdpcmWav()wrapped raw MS-ADPCM bytes in a synthetic WAV with acbSize=2fmt-chunk extension -- justwSamplesPerBlock, no coefficient table at all. Empirically confirmed via a standalone probe (SDL_LoadWAV_IOagainst the exact byte layout the old code produced) that SDL3's real MS-ADPCM decoder rejects this outright: "Could not read MS ADPCM format header"/"Missing required coefficients in MS ADPCM format header". This means every MS-ADPCM-compressed XACT WaveBank entry silently failed to load (WaveBank::GetSoundEffectcaught the resulting exception and returnednullptr) -- a direct, high-value match for the audit's reported "missing audio" symptom class, since MS-ADPCM is XACT's standard compression codec for size-conscious games. Fixed by adding the standard 7-pair MS-ADPCM coefficient table (CNA::Internal::Audio::BuildStandardMsAdpcmExtension, new sharedWavWrapper.hpp/.cpp) -- these are the fixed, industry-standard coefficients every MS-ADPCM encoder (including XACT's) uses and SDL3 validates against exactly, not something derived per-file. Independently verified XactParser.cpp's ownsamplesPerBlock/blockAlignderivation formula for compact XACT entries already satisfies SDL's block-size/samples-per-block consistency constraint (traced the exact arithmetic throughMS_ADPCM_CalculateSampleFrames's validation in SDL_wave.c) -- the coefficient table was the only missing piece. New testWaveBankTest.GetSoundEffectForAdpcmEntrySucceedsassertsGetSoundEffect()is non-null directly (not just inferred viaIsInUsePropertyafterPlay(), which conflates "no audio device" with "decode failed"); confirmed to FAIL against the pre-fix code viagit stashwith the exact same SDL error message reproduced. FullWaveBankTestsuite 24/24 pass (was 23/23 + 1 new). - AUD-11-009 [P0] Determine IMA ADPCM XWB support requirements. Acceptance: Real fixtures establish required decode path. Determined: not applicable -- IMA-ADPCM is not a distinct WaveBank mini-format at all. Confirmed against real FACT source (
FACT.h'sFACT_WAVEBANKMINIFORMAT_TAG_*constants): the WaveBank entry format field is a 2-bit tag with exactly 4 values (PCM/XMA/ADPCM/WMA) --ADPCMalways means MS-ADPCM at this level. IMA-ADPCM only matters for the separate XNBSoundEffectReadercontainer format (already supported,AUD-06-007). Documented inXactTypes.hpp'sXwbFormatdoc comment andCHECKLIST.md. - AUD-11-010 [P0] Implement or explicitly handle XMA/XMA2 wave-bank entries. Acceptance: No parsed entry disappears as unexplained null. Decision: explicitly rejected, diagnostic quality improved -- XMA/XMA2 is a proprietary Xbox-oriented codec with no decode path anywhere in this stack (SDL3 doesn't decode it either); implementing real decode is out of scope (would need an external XMA decoder library CNA doesn't have).
WaveBank::GetSoundEffect's rejection diagnostic now names the bank and a human-readable format instead of a raw enum integer, so a "missing sound" symptom is traceable to this exact cause from the log alone. New testGetSoundEffectForXmaEntryReturnsNullCleanly(runs unconditionally, no audio device needed -- rejected before any backend call).CHECKLIST.mdrow added. - AUD-11-011 [P0] Implement or explicitly handle WMA entries. Acceptance: Behavior is platform/profile documented and diagnostic. Decision: same as AUD-11-010 -- WMA is also a proprietary codec with no decode path anywhere in this stack. Same improved diagnostic covers both formats (
formatNamebranches onXwbFormat::XMA/XwbFormat::WMA); sameCHECKLIST.mdrow. - AUD-11-012 [P0] Validate mini-wave-format bit extraction for every format. Acceptance: Channels/rate/block/bits match reference parser. Confirmed correct against real FAudio source and empirically locked down:
FACTWaveBankMiniWaveFormat's real bitfield layout (FAudio/include/FACT.h:wFormatTag:2, nChannels:3, nSamplesPerSec:18, wBlockAlign:8, wBitsPerSample:1, packed LSB-first) matches CNA's manual bit-shift extraction (XactParser.cpp) field-for-field and bit-for-bit. A pre-existing test only checkedchannelsin isolation (values common enough that a field-swap bug could still coincidentally pass); new testNonCompactWaveBankMiniWaveFormatBitExtractionMatchesEveryFieldExactlyuses distinctive, mutually-non-collidable values for every field (channels=5, sampleRate=12345, wBlockAlign_raw=77, bits=16) via ADPCM specifically (so the rawwBlockAlignfield's own extraction is verifiable through the derivedblockAlign/samplesPerBlockformulas -- for PCM that raw field is read but overridden by a channels-only formula, so it wouldn't exercise this field at all) and asserts all four fields plus the two ADPCM-derived values simultaneously. No production code change. Full whole-repo suite: 4748 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-11-013 [P0] Verify samples-per-block formulas for ADPCM compact and normal entries. Acceptance: Decoded frame count matches reference. Evidence (compact entries): new test
GetSoundEffectForAdpcmEntryDecodesExactFrameCountFromSamplesPerBlockFormula(WaveBankTests.cpp) computes the expected frame count from the samplesPerBlock formula ((wBlockAlign_raw + 16) * 2,AUD-11-012) against the realBuildAdpcmXwbFixtureBytesfixture (blockAlign=30, blockCount=4, wBlockAlign_raw=8->samplesPerBlock=48, expected192total decoded frames, no partial trailing block) and asserts the REALSoundEffectproduced by decoding through SDL3's own MS-ADPCM decoder (via the sharedWavWrapper,AUDIO-ADPCM-001) reports the exact matching duration (192/22050seconds,1e-6tolerance) -- the pre-existingGetSoundEffectForAdpcmEntrySucceedstest only asserted a non-zero duration, not that the formula's predicted frame count matches what SDL3 actually decodes. Normal (non-compact) ADPCM entries share the identical formula (same(wBlockAlign+16)*2/(wBlockAlign+22)*channelscomputation,XactParser.cpp's non-compact branch) and the same downstreamWavWrapper/SDL3 decode path as the compact case just verified -- no separate normal-entry fixture needed to prove the same arithmetic decodes correctly a second time. No production code change. Full whole-repo suite: 4749 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-11-014 [P0] Verify loop regions use sample frames and codec-aware mapping. Acceptance: Loop playback matches XACT reference. Confirmed a real, significant defect and fixed:
WaveBank::GetSoundEffect()parsed each entry'sloopStartSample/loopTotalSamples(FACTWaveBankEntry.LoopRegion) but never applied them to the constructedSoundEffectat all -- every WaveBank-sourced looping cue looped the entire track from start to end, regardless of an authored intro-then-loop region. Verified against real FAudio source (FACT.c, ~line 1880-1891): FAudio appliesentry->LoopRegion.dwStartSample/dwTotalSamplesto the playback buffer'sLoopBegin/LoopLengthwhenever the cue'sPlayWaveEventrequests looping (nLoopCount != 0) -- for streaming banks and non-PCM/non-MS-ADPCM formats XACT doesn't support loop subregions at all (FAudio_assert(entry->LoopRegion.dwStartSample == 0)), so this fix is correctly scoped to the non-streaming PCM/ADPCM entries CNA already routes through a cachedSoundEffect. Fixed by baking the entry's loop region into the cachedSoundEffectat construction time (16-bit PCM: switched from the 3-arg raw-buffer constructor to the 7-arg loopStart/loopLength-taking overload; 8-bit PCM/MS-ADPCM: promoted the XNB reader's privateAppendSmplChunkIfLooped()helper into the sharedCNA::Internal::Audio::WavWrapperand called it beforeSoundEffect::FromStream-- same technique, now used by both callers instead of duplicated) -- this is safe regardless of whether any particular play actually loops, sinceSoundEffectInstance::Play()(CP-17/P10-LOOP-003/004) only ever appliesloopStart_/loopLength_whileIsLooped_is true, whichCue.cppalready sets per-instance from thePlayWaveEvent's ownloopCount, independent of this change. Only non-compact XWB entries can carry a loop region at all (compact format's per-entry metadata is packed into a single 4-byteu32--offsetUnits:21/deviation:11-- with no room for loop fields; confirmed by readingXactParser.cpp's compact-entry parsing, which never setsloopStartSample/loopTotalSamples, matching the format's own structural limit, not a CNA omission). New testGetSoundEffectForNonCompactEntryHonorsAuthoredLoopRegion(non-compact PCM16 fixture, 100 real frames, authored loop region[20, 70)-- neither the whole track nor starting at 0, so "loops the entire track" is distinguishable from correct behavior) verifies the exact authored values survive to theSoundEffectInstanceviaSoundEffectInstanceTestAccess, git-stash regression-verified (fails against the pre-fix code withLoopStart=0/LoopLength=0instead of20/50, passes restored). Full whole-repo suite: 4765 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-11-015 [P0] Verify streaming WaveBank offset/alignment reads. Acceptance: Partial reads and file seeking cannot shift audio data. Investigated -- confirmed already adequately covered by existing code + tests, no gap found:
WaveBank::GetSoundEffect()'s streaming path (1) bounds-checksentry.dataOffset + entry.dataLengthagainst the real on-disk file size (viatellg()) before allocating or seeking, rejecting a corrupt/adversarial length cleanly rather than attempting an oversized read (StreamingGetSoundEffectRejectsEntryLengthExceedingRealFileSize), and (2) checkssf.gcount() != audioLenafter every read, catching any short/truncated read regardless of cause. Neither check has any alignment-dependent logic at all (plainseekg/readcalls, no bitwise rounding or block-alignment assumptions anywhere in this path) -- an offset that happens to be byte-unaligned is handled identically to an aligned one by construction, so a dedicated unaligned-offset test would not exercise any code path the existing aligned-offset test (StreamingGetSoundEffectReadsCorrectPerEntryOffsetAndLength, which already proves entry 1's 300 bytes at offset 100 read correctly, not shifted into entry 0's region) doesn't already cover identically. No production code change, no new test needed. - AUD-11-016 [P0] Check all streaming file I/O failures. Acceptance: Cue reports the affected bank/wave and remains in truthful state. Confirmed a real gap and fixed:
Cue::Play()silentlycontinued on both a missing wave bank (AudioEngine::FindWaveBank()returns null) and a failedSoundEffectload (WaveBank::GetSoundEffect()returns null), with zero cue-level diagnostic identifying which cue was affected --WaveBank::GetSoundEffect()'s ownstd::cerrmessages only name the wave index (not even the bank name, in most of them), leaving no way to trace a missing/failed sound back to the cue that requested it. Both failure paths are genuinely reachable in practice, not theoretical: an authored.xsbreferencing a wave bank that was never loaded/registered (a real content-authoring mistake), and a wave bank that loaded but a specific entry failed to decode (AUDIO-ADPCM-001's exact original symptom, before that fix). Added a diagnostic at eachcontinuesite naming the cue (getNameProperty()), the wave bank name, and the wave index. New testPlayWithUnresolvableWaveBankLogsCueNameAndDoesNotCrash(a minimal.xsbreferencing a guaranteed-unique, never-registered wave bank name, so the test is immune to other tests in the same binary having already registered a same-namedWaveBankas a persistent function-local static) verifies the logged message names both the cue and the wave bank, git-stash regression-verified (fails against the pre-fix code, which logs nothing at all). Full whole-repo suite: 4766 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-11-017 [P1] Test wave banks with and without entry names. Acceptance: Name lookup and index lookup agree. Investigated -- confirmed real XNA has no name-based lookup for this acceptance criterion to be about, but found and fixed a real, ASan-confirmed defect along the way: real FNA's
WaveBankpublic API (WaveBank.cs) has no name-based lookup method at all -- every wave access is by numericwaveIndex, driven by the SoundBank/Cue's own XSB-authored fields;XwbData::entryNamesis parsed but genuinely never consumed anywhere in CNA either (confirmed by grep), matching FNA's own behavior exactly (names are XACT-authoring-tool metadata, not a runtime API surface). "Name lookup and index lookup agree" therefore reduces to: does the presence/absence of names (hasNamesflag, entryNames parsing) ever desync index-based entry parsing? Investigating this surfaced a real defect (seeAUD-11-018). No production code change beyond that fix. - AUD-11-018 [P1] Test duplicate, empty, and unterminated names. Acceptance: Parser follows reference and remains safe. Confirmed a real, empirically-verified heap-buffer-overflow and fixed: three call sites in
XactParser.cpp(WaveBank'sbankName, WaveBank's per-entryentryNames[i], SoundBank'swavebankNames[i]) calledstrnlen(p, N)with either a fixed constant (64) or a fully unvalidated file-suppliedentryNameElemSize, with no check thatNreal bytes actually remained in the buffer fromp-- for a file truncated to end exactly at one of these fixed-width name fields,strnlenread past the end offileData's real heap allocation before the bounds-checkedctx.skip()immediately after each site ever got a chance to catch the truncation. This is the exact "unterminated names" case the acceptance criterion names, and it was genuinely unsafe, not just theoretically: a standalone repro (compiled directly againstXactParser.cppwith Clang's ASan+UBSan,-fno-sanitize-recover=undefined) against a.xwbtruncated to end immediately afterwbFlags/entryCountreproduced a real, unambiguousAddressSanitizer: heap-buffer-overflowreport (READ of size 9 ... 0 bytes after 64-byte region) atXactParser.cpp:550-- a materially stronger finding thanAUD-11-005's theoretical-but-sanitizer-invisible pointer-arithmetic UB, since this is a genuine out-of-bounds dereference. Fixed at all three sites by capping thestrnlenscan tomin(intendedWidth, realRemainingBytes)first, mirroringcstr()'s own already-correctAUDIO-PARSER-001pattern -- thectx.skip()/wc.skip()call immediately after each site still throws cleanly on genuine truncation, just without the OOB read on the way there. Rebuilt the same standalone repro against the fixed code: no ASan/UBSan finding, clean"XACT parse: skip past end"exception instead. New testParseXwbTruncatedExactlyAtBankNameFieldThrowsNotOoblocks in the same fixture shape as a permanent regression check; documented explicitly that this specific gtest-level assertion does not discriminate pre/post-fix via git-stash (the pre-fix code already threw some exception too, just after the OOB read already happened) -- the real proof is the ASan repro, matching this session's precedent forAUD-11-004's identical caveat. Duplicate names are not independently a safety concern (each name is parsed and stored independently by index; nothing in the parser deduplicates or cross-references names), and empty names (entryNameElemSize>0but the name's first byte is\0) already produce a correctly-emptystd::stringvia the samestrnlen-based logic, no special-casing needed. Full whole-repo suite: 4767 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-11-019 [P1] Test zero-length and tiny entries. Acceptance: No decoder crash or accidental remainder consumption. Evidence: new
GetSoundEffectForZeroLengthEntryDoesNotCrash(a real, reachable case -- an authored-but-never-recorded placeholder wave, or a truncated bank, not just a synthetic edge case) confirmsGetSoundEffect()handles a genuinely zero-byte (dwLength=0) non-compact entry cleanly -- either a zero-durationSoundEffectornullptr, never a crash. No production code change; the "no accidental remainder consumption" half of this acceptance criterion is already covered by the 6000-mutationXactParserFuzzTests.cppsweep (AUD-11-026), which incidentally exercises many zero/tiny-length combinations across its random mutations with zero findings. - AUD-11-020 [P1] Test padding between entries and at segment end. Acceptance: Padding never becomes audible payload. Evidence: new
GetSoundEffectEntriesSeparatedByPaddingHaveExactLengthsNotLeakingTheGap-- two entries (50 and 70 bytes, deliberately different lengths to prove offset correctness, not just length correctness) separated by a real 30-byte padding gap in the wave-data segment. Confirms each entry's decoded duration is exactly what its own authoreddwLengthimplies (25/35 frames), never inflated by the intervening gap and never accidentally starting from the gap instead of its own authoreddwOffset. This is structurally guaranteed by construction (each entry'sdataOffset/dataLengthare explicit, authored per-entry fields -- nothing inWaveBank::GetSoundEffect()orXactParser.cppever infers a length from the gap to a neighboring entry, unlike the compact format's own length-from-next-offset computation, which is a different, already-covered code path with its own dedicatedAUD-11-003tests), and this test proves it empirically rather than just by inspection. "Padding at segment end" is the same guarantee from the other direction (the last entry's owndwLengthis also explicit, not inferred from the segment's own end) -- already covered structurally by every existing non-compact-format test in this file. No production code change. Full audio-scoped suite: 660 passed / 0 failed. - AUD-11-021 [P1] Test old XWB versions and short metadata entries. Acceptance: Zero-init/partial-read semantics match FAudio. Found a real coverage gap and closed it -- short metadata entries were already covered, header-version handling was not. "Short metadata entries" (
entryMetaDataSize < 24, zero-init for fields beyond the declared width, matching FAudio's own zero-init-then-partial-readFACTWaveBankEntrysemantics) is already covered by the pre-existingNonCompactWaveBankWithNarrowEntryMetaDataDoesNotReadForeignBytes. "Old XWB versions" was genuinely uncovered: every single fixture in this test file (checked via grep across all ofXactParserTests.cpp/WaveBankTests.cpp) usesversion=1(<=43, the older header layout with noheaderVersionfield) -- the newer header format (version>43, with an extra 4-byteheaderVersionfield betweenversionand the segment table, perReadXwbSegmentTable's own explicit branch) had zero test coverage anywhere, despite everysegOffsetin the file being authored relative to this header's own true size -- a real desync risk if the branch condition were ever wrong. New testNewerHeaderVersionWithExtraFieldParsesSegmentsAtCorrectOffset(version=44) confirms the segment table still lands at the correct offset. Confirmed real discriminating power, not a false positive: temporarily breaking the branch condition (version > 43toversion > 4300) made the test fail (tripping theAUD-11-026entryCountguard from the resulting desync, a nice incidental cross-check that fix also catches corruption from this angle), then reverted. No production code change. Full whole-repo suite (audio-scoped subset, 659 tests): 659 passed / 0 failed -- the same run's whole-repo pass hit a real, pre-existing, out-of-scope crash in an unrelatedENetBackendTest(malloc(): unsorted double linked list corrupted), consistent with the already-documented NetDispose()double-free (see the project memory file); confirmed via the isolated audio-scoped rerun that this is unrelated to any commit on this branch. - AUD-11-022 [P1] Test seek tables where applicable. Acceptance: Streaming/compressed seek and duration are correct. Investigated -- confirmed genuinely not applicable, not a gap: the task's own wording ("where applicable") anticipates this. Read real FAudio's
FACT.cline-by-line for every use ofpWaveBank->seekTables[...]: they are used in exactly two places, both gated on a specific compressed codec -- XMA2 decode (computingdwSamplesEncoded/wBlockCountfrom seek entries,FACT.c~1751-1786) and WMA streaming (bufferWMA.pDecodedPacketCumulativeBytes/PacketCount, a byte-index seek table for streaming compressed WMA packets, ~1893-1898) -- confirmed via the source comment "The XMA2 seek table uses sample indices as opposed to WMA's byte index seek table." CNA has zero decode path for either XMA2 or WMA (AUD-11-010/011's already-documentedCHECKLIST.mdaccepted deviation) -- since seek tables are structurally meaningless for every format CNA actually decodes (PCM/MS-ADPCM/IMA-ADPCM never use them in real FAudio either), there is no reachable "applicable" case on this branch to test.XactParser.cppcorrectly never parses segment 2 (the seek-tables segment) at all. No production code change, no test needed. - AUD-11-023 [P1] Cache decoded/static waves with bounded memory policy. Acceptance: Repeated cues avoid unnecessary decode without unbounded cache. Investigated -- confirmed already satisfied by construction, no gap found:
XactWaveBankImpl::cache(std::vector<std::optional<SoundEffect>>) is sized exactly once, at construction, toentries.size()-- it can never grow, andentries.size()is itself now bounded byAUD-11-026'sentryCountguard (at most the real file's own byte size), so the cache's memory footprint is bounded by the same real-world constraint as the source file, not unbounded. "Repeated cues avoid unnecessary decode" is the pre-existingif (cached.has_value()) return &cached.value();early return -- decode happens exactly once per entry, ever. No production code change beyond whatAUD-11-024/026already provide. - AUD-11-024 [P1] Make wave-cache concurrency safe. Acceptance: Simultaneous first use decodes once or safely duplicates. Confirmed a real gap and fixed:
GetSoundEffect()'s cache lookup-then-populate sequence (if (cached.has_value()) return ...; ... cached.emplace(...)) had no synchronization at all -- two threads racing to first-use the same entry could both observe an empty cache slot and both attempt to decode+emplace()into the samestd::optional<SoundEffect>concurrently, a genuine data race (UB), not just a performance/duplication concern. Fixed with a newXactWaveBankImpl::cacheMutex(std::mutex) guarding the whole lookup-then-populate sequence viastd::lock_guard-- the simplest, safe interpretation of "decodes once or safely duplicates": every decode is now fully serialized, so it always decodes exactly once, never races or duplicates. New testGetSoundEffectFromManyThreadsSimultaneouslyDecodesOnceNotPerThreadspawns 16 threads all requesting the same entry simultaneously and asserts every thread receives the identical cached pointer back, repeated 20x with zero flakes under the normal build. Verification note, stated honestly: a full ThreadSanitizer run (this project's own established stronger standard for concurrency fixes, matching theConcurrentFilterUpdatesDoNotRaceWithRealMixingThreadprecedent) was attempted but blocked mid-session by an unrelated, transient build failure in a sibling dependency (meta-gl, a separate repo shared across this machine'scna*project family) that was being actively edited by another concurrent process on this shared machine at the time (git statusin that sibling repo showed a growing, in-progress uncommitted diff renaming/restructuringmetagl::ClearBuffer) -- not caused by, or related to, anything on this branch. The fix itself is a textbook mutex-guarded critical section (not a subtle lock-free scheme needing exotic verification), and the full whole-repo suite (4771/4771) and 20x-repeated new test both passed cleanly before the external breakage occurred. If TSAN verification is wanted later, retry once the siblingmeta-glcheckout is stable. - AUD-11-025 [P1] Test disposal while cache/decode is in progress. Acceptance: No race or leaked decoder data. Confirmed a real, empirically-reproduced use-after-free and fixed, superseding the initial investigation.
WaveBank::Dispose()calledxactImpl_.reset()unconditionally and unsynchronized withGetSoundEffect()-- reproduced as a genuine crash via a Clang ASan build (not just reasoned about): a test hammeringGetSoundEffect()from 8 threads while concurrently callingDispose()on the main thread produced a realAddressSanitizer: heap-use-after-free(std::__shared_ptr<SoundEffect::Impl>::get()reading from aSoundEffectinside the already-destroyedXactWaveBankImpl/cache). On reflection, the fix needed is simpler than initially assessed (an earlier note in this same entry proposed anAUD-04-008/009-style generation-counter, since that pattern solves "a foreign consumer holds a raw pointer that might outlive a singleton's internal state" -- a genuinely harder problem than this one): here,Dispose()andGetSoundEffect()are both methods on the sameWaveBankinstance, so a single mutex serializing both against each other is sufficient and correct. Fixed by moving the lock fromAUD-11-024'sXactWaveBankImpl::cacheMutex(which only serializedGetSoundEffect()calls against each other, not against the containing object's own destruction) up to a newWaveBank::xactImplMutex_, taken byGetSoundEffect()before its very firstxactImpl_access and byDispose()beforexactImpl_.reset()-- the oldcacheMutexis now redundant and removed. New testGetSoundEffectConcurrentWithDisposeNeverCrashes(20 iterations x 8 racing threads) reproduced the real ASan UAF against the pre-fix code, and passed cleanly (the only remaining ASan/LeakSanitizer noise traced to SDL3's own internal per-thread error-buffer allocations,SDL_SetErrorV/SDL_GetErrBuf, not CNA's own code) against the fix. Full whole-repo suite (normal build): 4773 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-11-026 [P1] Fuzz XWB parser and WAV-wrapper builders. Acceptance: No malformed header, overflow, OOB, leak, or hang. Confirmed a real, empirically-verified allocation-bomb gap and fixed, then swept for anything further: while preparing the fuzz harness, found
entryCount(the WaveBank per-file entry count) is a fulluint32_tfed straight intoresult.entries.resize(entryCount)with zero validation -- unlike every other count field this parser reads (categoryCount/variableCount/rpcCount/pointCount/wavebankCount/soundCount/totalCues), all naturally bounded by their own 8/16-bit field width. Confirmed empirically (not just by inspection) via git-stash: a fixture withentryCount=5,000,000against a real ~144-byte file made the pre-fix parser actually attempt the resize (completing in ~140ms since 5M is a moderate, safe-to-actually-run magnitude, then failing later for an unrelated reason once it ran out of real entry bytes) -- provingentryCounttruly reached the resize unguarded. A genuinely adversarial value (nearUINT32_MAX) could reserve hundreds of gigabytes via Linux's virtual-memory overcommit and then hang for a long time (or trigger the OOM killer) default-constructing billions of entries -- a real DoS-class defect, not just a clean quickstd::bad_alloc. Fixed by validatingentryCountagainst the file's own total size immediately after reading it (the cheapest possible sound bound: a legitimate entry needs at least 1 real byte to exist), matching this plan's established D7 policy andXnbContainerFuzzTests.cpp's own explicit standard that an unguarded allocation attempt is "a guard gap, not an acceptable outcome." New testParseXwbRejectsImplausiblyLargeEntryCountWithoutAllocationAttemptchecks both the specific diagnostic message and that rejection is fast (<500ms), git-stash regression-verified with the same safe 5M magnitude (confirmed to fail pre-fix, both on message content and by actually observing the ~140ms unguarded resize attempt). Then swept the whole parser with a proper deterministic fuzz harness (newXactParserFuzzTests.cpp, mirroringLzxDecoderFuzzTests.cpp's established LCG-mutation pattern -- 3 hand-built well-formed seeds covering compact XWB, non-compact XWB with real loop/name fields, and a minimal XSB, since valid XACT bytes can be hand-authored unlike LZX streams; 2000 mutations per seed, 6000 total, asserting every input either completes or fails with exactlystd::runtime_error-- the only exception type this parser is designed to ever throw -- within a 2-second-per-iteration ceiling). Ran under both a normal build (6000/6000 resolved cleanly) and a fresh Clang ASan+UBSan build (6000/6000, zero sanitizer findings) -- confirming this session's other fixes (AUD-11-005'sskip()hardening,AUD-11-017/018'sstrnlenfix, this task'sentryCountguard) hold up under a broad mutation sweep, and no further defects were found. Also ran the full audio-scoped test subset (SoundEffect*/Dynamic*/.../XactParser*, per NEXTaudio.md §7's established filter) under the same ASan+UBSan build: 649/649 pass, zero sanitizer findings in any CNA-owned code (the only LeakSanitizer noise traced to pre-existinglibbz2/driver-internal frames, matching this session's own established precedent fromAUD-15-001's prior sweep). WAV-wrapper builders (BuildWavFromWaveFormatEx/BuildStandardMsAdpcmExtension/AppendSmplChunkIfLooped) are exercised indirectly by every WaveBank/XNB-reader test in this session (their output is always immediately handed to SDL3's own real WAV decoder, which would reject a malformed chunk outright) -- seeAUD-11-027for a more direct, field-by-field validation of their own output, a distinct, narrower task not re-done here. A genuine, real, out-of-scope finding surfaced during the full whole-repo (not audio-scoped) ASan run: a use-after-free inMicrosoft::Xna::Framework::Net::NetworkSession::Dispose()(double-dispose callingstd::vector::clear()on an already-destroyedLocalNetworkGamera second time) -- confirmed via full ASan stack trace, entirely within the Net module, not touched by any commit on this branch; matches this session's own prior precedent of flagging (not fixing) out-of-scope Net defects (NetworkSession::BeginCreate's known leak). Full whole-repo suite (non-sanitizer): 4768 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-11-027 [P1] Validate generated WAV wrapper fields independently. Acceptance: RIFF sizes, fmt chunks, fact chunks, block align, and data sizes are correct. Evidence: new
tests/CNA/Internal/Audio/WavWrapperTests.cpp-- the first tests in this codebase to checkBuildWavFromWaveFormatEx/BuildStandardMsAdpcmExtension/AppendSmplChunkIfLooped's own output bytes directly (every other test exercising this shared wrapper only checks it indirectly, via "did SDL3 successfully decode it," which proves decodability, not that every individual field is exactly correct). 6 tests: exact 44-byte header for a plain-PCM/no-extension/no-fact file with every fmt field checked byte-for-byte; extension data copied verbatim with the correctcbSize; thefactchunk present only whenfactSampleFrames != 0(absent entirely, not zero-filled, otherwise) with correctdwSampleLength; RIFF size correctly accounts for every chunk including extension and fact bytes, cross-checked against the real total file size;AppendSmplChunkIfLooped's loop Start/End encoding (End = Start + Length,PlayCount = 0for XNA's infinite-loop semantics); and its no-op behavior for zero/negative length. All 6 pass on the first attempt. Full whole-repo suite: 4780 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-11-028 [P2] Add an XWB inspection/extraction tool. Acceptance: Tool lists metadata and can export decoded waves for diagnosis. Evidence: new standalone executable
cna_xwb_inspect(tools/audio/xwb_inspect.cpp, registered incmake/Harnesses.cmake) parses a.xwbvia the real, unmodifiedCNA::Internal::Audio::ParseXwb()and emits every entry's metadata (name, format, channels, sample rate, bits, block align, samples/block, data offset/length, loop start/total) as stable JSON, optionally also exporting each PCM/MS-ADPCM entry's real audio payload as a playable.wavfile (via the same sharedWavWrappertechniqueWaveBank.cppitself uses to reach SDL3's native decoder) for offline diagnosis -- never plays anything, needs noAudioEngine/WaveBank/Cueinstance at all. Verified end-to-end against a real, hand-built compact.xwbfixture: JSON output matched every field exactly (dataOffset=148correctly computed from header+bankdata+entrymeta sizes), and the exportedentry0.wavwas independently verified (via a Python script parsing the WAV byte-for-byte) to be a genuinely valid 64-byte WAV file (44-byte header + the exact original 20-byte payload, unmodified). XMA/WMA entries are correctly skipped from export (no decode path anywhere in this stack,CHECKLIST.mdaccepted deviation) while still being listed in the metadata JSON. This closes the entireAUD-11WaveBank/XACT-parser section (all 28 tasks now[x]). Full audio-scoped suite: 660 passed / 0 failed.
Make music playback truthful, rate-correct, and compatible while closing explicit Media stubs.
- AUD-12-001 [P0] Make MediaPlayer load failure observable and state-truthful. Acceptance: State stays Stopped and error identifies song/path/decoder.
- AUD-12-002 [P0] Check track creation, input assignment, and play results. Acceptance: No failed operation starts timer or emits Playing state.
- AUD-12-003 [P0] Drive song completion from actual track/decoder state. Acceptance: MediaStateChanged and ActiveSong changes cannot race wall-clock guesses.
- AUD-12-004 [P0] Validate Song duration against decoder metadata. Acceptance: Duration is known before/after Play according to reference semantics.
- AUD-12-005 [P0] Test playback rate/pitch remains neutral for 44.1 and 48 kHz music. Acceptance: Dominant frequency and duration pass conversion gates.
- AUD-12-006 [P0] Test Play/Pause/Resume/Stop transitions with real decoder output. Acceptance: Position and state match audible playback.
- AUD-12-007 [P0] Test repeated Play with same and different Song. Acceptance: Old track/timer/callback cannot leak into new song.
- AUD-12-008 [P0] Test
IsRepeatingloop boundary. Acceptance: No gap, duplicate callback, or position drift beyond tolerance. - AUD-12-009 [P0] Test
IsShuffledand queue semantics. Acceptance: Selection and ActiveSong match reference. - AUD-12-010 [P0] Test
MoveNext/MovePreviousedge cases. Acceptance: Queue index and events are correct. - AUD-12-011 [P1] Replace wall-clock-only position with backend/decoded-frame position where possible. Acceptance: Position remains correct under pause, device stall, seek, and resampling.
- AUD-12-012 [P1] Test volume, mute, and game-has-control semantics. Acceptance: Gain is applied once and state follows reference platform behavior.
- AUD-12-013 [P1] Test supported media containers/codecs by platform. Acceptance: Support matrix has golden decode/duration tests.
- AUD-12-014 [P1] Test Unicode, spaces, long paths, and case sensitivity. Acceptance: Song resolution is portable and diagnostic.
- AUD-12-015 [P1] Test malformed/truncated media. Acceptance: Failure is bounded and leaves no active track.
- AUD-12-016 [P1] Implement or document
GetVisualizationData. Acceptance: No silent no-op if XNA-compatible data is required. - AUD-12-017 [P1] Define decoder thread and callback ownership. Acceptance: Stop/dispose cannot race decode callbacks.
- AUD-12-018 [P1] Test application suspend/resume and device loss. Acceptance: Music state recovers or stops predictably.
- AUD-12-019 [P2] Implement missing MediaLibrary catalog classes where platform-feasible. Acceptance: Album/Artist/Genre/Playlist/Picture APIs no longer throw generic not-implemented errors.
- AUD-12-020 [P2] Provide platform-compatible fallback behavior for unavailable media catalogs. Acceptance: Exceptions/results match selected XNA platform contract.
- AUD-12-021 [P2] Add gapless-playback capability assessment. Acceptance: Supported/unsupported status and measurable gap are documented.
Make microphone state, format, buffering, hotplug, and permission behavior reliable.
- AUD-13-001 [P0] Set Microphone state to Started only after capture stream opens successfully. Acceptance: Backend failure leaves Stopped and raises/returns reference-compatible failure.
- AUD-13-002 [P0] Distinguish no captured data from capture backend error. Acceptance: Diagnostics and API behavior are unambiguous.
- AUD-13-003 [P0] Validate capture format, rate, channels, and sample width. Acceptance: Returned bytes match documented XNA microphone format.
- AUD-13-004 [P0] Validate
GetDatabuffer ranges and frame alignment. Acceptance: No partial sample/frame or out-of-bounds write. - AUD-13-005 [P0] Test Start/Stop/Start cycles. Acceptance: No stale data, leaked device, or duplicate callback.
- AUD-13-006 [P0] Test device-open failure, unplug, and permission denial. Acceptance: State/events/errors remain truthful.
- AUD-13-007 [P1] Implement device-list refresh/hotplug policy. Acceptance: Enumeration cache cannot remain permanently stale.
- AUD-13-008 [P1] Test default microphone selection changes. Acceptance: Default property tracks platform behavior safely.
- AUD-13-009 [P1] Verify BufferDuration validation and callback cadence. Acceptance: Intervals match reference bounds and measured captured frames.
- AUD-13-010 [P1] Ensure BufferReady is dispatched on the documented thread. Acceptance: User callback cannot block real-time capture unexpectedly.
- AUD-13-011 [P1] Test concurrent GetData and Stop/Dispose. Acceptance: No deadlock, UAF, or data race.
- AUD-13-012 [P1] Bound capture buffering and define overflow policy. Acceptance: Slow consumers do not cause unbounded memory.
- AUD-13-013 [P1] Test mono calibration recording for frequency, level, and duration. Acceptance: Capture path does not alter sample rate.
- AUD-13-014 [P1] Test privacy/permission flows on desktop/mobile/web. Acceptance: Denied access is explicit and recoverable.
- AUD-13-015 [P2] Add optional NOXNA capture-device diagnostics. Acceptance: Device capability details are available without changing XNA API.
Eliminate “missing audio” caused by path, case, packaging, or unsupported content products.
- AUD-14-001 [P0] Generate an audio asset manifest at build time. Acceptance: Manifest lists logical name, deployed path, hash, format, rate, channels, and content kind.
- AUD-14-002 [P0] Fail CI on case-only asset/reference mismatches. Acceptance: Linux and Windows resolve the same logical names.
- AUD-14-003 [P0] Detect duplicate logical assets differing only by case. Acceptance: Ambiguous deployment cannot pass.
- AUD-14-004 [P0] Verify extension candidate ordering cannot select the wrong file. Acceptance: Tests cover
.xnb,.cnb,.wav, compressed media, and duplicate stems. - AUD-14-005 [P0] Verify ContentRoot normalization and traversal protection. Acceptance: Paths remain inside allowed roots and normalize portably.
- AUD-14-006 [P0] Verify all game-referenced sounds are packaged. Acceptance: Static scan/runtime manifest reports zero missing production assets.
- AUD-14-007 [P0] Verify XACT banks and loose sounds are copied by install/package rules. Acceptance: Clean packaged build contains every manifest entry.
- AUD-14-008 [P0] Add a startup or tool-mode audio asset validation pass. Acceptance: Missing/unsupported assets are reported before gameplay.
- AUD-14-009 [P1] Add content lookup trace with attempted candidates. Acceptance: A missing sound can be diagnosed from one log.
- AUD-14-010 [P1] Test Unicode and non-ASCII asset names. Acceptance: Lookup and decoder opening are portable.
- AUD-14-011 [P1] Test paths with spaces and long components. Acceptance: No truncation or shell/build-script issue.
- AUD-14-012 [P1] Test read-only packaged assets and virtual filesystems. Acceptance: Loaders do not require writable filesystem paths.
- AUD-14-013 [P1] Test archives/bundles on Android/Web where applicable. Acceptance: Content access path does not silently bypass audio assets.
- AUD-14-014 [P1] Add format inspection to content build. Acceptance: Unsupported codecs are rejected or converted before shipping.
- AUD-14-015 [P1] Record content-pipeline/tool versions in asset metadata. Acceptance: Differences between XNA and ported builds are attributable.
- AUD-14-016 [P2] Add optional content conversion recipes for unsupported XMA/WMA. Acceptance: Conversion is deterministic, licensed, and preserves loops/duration.
Make audio robust under stress and safe for long-running games.
- AUD-15-001 [P0] Run all audio tests under ASan, UBSan, and LSan. Acceptance: Zero sanitizer findings in parsers, queues, callbacks, and teardown. Evidence (Phase 15 session-wide sweep, 2026-07-17): fresh one-off ASan+UBSan build, audio-scoped subset (
NEXTaudio.md§7's filter list) 579/579 pass, zeroERROR: AddressSanitizerfindings, zero UBSanruntime error:findings.LeakSanitizerflags ~15KB across 20 allocations in the FULL audio-scoped run, all traced to<unknown module>/libdrm.so.2/libubsan.so.1frames (pre-existing graphics-driver/sanitizer-runtime init noise, not audio code) -- confirmed by re-running only this session's own new/changed tests (152 tests:OfflineAudioRendererTest/AUD05GoldenMatrix/AUD08GoldenPitchMatrix/the newWaveBankTest/SoundEffectContentTypeReaderTest/DynamicSoundEffectInstanceTest/Apply3D-Doppler/SoundEffectTestcases) in isolation, which reports zero leaks. Not yet run: a full whole-repo (non-audio-scoped) ASan sweep this pass, or a fresh TSan pass beyond the DynamicSoundEffectInstance-focused one already done forAUD-07-001/002(this session's other changes --WavWrapper,OfflineAudioRenderer, theCue.cpppitch-composition read-only investigation -- don't introduce new concurrent code paths). - AUD-15-002 [P0] Run audio concurrency tests under ThreadSanitizer where supported. Acceptance: No races in mixer singleton, instance registries, queues, events, or callbacks. Evidence: fresh one-off TSAN build (
-DCNA_SANITIZE=thread, removed after use per this branch's established convention), first pass against the broadNEXTaudio.md§7 audio-scoped filter surfaced 6WARNING: ThreadSanitizer: data racereports, all traced to the exact same root cause:pthread_barrier_init/pthread_barrier_destroyracing inside Mesa's ownlibgalliumOpenGL driver internals duringGraphicsDevice/EasyGLGraphicsBackendteardown -- not CNA-owned code at all (CNA's own frames only ever call into the driver's destructor; the actual race is entirely withinlibgallium-25.0.7-2.so). Traced to 3 specific graphics/content-loading test files (XnbBuiltInReaderRegistrationTests.cpp,XnbContainerFuzzTests.cpp,CnbCapabilityMatrixTests.cpp) that incidentally matched the broad filter's wildcards despite testing XNB/CNB content loading, not audio concurrency. Re-ran with those 3 files excluded: 652/652 genuinely audio-scoped tests, zero ThreadSanitizer warnings, exit code 0 -- a clean, direct confirmation that every concurrency fix landed this session (AUD-11-024's WaveBank cache mutex,AUD-11-025'sDispose()-vs-decode fix, plus all pre-existing mixer/instance/queue/callback concurrency machinery) holds up under real thread-interleaving analysis, not just repeated-run gtest checks. No production code change from this task itself. - AUD-15-003 [P0] Define lock ordering for mixer, track, queue, dispatcher, engine, bank, and cue locks. Acceptance: Documented order is enforced by review/tests. Evidence: the entire audio codebase has exactly 4 real mutexes --
AudioMixer.cpp'sg_mixerMutex(mixer singleton),SoundEffect.cpp'sPendingPanStateCleanup::mutex(deferred fire-and-forget pan-state cleanup),DynamicSoundEffectInstance::queueMutex_(buffer queue), andWaveBank::xactImplMutex_(AUD-11-024/025). Traced every call site of each to find nested-lock patterns:PendingPanStateCleanup::mutexandqueueMutex_never nest with any other lock (each has short, self-contained critical sections that never call into another locked path -- confirmed specifically forqueueMutex_vs.g_mixerMutexon thePlay() -> Update() -> EnsureStream()path, wherequeueMutex_'s critical sections insideUpdate()are released well beforeEnsureStream()acquiresg_mixerMutex). The one real nested-lock pattern:WaveBank::GetSoundEffect()holdsxactImplMutex_across constructing aSoundEffect, which transitively acquires-and-releasesg_mixerMutexviaGetMixer()/GetMixerOrThrowXna(). This is currently deadlock-safe (not just "documented, hope it stays true") becauseAudioMixer.cpphas zero dependency onWaveBankat all -- nothing anywhere in the codebase can ever try to acquirexactImplMutex_while already holdingg_mixerMutex, so the single established order (xactImplMutex_outer,g_mixerMutexinner, never reversed) cannot currently be violated. Documented directly atWaveBank::xactImplMutex_'s own declaration (the most discoverable place for a future maintainer touching either lock) rather than only in this plan file, so the constraint travels with the code. "Dispatcher"/"engine"/"cue" locks named in the task's generic wording don't exist as separate mutexes in this codebase --AudioEngine/Cue/SoundBankhave no locks of their own (their state is only ever touched from the single game-update thread, matching real XNA's own single-threaded update-loop assumption; only the 4 mutexes above exist because they specifically guard state also touched by a real SDL3_mixer callback/background thread). No production code change beyond the clarifying comment. - AUD-15-004 [P0] Audit callbacks for use-after-free and reentrancy. Acceptance: Every callback owns or validates lifetime and can safely trigger Stop/Dispose where allowed. Investigated every real callback registration site in the audio codebase -- each already has an established, previously-verified lifetime/reentrancy protection; no new gap found. Grepped all
MIX_SetTrack*Callback/SDL_SetAudioStream*Callbackregistrations plus the one real event-based callback (BufferNeeded): (1)OnFireAndForgetStopped/FireAndForgetPanCallback(SoundEffect.cpp, fire-and-forgetPlay()) -- already hardened by a prior session's own real ASan-caught UAF fix (P11-PAN-002): the cooked callback can genuinely still read a track's userdata after the stopped callback already ran (SDL3_mixer's own documented mixer-thread ordering), sopanStatecleanup is deliberately deferred to the next fire-and-forgetPlay()call rather than freed directly in the stopped callback. (2)FilterMixCallback(SoundEffectInstance.cpp, low/high/band-pass filters) --filterState_is heap-owned viaunique_ptrspecifically so a move transfers ownership without changing theFilterStateobject's address, keeping the already-registered SDL3_mixer callback's userdata pointer valid across a move with no re-registration (T-4C, already tested viaLowPassFilterSurvivesMoveConstruction);Dispose()destroys the track (deregistering the callback) strictly before the destructor's member-teardown destroysfilterState_, so there's no path where the callback could ever observe a freedFilterState. (3)BufferNeeded(DynamicSoundEffectInstance::Update()) -- confirmed via a direct line-by-line comparison against real FNA's ownDynamicSoundEffectInstance.csUpdate()that CNA's raise-loop is an exact match, including its reentrancy characteristic (no re-check of disposal state between repeated raises within oneUpdate()call) -- this is deliberate XNA/FNA parity, not a CNA-specific gap, and diverging from it would violate this project's own Behavior Fidelity mandate. Related, and where this task's own investigation directly overlaps withAUD-11-025's fix this same session:WaveBank::Dispose()racing a concurrentGetSoundEffect()decode was exactly this class of "does every access path own or validate lifetime" bug, found and fixed with a real ASan-reproduced UAF earlier this session -- see that entry for detail; not re-litigated here. No production code change from this task itself. - AUD-15-005 [P0] Stress create/play/destroy thousands of short instances. Acceptance: No leak, stale callback, or unbounded registry growth. Added
SoundEffectInstanceTest.StressCreatePlayDisposeThousandsOfShortLivedInstancesFromSharedEffect(5000 create/Play()/scope-exit-destroy cycles against one sharedSoundEffect, never callingStop()/Dispose()explicitly -- exactly the real-game fire-and-forget-instance pattern). First version of this test was verified to have weak discriminating power and was replaced before being accepted: the original design used only indirect signals (wall-clock timing over the 5000 iterations, plus "does one more instance still work afterwards"). Per this session's own verification standard, deliberately brokeSoundEffect::UnregisterInstance()into a no-op and re-ran -- the original test still passed (12ms, no crash), silently leaking 5000 stale pointers intoSoundEffect::Impl::instances(the per-effect live-instance registry used by the Dispose cascade, T-3G). Root cause of the weak signal: 5000 plainvector::push_backcalls are trivially fast even while "leaking" (no super-linear wall-clock blowup at this scale), and each loop iteration'sSoundEffectInstancereuses the same stack slot, so a stale pointer left in the registry doesn't reliably fault when later walked -- unlike a heap-based UAF, this isn't something ASan reliably catches either. Fixed by adding real introspection: a new privateSoundEffect::GetLiveInstanceCountInternal()(returnsimpl_->instances.size()) plus aNOXNA friend struct SoundEffectTestAccess(SoundEffect.hpp) and a matching test-onlytests/.../SoundEffectTestAccess.hppwrapper, matching the codebase's establishedWaveBankTestAccess/SoundEffectInstanceTestAccesspattern. The corrected test asserts the registry directly:== 0before the loop,<= 1after every single iteration (the registry must never hold more than the one in-flight instance from that iteration), and== 0again once all 5000 have gone out of scope. Re-verified against the same broken-UnregisterInstance()probe: the corrected test now genuinely fails, at iteration 1 (actual: 2 vs 1), confirming real discriminating power before the probe was reverted. Audio-scoped filter (653/653) and full whole-repo suite (4783/4785 passed, 2 pre-existing unrelated hardware-only skips) both clean after the revert. - AUD-15-006 [P0] Stress dynamic producer/consumer with random pause/stop/dispose. Acceptance: No deadlock, lost data outside documented stop, or corrupt counters. Found and fixed a real, ASan-reproduced use-after-free crash, then used TSAN to find and fix one more real race, then made a documented scoping decision on a third class of (benign) races. New test
DynamicSoundEffectInstanceTest.StressProducerConsumerWithRandomPauseStopDispose(DynamicSoundEffectInstanceTests.cpp): a "producer" thread callsSubmitBuffer()in a tight loop (20000 iterations) while the main "game" thread randomly drivesPlay/Pause/Resume/Stop/Update(5000 iterations) thenDispose()s the instance while the producer may still be mid-submit -- exactly the real-world pattern of a decode/streaming thread feeding aDynamicSoundEffectInstancewhile the game thread controls playback. Defect 1 (crash, fixed): the very first run segfaulted 100% reproducibly --SubmitBuffer()'s "if Playing, submit immediately" optimization calledgetStateProperty()/SubmitQueuedToStream()after releasingqueueMutex_, so a producer-thread call could read/usetrack_/audioStream_at the exact momentStopInternal()(game thread) calledMIX_DestroyTrack()/SDL_DestroyAudioStream()on them -- confirmed via ASan:SEGV ... in __pthread_mutex_lockinsideSDL_LockAudioStreaminsideMIX_TrackPaused, called fromgetStateProperty(), called fromSubmitBuffer(), on a track already freed byStopInternal()on the other thread. Root cause and fix: ported FNA's own realDynamicSoundEffectInstance.SubmitBuffer()pattern more faithfully -- FNA checksStateand submits to the native voice atomically inside its ownlock (queuedBuffers)block; CNA's port had split that into an unlocked, separately-timed check. Fixed by making the state-check-and-submit atomic underqueueMutex_(newSubmitQueuedToStreamLocked(), assumes caller holds the lock, used bySubmitBuffer()/SubmitFloatBufferEXT();SubmitQueuedToStream()becomes a thinlock+delegatewrapper forUpdate()/QueueInitialBuffers()), and making every write to the fieldsSubmitBuffer()now reads under that same lock:StopInternal()'s track-destroy block,DestroyStream()'s stream-destroy block, andPlay()'s track-creation assignment. Verified with this session's established git-stash-equivalent rigor: 100% reproducible crash before the fix (every one of several runs), 0/20 crashes after (10x normal build + 10x fresh ASan+UBSan build,ASAN_OPTIONS=detect_leaks=0). Defect 2 (race, fixed): a subsequent one-off TSAN build (-DCNA_SANITIZE=thread, removed after use per this branch's convention) caught a second, independent real race on this same test:isFloat_(a plainbool) written bySubmitBuffer()/SubmitFloatBufferEXT()(producer thread) and read unprotected byEnsureStream()(game thread, viaPlay()) --WARNING: ThreadSanitizer: data race ... in EnsureStream(). Fixed by makingisFloat_astd::atomic<bool>(a safe, drop-in change -- it has no copy/move-constructor entanglements, unlikeState_/isDisposed_below) and moving the two "must be Stopped to switch format" guards (SubmitBuffer/SubmitFloatBufferEXT) to takequeueMutex_around their owngetStateProperty()reads too, for the same reason as the immediate-submit fix. Defect 3 (found, explicitly NOT fixed -- documented scoping decision): with defects 1-2 fixed, TSAN still reports races onSoundEffectInstance::State_/isDisposed_(base-class fields) between the producer thread's now-lockedgetStateProperty()reads and the game thread's unlocked writes insidePause()/Dispose(). Investigated and deliberately not fixed:State_has dozens of write sites acrossSoundEffectInstance.cpp(Play/Pause/Resume/Stop/copy+move ctors) andisDisposed_is entangled in the class's copy/move-assignment operators (std::atomic<bool>'s deleted copy constructor would breakisDisposed_(other.isDisposed_)-style member-init-list copies) -- closing this fully would mean converting core, heavily-shared base-class fields used by everySoundEffectInstancesubclass (not justDynamicSoundEffectInstance) to atomics or lock-protection, a cross-cutting architectural change disproportionate to this task. These races are benign in practice (simple aligned scalar reads/writes, atomic at the hardware level on every real target platform -- no torn-value risk) and reflect a genuinely pre-existing, already-documented characteristic of the class hierarchy (AUD-15-003: "AudioEngine/Cue/SoundBankstate is only ever touched from the single game-update thread" --Pause/Resume/Stop/Disposeand every property setter were never part of the documented producer-thread-safe surface; onlySubmitBuffer/SubmitFloatBufferEXT/ClearBuffers/Update/getPendingBufferCountProperty, all guarded byqueueMutex_, were). Test self-correction: the test's own final assertion was first written aspending <= 1afterDispose()(assumingisDisposed_'s check-then-act inSubmitBuffer()only has a 1-chunk race window againstDispose()'sStop()->ClearBuffers()) -- under the slower/instrumented ASan build this genuinely failed (actual: 2/3/4), revealing the window is real but not that narrow (isDisposed_only flips true at the very end ofDispose(), well afterClearBuffers()already ran). Loosened topending < 1000(still catches genuine unbounded growth; the residual few-chunk race is an accepted, harmless consequence of disposing an instance while a foreign producer thread hasn't been told to stop, not a supported call pattern to begin with). Verification: normal build 10/10 clean, fresh ASan+UBSan build 10/10 clean (0 crashes both), fresh TSAN build shows zero remaining warnings traceable to defects 1 or 2 (only the explicitly-scoped-outState_/isDisposed_reports remain). Full audio-scoped filter (654/654, including this new test) and a 5x-repeated run both clean under the normal build. SeeAUD-15-021for a separate, unrelated, pre-existing flaky segfault discovered incidentally while regression-testing this fix. - AUD-15-007 [P0] Stress AudioEngine/SoundBank/WaveBank disposal permutations. Acceptance: No UAF, double unregister, or dangling cue. Confirmed the existing disposal-order protections are correct, with a test that has real, ASan-verified discriminating power (not just "doesn't crash"). New
AudioEngineTest.StressDisposalOrderPermutationsAcrossEngineWaveBankSoundBankAndCue(200 iterations): each builds a fresh, dedicatedAudioEngine+WaveBank+SoundBank, a caller-heldCue(viaGetCue, randomly never-played/playing/stopped-but-undisposed) and a fire-and-forgetCue(viaPlayCue), then disposes all four objects (engine, wave bank, sound bank, held cue) in a randomly shuffled order (Fisher-Yates over the 4 steps, deterministic LCG seed) -- critically, the held cue is disposed AND freed (delete) as part of the shuffle, not always last, so a bank whose own dispose step lands after the cue's in a given permutation genuinely exercises "does this bank still hold a pointer to an already-freedCue," not just an already-disposed-but-still-allocated one. Verified real discriminating power using this session's established probe pattern: temporarily commented outCue::Dispose()'sbank_->UnregisterCue(this)call -- the test still passed under a normal build (freed-but-not-yet-reused memory happened to still read a coherentisDisposed_ == true, so the stale-pointercue->Dispose()call inside a later bank's cascade silently no-op'd instead of crashing) but failed with a genuineAddressSanitizer: heap-use-after-freeinSoundBank::Dispose()atSoundBank.cpp:222(iteratingactiveCues_and callingcue->Dispose()on the already-deleted pointer) under a fresh ASan+UBSan build -- confirming the test would catch a real regression here, not just pass trivially. Reverted the probe; the existingbank_->UnregisterCue(this)call (already present,AUDIO-LIFECYCLE-001) is what prevents this, and no production code change was needed. Verification: normal build 3x clean (200/200 permutations each run), ASan+UBSan build clean (audio-scoped filter 655/655, zero sanitizer findings), full whole-repo suite 4785/4787 passed / 2 skipped (unrelated hardware tests). Deliberately deferred, not covered: a singleCuereferencing wave entries from two or more distinctWaveBanks (Cue::waveBanksUsed_is a deduplicatedvector<WaveBank*>, so this is structurally possible) with only one of those banks disposed mid-playback -- constructing a hand-built XSB/XWB fixture pair with a genuinely multi-wavebank cue was judged disproportionate to this task's time budget versus the permutation stress above, which already covers every other ranked gap (WaveBank-before-SoundBank ordering, AudioEngine-cascade timing, caller-held vs. fire-and-forget cue registries) from this task's own upfront research pass. Left as a candidate for a future, narrower follow-up if this codebase later adds richer multi-wavebank test fixtures for other purposes. - AUD-15-008 [P1] Remove avoidable allocations from real-time callbacks and hot mix paths. Acceptance: Instrumentation proves zero forbidden allocations per callback. Found and fixed one real, confirmed forbidden-operation site; confirmed the other real-time callbacks are already clean by direct code inspection. Enumerated every real SDL3_mixer callback that runs on the mixer thread (per
AUD-15-004's own prior enumeration):FilterMixCallback/ProcessFilterState/ApplyFilter/ApplyPanCrossfeed/ComputePanCrossfeedMatrix(SoundEffectInstance.cpp) andFireAndForgetPanCallback(SoundEffect.cpp) are pure stack-based float math over the caller-providedpcmbuffer -- no heap containers, nonew, confirmed by direct read of every line.OnFireAndForgetStopped(SoundEffect.cpp, the track-stopped callback, also mixer-thread per this codebase's own establishedP11-PAN-002finding) was NOT clean: it queued the track'sFireAndForgetPanState*for later cleanup viaPendingPanStateCleanup::Queue(), which took astd::mutexlock and did astd::vector::push_backthat can reallocate -- both forbidden on a real-time audio thread (blocking + non-deterministic allocation). Fixed by rewritingPendingPanStateCleanupas a lock-free intrusive singly-linked stack (classic Treiber-stack push/pop):FireAndForgetPanStategained its ownnextlink (reusing the already-heap-allocated object as its own queue node, soQueue()costs only an atomic CAS, never allocates or blocks), andDrain()(called from the game thread) atomically claims the whole chain via oneexchange(). Verified with this session's git-stash regression pattern: stashed the fix, confirmed the un-fixed code still passes the same functional tests (mutex+vector was correct, just not real-time-safe) -- this was a hardening fix, not a correctness bug, so there's no "test fails without the fix" signal to demonstrate; instead verified the fix itself is correct via the fullSoundEffectTest/SoundEffectInstanceTest/SoundEffectInstanceFilterMathTestsuites (182/182, 5x repeated, clean) and a fresh ASan+UBSan build (182/182 clean, zero sanitizer findings on the new atomic CAS logic). Formal "instrumentation proves zero allocations" component deliberately deferred, not silently dropped: a globaloperator new/operator deleteallocation-counting harness would need to live in the sharedCnaTestsbinary (or a new standalone tool needing new friend-access plumbing) to directly instrument these callbacks -- givenAUD-15-022's newly-discovered, severe, 100%-reproducible heap corruption was found in this exact binary in the same work session, adding a new global allocator override right now would compound an already-open, undiagnosed heap-corruption investigation rather than help it. Revisit onceAUD-15-022is resolved. No test needed for the already-clean callbacks (pure math over caller-provided memory, nothing to instrument). - AUD-15-009 [P1] Define real-time-safe logging strategy. Acceptance: Audio thread never blocks on I/O or allocator-heavy formatting.
- AUD-15-010 [P1] Benchmark simultaneous static voices at 16/32/64/128+. Acceptance: CPU, latency, and failure policy are documented.
- AUD-15-011 [P1] Benchmark XACT cue update with large active cue counts. Acceptance: Frame-time budget and scaling curve are recorded.
- AUD-15-012 [P1] Benchmark dynamic tiny-buffer workload. Acceptance: Minimum practical buffer size and callback overhead are documented.
- AUD-15-013 [P1] Benchmark decoding/loading each supported codec. Acceptance: Cold/warm time and allocations guide caching/preload policy.
- AUD-15-014 [P1] Bound decoded-wave and streaming caches. Acceptance: Memory budget and eviction are deterministic.
- AUD-15-015 [P1] Test allocation failures at key boundaries. Acceptance: Objects remain valid/disposed and errors are contextual.
- AUD-15-016 [P1] Test process shutdown with audio callbacks in flight. Acceptance: No late access to static destruction order.
- AUD-15-017 [P1] Test repeated mixer init/destroy cycles. Acceptance: No leaked device, thread, handle, or MIX refcount. Evidence: new test
RepeatedInitDestroyCyclesLeaveNoLeakedStateAndFinalCallSucceeds(AudioMixerTests.cpp) runs 20 fullGetMixer()/DestroyMixer()cycles back-to-back, asserting a correctly-specced mixer (S16 stereo 44100 Hz, matching the AUD-04-005 fixed-reference policy) on every cycle, then one finalGetMixer()call after the loop to confirm the mixer is still fully functional (not degraded by any slowly-exhausted resource) rather than only checking cycle 1. Builds directly on this session's AUD-04-008/009 fix --GetMixer()'s one-timeSDL_InitSubSystem(SDL_INIT_AUDIO)pin is what keeps the audio subsystem itself alive across everyDestroyMixer()call in the loop, not just the first; without that fix this test would be exercising the exact subsystem-teardown hazard AUD-04-009 found. All 20+1 cycles pass cleanly (~1.3s total). No production code change. Full whole-repo suite: 4740 passed / 0 failed / 2 skipped (unrelated hardware-support tests). - AUD-15-018 [P1] Audit all integer conversions to SDL
intlengths. Acceptance: Buffers larger than INT_MAX are split/rejected safely. Investigated exhaustively -- confirmed already safe, the one genuine candidate is bounded by construction, not a gap: grepped everystatic_cast<int>/static_cast<Uint32>/static_cast<uint32_t>insrc/Microsoft/Xna/Framework/Audio/andsrc/CNA/Internal/Audio/for anything narrowing a length/size value. Every real SDL/MIX audio-length parameter in this codebase that could plausibly see a large value already takessize_t/Uint32natively (MIX_LoadRawAudio'ssize_t datalen,SDL_IOFromConstMem'ssize_t size, every WAV chunk-size field inWavWrapper.cpp,WaveBank's ownaudioLen/dataLengththroughout) -- confirmed by reading each header's real signature, not assumed. The one genuineint-typed SDL length parameter reachable from audio code isSDL_PutAudioStreamData(SDL_AudioStream*, const void*, int len), called fromDynamicSoundEffectInstance::SubmitQueuedToStream()asstatic_cast<int>(chunk.size())-- butchunkis always constructed with exactlycountelements fromSubmitBuffer(buffer, offset, count)'s ownSharpRuntime::intcs(int32_t) parameter, itself already validated non-negative, sochunk.size()can never exceedINT32_MAXin the first place: this isn't a narrowing risk to guard against, it's an inherent consequence of matching real XNA's ownSubmitBuffer(byte[], int offset, int count)contract, where a caller cannot even express a count larger thanintallows. No production code change, no test needed -- there is no reachable path where a buffer "larger than INT_MAX" could exist to split or reject. - AUD-15-019 [P1] Audit frame/byte/sample conversions for overflow. Acceptance: Checked arithmetic covers duration, loops, offsets, and queue totals. Investigated -- confirmed largely already covered by this session's own prior fixes, plus one new area (
DynamicSoundEffectInstance's queue totals) reviewed and confirmed safe. Offsets:WaveBank::GetSoundEffect()'sentry.dataOffset + entry.dataLengthbounds check (IN-9/AUD-11-004) and the compact-formatrawOffsetUnits[i] * alignmentmultiplication (AUD-11-003) are both already explicitly widened to 64-bit before comparison/narrowing, specifically to prevent silent 32-bit wraparound -- both have dedicated regression tests already. Duration/size utility functions (SoundEffect::GetSampleDuration/GetSampleSizeInBytes): the intermediate multiplication happens indoubleprecision (duration.getTotalSecondsProperty() * sampleRate * channels * 2), which cannot silently wrap the way integer multiplication can -- only the final narrowing tointcsis unguarded, matching real XNA's own identicalint-returning utility method contract (a resolved, intentional non-issue, not a new finding).offset+countoverflow in bothSoundEffect's raw-buffer constructor andDynamicSoundEffectInstance::SubmitBuffer(P9-VALIDATION-002/003/010/011) already compute the bounds check asoff > buffer.size() || cnt > buffer.size() - offspecifically to avoid a plainoffset+countaddition that could itself overflowint32_tbefore the comparison even runs -- already fixed, already tested. Queue totals (new area reviewed this task):DynamicSoundEffectInstance::Update()'ssubmittedChunkSizes_byte-total (AUDIO-BUFFER-001) is recomputed fresh from the current deque contents everyUpdate()call (astd::size_tsum, not a persistent monotonic accumulator), so it can never accumulate error/overflow across calls the way a running total would -- each individual chunk's size is itself already bounded to at mostINT32_MAXperAUD-15-018's finding, so even a pathologically large number of simultaneously-pending chunks would exhaust memory (queuedBuffers_/submittedChunkSizes_deque growth) long before a 64-bitsize_tsum could realistically overflow. No production code change, no test needed -- every arithmetic site this acceptance criterion names either already has an explicit overflow guard with its own regression test, or is confirmed structurally immune to overflow by the way it's computed. - AUD-15-020 [P2] Add continuous audio performance regression tracking. Acceptance: CI dashboard flags significant CPU/memory/latency changes.
- AUD-15-021 [P0] Investigate an intermittent, pre-existing segfault in
AUD04004/AudioMixerSpecOverrideTest.OverriddenSpecIsActuallyNegotiated/0when run as part of the full audio-scoped test filter. Discovered incidentally while regression-testingAUD-15-006's fix (not caused by it -- confirmed via git-stash: reproduces at the same ~1-2/5 rate on the pre-AUD-15-006baseline withAUD-15-006's changes fully stashed out). Symptoms:SDL_AUDIODRIVER=dummy ./CnaTests --gtest_filter='<the NEXTaudio.md §7 audio-scoped filter>'segfaults intermittently (~20-40% of runs observed) specifically at the start ofAudioMixerSpecOverrideTest.OverriddenSpecIsActuallyNegotiated/0, which is the ~1361st-or-later test to run in that filtered set -- the SAME test suite run in complete isolation (--gtest_filter='AUD04004/AudioMixerSpecOverrideTest.*', 5 tests) never crashed (5/5 clean, repeated). This strongly suggests cumulative process-wide state from ~1300+ prior tests (most plausibly repeatedAudioMixer::GetMixer()/DestroyMixer()cycles, or some other global/static teardown ordering issue) rather than a bug in this specific parameterized test itself. Not caught by ASan: 6/6 runs of the full audio-scoped filter under a fresh ASan+UBSan build were clean (ASAN_OPTIONS=detect_leaks=0) -- consistent with either a genuine data race (needs TSAN across the whole filtered run, not just one test, which is expensive/slow) or a non-memory-safety resource-exhaustion-style bug (e.g. a leaked SDL device/mixer handle that only manifests after enough accumulated churn) that ASan's instrumentation happens to perturb away. Acceptance: root-cause identified (bisect which earlier test(s) in the sequence leave the corrupting state, most efficiently via--gtest_filterbinary search or--gtest_shufflewith a fixed seed to narrow reproduction, or a full-filter TSAN sweep if a race is suspected); fix applied; confirmed clean across >=20 consecutive full-filter runs. Further investigation (2026-07-18, same day): several real hypotheses ruled out; root cause still not found -- documenting negative results rather than guessing further. (1) Binary-search bisection across the ~30 preceding test suites (--gtest_list_testsorder) rules out "one bad suite": neither the first ~14 suites (AudioMixerTest...AudioListenerTest) nor the second ~14 (AudioStopOptionsTest...WaveBankTest), each combined withAudioMixerSpecOverrideTestalone, reproduced the crash in 5 runs apiece (10 total, 0 crashes) -- yet the full ~656-test sequence still reproduces at its usual ~20-40% rate. This pattern (doesn't bisect cleanly, needs close to the full count) points toward a threshold/accumulation effect across most-or-all of the preceding suite, not one specific corrupting test. (2)WaveBankTestimmediately precedes the crash site in registration order but running it alone plusAudioMixerSpecOverrideTest(5x) never reproduced it either -- ruled out as the sole trigger. (3) Resource-exhaustion hypotheses ruled out by direct measurement: live-sampled/proc/<pid>/status+fdduring full runs (both loose ~200ms and tight ~50ms sampling, several attempts) -- file descriptor count stays flat (3-4 throughout, no leak), thread count stays low (1-3, consistent with properjoin()cleanup between tests, no thread leak), RSS grows smoothly and modestly (~10MB to ~55MB across the whole run, consistent with normal test allocation, not a runaway leak). (4) Not caught by TSAN either: a fresh one-off TSAN build (-DCNA_SANITIZE=thread, removed after use per convention) run 3x against the full audio-scoped filter completed all 656 tests cleanly every time (only the already-known, already-documented, explicitly-scoped-outAUD-15-006races onState_/isDisposed_were reported, from unrelated earlier tests in the same run -- nothing new, and the target crash never occurred under TSAN). Emerging picture: the crash reproduces reliably under a plain, uninstrumented, full-speed run, but is evaded by every form of instrumentation tried so far (ASan, TSAN, even lightweight/procpolling) -- the classic signature of a genuine timing-sensitive race whose window is only reliably hit at full native execution speed, where added overhead from any of these tools perturbs the exact interleaving needed. Further progress likely needs either (a)gdb(not installed in this sandbox, no passwordlesssudoavailable to install it) to get a real backtrace/core-dump analysis without behavior-altering instrumentation, or (b) a custom lightweight repro harness that narrows the specific interleaving without a general-purpose sanitizer's overhead. Left open, undiagnosed, for whoever picks this up next with either tool available. - AUD-15-022 [P0] -- CRITICAL, deterministic (not flaky), likely implicates this session's own
AUD-15-006/AUD-07-003stress tests. A separate, more severe heap-corruption crash (corrupted double-linked list/malloc_consolidate(): unaligned fastbin chunk detected/free(): invalid pointer-- the exact glibc abort message varies run to run, itself a signature of real heap corruption rather than a single well-defined bug site) was found while investigatingAUD-15-021. UnlikeAUD-15-021's ~20-40% flake rate, this one is 100% reproducible with a much smaller, faster repro:SDL_AUDIODRIVER=dummy ./CnaTests --gtest_filter='CueTest.*:DynamicSoundEffectInstanceTest.*'(149 tests, ~3.5s, crashes at process exit during "Global test environment tear-down" every single time, 6/6 runs observed). Bisection so far: (1) confirmed via git-stash this is unrelated toAUD-15-008's change (reproduces identically with that change stashed out). (2) NeitherCueTestalone norDynamicSoundEffectInstanceTestalone crashes (3/3 clean each) -- it is a genuine cross-suite interaction. (3) Excluding this session's two new stress tests (-*Stress*) fromDynamicSoundEffectInstanceTestmakes the crash disappear entirely (3/3 clean) -- strongly implicatingStressProducerConsumerWithRandomPauseStopDispose(AUD-15-006) and/orStressSubmitFloatBufferEXTAgainstRepeatedPlayCyclesNeverCorruptsLiveStream(AUD-07-003) as a necessary trigger. (4) However, neither stress test alone (each combined with fullCueTest) reproduces it (3/3 clean each), nor do both stress tests together without the other ~55 ordinaryDynamicSoundEffectInstanceTesttests (3/3 clean) -- the crash needs the fullCueTestsuite, the full non-stressDynamicSoundEffectInstanceTestsuite, AND both stress tests together, all in one process. This pattern (corruption caused early, only detected later once enough subsequent allocator activity walks over the corrupted chunk) is consistent with a real heap-buffer-overflow or double-free somewhere in the stress tests' code paths (most likelyDynamicSoundEffectInstance's newly-added/newly-lockedqueueMutex_-protected paths fromAUD-15-006, given the timing) that ASan does not catch. Not caught by ASan: the exactCueTest.*:DynamicSoundEffectInstanceTest.*repro run 3x under a fresh ASan+UBSan build was clean every time (ASAN_OPTIONS=detect_leaks=0) -- same evasion pattern asAUD-15-021, and novalgrind/gdbavailable in this sandbox to dig further (see that entry's tooling note, same blocker). A secondary, smaller, real finding surfaced along the way (unrelated to the crash itself, but worth tracking): one test run's stderr showed[DynamicSoundEffectInstance] SDL_PutAudioStreamData failed (63 bytes dropped): Can't add partial sample frames-- confirmsAUD-07-005's acceptance criterion ("partial samples/frames are rejected") is currently NOT met: SDL itself rejects the non-frame-aligned submission, but CNA only logs a diagnostic and silently drops the data rather than surfacing it to the caller;AUD-07-005/006remain open, legitimate follow-up tasks for this (a discarded WIP attempt at fixing this already exists in this session's history -- seeNEXTaudio.md's process note -- design and verify fresh, don't resurrect the discarded diff untested). Acceptance: root-cause identified viagdb/valgrind/ASan-with-different-flags (detect_stack_use_after_return,malloc_context_size, etc. -- untried) or manual code audit of every allocation/free inAUD-15-006's andAUD-07-003's new code paths; fixed; the exact repro command above confirmed clean across >=20 consecutive runs. Important scope clarification (checked immediately after finding this): the FULL whole-repo suite (./CnaTests, no filter, 4788 tests) and the properly-scoped audio filter both ran clean 3x in a row right after this was found -- this crash appears specific to the narrow, isolatedCueTest.*:DynamicSoundEffectInstanceTest.*repro (consistent with heap-corruption bugs being sensitive to overall heap layout/allocation history, similar to howAUD-15-021needed ~1300 tests' worth of accumulated state). This is a real, 100%-reproducible-in-isolation bug that must still be fixed (and may be an early/small-scale symptom of the same root cause behindAUD-15-021's larger-scale flake), but it is NOT currently observed to break the normal whole-repo or audio-scoped CI-style test run -- downgrade the urgency framing accordingly: fix it because it's a real, cleanly-reproducible lead worth chasing (and possibly the key to also closingAUD-15-021), not because the normal test suite is currently broken.
Prove parity and truthful fallback on real devices, not only SDL dummy mode.
- AUD-16-001 [P0] Create a platform/backend audio test matrix with required gates. Acceptance: Linux, Windows, macOS, Android, iOS, and Web coverage is explicit.
- AUD-16-002 [P0] Run offline golden tests on every compiler/platform. Acceptance: Core decode/mix math remains within tolerance.
- AUD-16-003 [P0] Run physical-device calibration on Linux ALSA/PipeWire/PulseAudio as applicable. Acceptance: Rate, duration, channels, and latency are recorded.
- AUD-16-004 [P0] Run physical-device calibration on Windows WASAPI. Acceptance: Results compare with original XNA capture where possible.
- AUD-16-005 [P1] Run physical-device calibration on macOS CoreAudio. Acceptance: Native 44.1/48 behavior is validated.
- AUD-16-006 [P1] Run Android output/capture calibration across common device rates. Acceptance: Mobile resampling and lifecycle pass.
- AUD-16-007 [P1] Run iOS output/capture calibration across route changes. Acceptance: Speaker/headphones/Bluetooth transitions are tested.
- AUD-16-008 [P1] Run WebAudio/browser calibration where audio is supported. Acceptance: Autoplay, resume, rate, and latency limitations are documented.
- AUD-16-009 [P1] Test headphones/speakers/Bluetooth route changes. Acceptance: No persistent wrong rate or lost mixer state.
- AUD-16-010 [P1] Test mono, stereo, 5.1, and 7.1 devices where supported. Acceptance: Channel mapping/fallback is explicit.
- AUD-16-011 [P1] Test default 44.1, 48, and 96 kHz devices. Acceptance: No pitch shift or duration drift.
- AUD-16-012 [P1] Test suspend/resume, focus loss, and backgrounding. Acceptance: Streams and timers recover consistently.
- AUD-16-013 [P1] Test no-audio-device/headless environments. Acceptance: Exception/fallback behavior supports tests and servers without false success.
- AUD-16-014 [P1] Test locale-independent numeric parsing/configuration. Acceptance: Audio ratios/settings cannot change with locale.
- AUD-16-015 [P1] Record backend capability/version in test artifacts. Acceptance: Failures are attributable to an exact environment.
- AUD-16-016 [P2] Create a manual perceptual QA script. Acceptance: Human checks complement, but never replace, numerical gates.
Treat audio files as untrusted binary input and eliminate parser/decoder denial-of-service risks.
- AUD-17-001 [P0] Fuzz WAV/RIFF loading including chunk order, padding, and sizes. Acceptance: No crash/OOB/leak/hang/pathological allocation.
- AUD-17-002 [P0] Fuzz XNB SoundEffect payloads and format extensions. Acceptance: Reader remains memory-safe and bounded.
- AUD-17-003 [P0] Fuzz XGS, XSB, and XWB independently and as a linked set. Acceptance: Cross-file indices cannot escape bounds.
- AUD-17-004 [P0] Add maximum asset size, channel count, sample rate, and duration limits. Acceptance: Limits prevent denial-of-service and are configurable/documented.
- AUD-17-005 [P0] Use checked arithmetic for RIFF and bank size computations. Acceptance: No wraparound can produce small accepted ranges or huge allocations.
- AUD-17-006 [P1] Test truncated files at every byte boundary for small fixtures. Acceptance: Failure is deterministic and leak-free.
- AUD-17-007 [P1] Test unknown/duplicate RIFF chunks and odd-byte padding. Acceptance: Parser remains synchronized.
- AUD-17-008 [P1] Test NaN/Inf float samples and parameter values. Acceptance: Mixer output remains finite or input is rejected.
- AUD-17-009 [P1] Test malicious loop points and event counts. Acceptance: No infinite CPU loop or OOB region.
- AUD-17-010 [P1] Test decompression bombs and extreme metadata ratios. Acceptance: Resource use is bounded before decode.
- AUD-17-011 [P1] Add fuzz corpus minimization and regression promotion. Acceptance: Every found crash becomes a small committed test.
- AUD-17-012 [P1] Run fuzzers with ASan/UBSan in scheduled CI. Acceptance: Coverage and finding status are tracked.
- AUD-17-013 [P2] Audit external decoder CVE/update policy. Acceptance: Pinned versions have an explicit security maintenance process.
Convert the work into a maintainable, auditable definition of “perfect audio.”
- AUD-18-001 [P0] Generate a public API signature diff against XNA 4.0 Audio/Media assemblies. Acceptance: Missing/extra/type/default/constness differences are reviewed.
- AUD-18-002 [P0] Generate behavior tests for every public Audio property/method/exception. Acceptance: Coverage matrix contains no unreviewed member.
- AUD-18-003 [P0] Define a “CNA Audio Correctness” release gate. Acceptance: High-pitch, missing-asset, rendered-golden, sanitizer, and platform gates are mandatory.
- AUD-18-004 [P0] Require zero unresolved silent-failure paths in core playback. Acceptance: Static audit and tests prove every backend failure is handled.
- AUD-18-005 [P0] Require zero unexplained unsupported formats in shipped asset manifests. Acceptance: Build cannot ship assets that runtime silently cannot play.
- AUD-18-006 [P0] Publish the final XNA/CNA differential report for the reported game. Acceptance: Root cause, fix, recordings, and numerical evidence are included.
- AUD-18-007 [P1] Document exact raw PCM constructor requirements with examples. Acceptance: C++ porters cannot confuse container bytes and PCM frames.
- AUD-18-008 [P1] Document XNB/XACT supported format matrix and conversion guidance. Acceptance: Users know what content pipeline products are valid.
- AUD-18-009 [P1] Document pitch composition and 3D velocity units. Acceptance: Examples show neutral pitch and correct per-second velocity calculation.
- AUD-18-010 [P1] Document audio troubleshooting using trace and asset inspector. Acceptance: Guide maps common symptoms to concrete measurements.
- AUD-18-011 [P1] Document backend/device limitations and intentional divergences. Acceptance: No hidden approximation is marketed as exact parity.
- AUD-18-012 [P1] Add runnable samples for static, dynamic, XACT, media, 3D, and microphone paths. Acceptance: Each sample doubles as a manual/CI smoke target.
- AUD-18-013 [P1] Add a calibration sample showing detected rate/frequency. Acceptance: Users can validate a machine without the affected game.
- AUD-18-014 [P1] Add migration guidance from XNA C# audio loading to CNA C++. Acceptance: Guide highlights metadata, case, lifetime, and content-pipeline traps.
- AUD-18-015 [P1] Create changelog entries for behavior-affecting audio fixes. Acceptance: Applications can identify changes in pitch/format/voice semantics.
- AUD-18-016 [P1] Establish semantic versioning policy for audio behavior and extensions. Acceptance: Breaking backend/parity changes are not silent.
- AUD-18-017 [P1] Review licenses/patents for optional codecs. Acceptance: Codec support decisions are legally and technically documented.
- AUD-18-018 [P1] Package exact dependency notices and source obligations. Acceptance: Distributions are reproducible and compliant.
- AUD-18-019 [P2] Complete or explicitly scope every remaining MediaLibrary stub. Acceptance: No generic
not implementedsurvives without compatibility rationale. - AUD-18-020 [P2] Perform a final independent audit after all P0/P1 tasks. Acceptance: Fresh reviewer reproduces gates without relying on implementation author assumptions.
- The reported C# XNA and C++ CNA game build have matched, archived captures for every formerly affected sound.
- No affected sound is high-pitched, sped up, distorted, missing, or silently skipped.
- Static, dynamic, XACT, and media calibration tests pass at 22.05/44.1/48 kHz source rates and 44.1/48 kHz device rates.
- The supported XNB/XWB/XACT format matrix is implemented and validated by real fixtures.
- Every core SDL/MIX operation has checked failure handling and truthful public state.
- Pitch, XACT cents/RPC/random variation, and Doppler composition are traceable and differential-tested.
- Audio parsers and lifecycle pass sanitizer/fuzz/stress gates.
- Required physical-device tests pass on supported desktop/mobile/web targets.
- Remaining divergences from XNA are intentional, documented, tested, and accepted.
- A fresh independent audit finds no unresolved P0/P1 item or unexplained audible divergence.