TASK 25: master limiter on the live mix (2026-08-15, queued from the TRAX discussion) — the review confirmed two of the creator's three instincts were already satisfied (MediaFoundationReader streams from disk so file size needs no guard; the MusicPlayer 0.20 bed rides the same loopback gain as the game, so music is always exactly 20% of the desktop volume by construction) and found one real gap: AudioMixer.FillAndMix summed mic + loopback with no output ceiling, so hot gains could pass 0 dBFS and clip the AAC encode. New pure Services/Audio/MasterLimiter.cs: -1 dBFS ceiling (Ceiling = 0.891), instant attack per frame (a hot frame is scaled exactly to the ceiling — no overshoot), smoothed release toward unity so loud passages don't pump, gain never exceeds 1. Applied at the end of FillAndMix right before the pipe write. 3 unit tests (trim-to-ceiling, never-boost, recovery) + ONE integration test (MasterLimiter_CapsTheLiveMix_OnThePipe: a 0.95 loopback bed capped to exactly 0.891 on the wire while staying audible). Docs in the same commit: ai.md go-live audio section, Services/index.md new row, TASKS.md TASK 25 record, HANDOFF.md shipped state. Build 0 warnings, 193 tests passing (189 + 4 new)

This commit is contained in:
2026-08-15 16:10:16 -07:00
parent 24f16b2cd9
commit 92f1471ab4
7 changed files with 201 additions and 9 deletions
+30
View File
@@ -761,6 +761,36 @@ The creator reviewed the TASK 9 build and filed 8 issues. All fixed in one branc
---
## TASK 25 — Master limiter on the live mix (2026-08-15)
**Queued by the creator during the TRAX discussion:** "should the soundtrack be limited to avoid
squandering resources / should it cap at 20% / how do the three sound events balance?" Review
conclusions (all three instincts checked out, only ONE real gap):
- **File size — no guard needed.** `MusicPlayer` uses `MediaFoundationReader`, which **streams from
disk** (progressive source): memory is flat (~a few MB) regardless of file size; CPU negligible.
- **20% cap — already enforced by construction.** `MusicPlayer.MusicVolume = 0.20f` + music rides the
SAME WASAPI loopback (and therefore the same gain) as game audio, so music:game is always exactly
**0.20:1 at any slider position** — it literally cannot rise above 20% of the current desktop volume.
- **The gap:** `AudioMixer.FillAndMix` summed mic + loopback with **no output ceiling** — mic 100% +
loud game/music could pass 0 dBFS and clip the AAC encode.
**Shipped:**
1. ✅ **`Services/Audio/MasterLimiter.cs`** (pure, unit-tested) — a **−1 dBFS ceiling** (`Ceiling =
0.891`), **instant attack per frame** (a hot frame is scaled exactly to the ceiling — no overshoot),
**smoothed release** toward unity so loud passages don't pump; gain never exceeds 1 (no boosting).
Applied at the end of `AudioMixer.FillAndMix`, right before the pipe write.
2. ✅ **Unit tests** — over-ceiling frames trimmed to ≤ ceiling; sub-ceiling frames never boosted; gain
recovers to unity after the loud frame ends.
3. ✅ **ONE integration test** (`MasterLimiter_CapsTheLiveMix_OnThePipe`) — real mixer + pipe harness:
a 0.95 loopback bed (hotter than the ceiling) is capped to exactly 0.891 on the wire while staying
audible.
4. ✅ **Docs in the same commit** — `ai.md` (go-live audio section), `Services/index.md` (new row).
No changes to `MusicPlayer`, the 0.20 cap, the ducker, or the meter zones. Build 0 warnings.
---
## Backlog (future versions)
1. v0.2 — Recording to local file (recordings carry the branding flash — see TASK 3 / `ai.md` Monetization)