From 8e6697312f218175ac5fb5d9e682c92599124e93 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Thu, 13 Aug 2026 16:39:18 -0500 Subject: [PATCH] soundboard game-rate fidelity + distributable: rate formula fileRate x 2^((note-60)/12) [T1 L4AUDIO BTNotePitchFactor via AL_PITCH; notes come ONLY from AudioControlSequence]; BTL4.RES audio-stream record format cracked -- all 324 sequences parsed, 92 (sample,note) pairs play OFF file rate, headline: Warnings01/Yip voice zones trigger at notes 16-40 = x0.079-x0.315 (raw files play the voice 3.2-12.7x TOO FAST -- the long-remembered mismatch, now quantified); dead zones found (Death01 z8-11, AuxExplosion01 z6-7 unreachable by any authored note). tools/soundboard.py plays game rates (header-rewrite temp copies, per-note buttons, raw toggle); NEW tools/mksoundboard.py -> dist/SOUNDBOARD_BT411.zip (31.8MB): self-contained index.html, 603 cards, playbackRate+preservesPitch=false, per-note buttons, COPY reference strings for tester bug reports, no fetch (file:// safe), tester README. AUDIO_FIDELITY.md gains the authored trigger-note census --- docs/AUDIO_FIDELITY.md | 54 +++++++ tools/mksoundboard.py | 327 +++++++++++++++++++++++++++++++++++++++++ tools/soundboard.py | 237 +++++++++++++++++++++++++---- 3 files changed, 586 insertions(+), 32 deletions(-) create mode 100644 tools/mksoundboard.py diff --git a/docs/AUDIO_FIDELITY.md b/docs/AUDIO_FIDELITY.md index 871a73d..56be4c3 100644 --- a/docs/AUDIO_FIDELITY.md +++ b/docs/AUDIO_FIDELITY.md @@ -617,6 +617,60 @@ reduce source-pool starvation. --- + +### The complete authored trigger-note census (2026-08-13, [T1]) — and the game-rate soundboard + +**The playback-rate model, end to end:** effective output rate = WAV declared rate x +2^((note-60)/12) (`L4AUDIO.cpp:21-24` BTNotePitchFactor -> `alSourcef(AL_PITCH)` at :971 +per-SetupPatch; per-zone SoundFont tuning is baked into each extracted WAV's declared rate per +F2, so note 60 == authored pitch). Sources default to note 60 (`AUDSRC.cpp:18 DEFAULT_NOTE`); +the ONLY senders of NoteAudioControlID are AudioControlSequences (grep-verified engine-wide), +so **every off-file-rate playback in the game comes from a sequence note**. Dynamic pitch-cents +(doppler, pitch-scale watchers) modulate on top and are not part of the base rate. + +**BTL4.RES AudioControlSequence record format** (matches the ctor read order, +`AUDSEQ.cpp:165-194`): `[classID=75 i4][objectID i4][dump i4][looped i4][targetObjID i4] +[divisionsPerBeat i4][tempo i4][eventCount i4][count x (tick i4, ctlID i4, value f4)]`; +ctl 8 = note, 1 = start, 2 = stop, 6 = attack-volume. Every object record in the audio +streams is `[classID i4][objectID i4][payload]` with ClassID = the `VDATA.h` enum +(AudioStateTrigger=28, sequence=75, splitter=76, DirectPatchSource=1001, PatchResource=1004, +PatchLevelOfDetail=1011 — LOD payload `[voiceCount i4][renderType i4][suspendSecs f4] +[bank u1][patch u1][maxFilter i4]`). Patch resources/LODs live in the shared +`StaticAudioStream` (id 0); vehicle `*int.scp` streams reference them cross-stream. +Sequence -> target -> source -> resource -> LOD resolution yields the patch for every one of +the **324 sequences (15 unique patterns)** in the RES; cross-validated against live SetupPatch +logs (mp4l/solo/genedge). + +| notes (authored order) | looped | patch | trigger (watcher census) | +|---|---|---|---| +| 16,19,23 | yes | 2:113 Warnings01 | coolant-leak voice loop (ReportLeak, 19 subsystems) | +| 16,33,36 | no | 2:113 | ammo cook-off countdown voice (FireCountdownStarted) | +| 29,16,26 | no | 2:113 | generator-out voice phrase (GeneratorState/GeneratorOn) | +| 29,16,40 | no | 2:113 | Entity.SimulationState warning voice phrase | +| 30,18 / 49,43 / 95 | no | 2:115 Death01 | death phrases — a **random 3-pick list** `[3][seqA][seqB][seqC]` at death | +| 36,84 | no | 1:36/40/41/56/70/71/75 | weapon loaded/jam/misfire clunk-then-blip (WeaponState) | +| 36,70 | no | 2:64 DestroyedInt06 | subsystem destroyed (SimulationState) | +| 58 x6 | yes | 1:74 IncomingAlarm01 | missile-lock beeper (IncomingLock; DistanceToMissile -> tempo) | +| 51,51 | yes | 1:83 ProgramButton01 | weapon-configure ticker (ConfigureActivePress) | +| 30,48,46,96,44,63,66 | no | 2:83 MechExplosion01 | brnext.scp | +| 45,38 | no | 2:97 MechFireLoop01 | brnext.scp | +| 57,69,46,24 / 69,51 | no | 2:119+120 AuxExplosion01(+LOD2) | bigexp.scp / medexp+mbnexp.scp | + +Net: **92 (sample-zone, note) pairs play rate-shifted vs their WAV header** (worst: the +Warnings01 voice zones at x0.079-x0.315 — the raw files render Yip's voice 3.2x-12.7x too +fast, which is why the old soundboard's voice sounded wrong; weapon ready blips x4; death +phrase C zones x7.55). Bonus census finding: **Death01 z8-11, AuxExplosion01 z6-7 and +AuxExplosion01LOD2 z3 are reachable by NO authored note** — dead zones. + +**Tools:** `tools/soundboard.py` now plays every sample at its game rate (header-rewritten +temp copies, per-note buttons, raw-rate toggle); `tools/mksoundboard.py` builds +`dist/SOUNDBOARD_BT411.zip` — a self-contained tester soundboard page (file://-safe `