Files
BT411/docs/AUDIO_FIDELITY.md
T
arcattackandClaude Opus 5 d39227ef39 Gitea #51 ROOT CAUSE: LoopAtWill was mapped to "loop forever", arming 224 of 603 samples as endless OpenAL sources
SAURON's "coolant flush sound glitched and perma" is one instance of a systemic
defect.  SetupPatch (L4AUDLVL.cpp) decided looping from the SAMPLE flag alone:

    AL_LOOPING = (info.loop != ForceStatic)

That armed every non-ForceStatic sample -- 224 of the 603 authored zones,
including unmistakable one-shots -- as an OpenAL source that never ends on its
own.  Any such sound whose note-off never arrives plays forever.  Measured 1946
LoopAtWill x Transient arming events in one short session.

The sample flag expresses PERMISSION; the SOURCE decides.  Three facts [T1]:

  * the enum's own comments (L4AUDLVL.h:14-19) -- LoopAtWill = "will play once
    OR LOOP AS DESIRED", ForceStatic = "plays only once EVEN IF LOOPED" (a
    veto), LoopAlways = "ramp up and then down";
  * LoopAtWill is enum value 0, i.e. the DEFAULT an unauthored sample receives
    (WTPresets.cpp:37) -- it cannot mean "loop forever";
  * LoopAlways, the real always-loop, is used by ZERO shipped samples.

"As desired" is the source's authored AudioRenderType (Transient=one-shot vs
Sustained=held), streamed from AUDIO*.RES (AUDLVL.cpp:28) and already consulted
by the renderer (L4AUDRND.cpp:567, AUDREND.cpp:184).  SetupPatch now takes it,
threaded from all three L4AudioSource call sites (Direct/Dynamic3D/Static3D).
New rule: LoopAlways->1, ForceStatic->0, LoopAtWill->the source's render type.

MEASURED before changing behaviour (BT_LOOP_AUDIT=1, scratchpad/loopaudit.py):

    LoopAtWill  x Transient -> loop=0  (was 1)   1946 events
    LoopAtWill  x Sustained -> loop=1  unchanged    65 events
    ForceStatic x Transient -> loop=0  unchanged   292 events

The engine loops (EngineAccel07_z0..z2, EngineMotor01, EnginePower01) are all
LoopAtWill/Sustained and PRESERVED -- no regression there.  All 24 reclassified
samples are genuine one-shots: laser charge/fire/explosion/loaded, missile
loading, engine shift, coolant pressure inc/dec.

A/B PROOF (scratchpad/loopab.py, same session both arms, only BT_LOOP_LEGACY
differs), asking the engine's own BT_AUDIO_DUMP what is still playing with
loop=1 long after every release:

    [legacy] EnginePower01, CoolantPresInc03_z2, CoolantPresDcr03_z2
    [fixed]  EnginePower01

The two stuck coolant sources are the reported symptom.  They were eventually
reclaimed when a later trigger reused the source, which is why the bug was
intermittent -- the "perma" case is when nothing else retriggers it.  The fix
removes the mechanism.

BT_LOOP_LEGACY=1 restores the old rule for a field A/B without a rebuild.  The
layers to listen to are the beam sustains: LaserA/CSustain* are authored
LoopAtWill on a TRANSIENT source, so they now end with the sample instead of
looping until note-off.  The render type is T1 authored data; the interpretation
that LoopAtWill defers to it is a strong reading of the enum + defaults rather
than a disassembled statement, and is flagged as such in the KB.

Plausibly bears on #32 (audio cutting in/out late in a match): a stuck looping
source holds its pool slot for the rest of the round.

Rigs: scratchpad/flushsnd.py (flush start/stop pairing), flushsnd2.py (stuck-
source hunt), loopaudit.py (the classification matrix), loopab.py (the A/B).
KB: context/wintesla-port.md audio section, context/decomp-reference.md env
table (BT_LOOP_AUDIT / BT_LOOP_LEGACY / BT_AUDIO_DUMP / BT_AUD_TAIL),
docs/AUDIO_FIDELITY.md F38.  checkctx.py CLEAN.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 13:20:19 -05:00

686 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Audio Fidelity Audit — btl4.exe OpenAL Port vs. Original AWE32/HMI-SOS
> **STATUS (2026-07-16): Phases 1-4 SHIPPED** (commits fc7f311, 691e885, 0ca7d26,
> cc2b109..0e2401f). Fixed: F1-F5, F7, F9-F13, F15, F16, F18-F20, F23, KB items;
> F6 fixed (ReportLeak -> coolantActive@0x138, binary table [T1]); F14 FIXED
> (attenuation + the static resonant fc/Q low-pass both baked, c581553);
> F17 FIXED (CollisionSpeed + ReduceButton real; UnstablePercentage live --
> the instability model @0x3F0 is byte-decoded and reconstructed, task #66);
> F8/F21/F22 open (voice budget unhit / attack polish / HRTF option).
> Post-audit: the footstep cold-start warm-up bug (idle watcher chains +
> fill-0 smoothers vs the transient drop gate) fixed in a11a697.
> attrnull = 0: every authored audio attribute binds a real member.
**Date:** 2026-07-15
**Scope:** Full fidelity audit of the OpenAL audio reconstruction against the original 1995
semantics: the authored audio object streams (`content/audit_capture.log` traces), the WinTesla
engine source (`engine/MUNGA`, `engine/MUNGA_L4`), the Ghidra decomp of `BTL4OPT.EXE`
(`reference/decomp/all/part_*.c`), and the SF2 v1.0 soundbanks (`content/AUDIO/AUDIO1.RES`,
`AUDIO2.RES`).
**Method:** Parallel findings independently generated, then adversarially verified (every count
re-derived from the banks with fresh parsers, every file:line re-read, decomp cross-checked, live
capture re-parsed). Verdicts: CONFIRMED = independently reproduced; PARTIAL = core claim holds
with material corrections (the corrected form is what appears below); UNVERIFIED-LOW = low-impact,
not independently re-verified.
**Excluded (already known-fixed):** note-pitch application (`BTNotePitchFactor`), calibration rate
1000, ExecuteWatchers poll restoration, configure-ticker -1 pad, published-velocity smoothing.
---
## 1. Executive Summary
Honestly: the **control architecture is in good shape, the rendering back-end is not.** The
watcher/sequence/mixer graph is substantially reconstructed and live — all 118 authored state
watchers bind real StateIndicators, weapons/footfalls/generators demonstrably fire, and the
patch-numbering and calibration questions are settled in the port's favor. But roughly a quarter
of attribute-gated watchers sit on dead attributes (the coolant-leak warning, the accelerating
missile-lock beeper, the torso-twist servo, the instability alarm can **never** sound), and on the
synthesis side nearly every axis diverges from what the AWE32/EMU8000 actually did: the SF2
extractor keeps one zone per preset (discarding key-splits, layers, and stereo pairs the engine
already has plumbing for), drops all tuning metadata (~83% of presets play >= 1 semitone off,
worst -36 st), the volume law is linear where the original was the squared GM CC7 curve, the
authored distance/doppler/reverb/filter/pan models are all computed and then discarded in favor of
wrong or absent OpenAL defaults.
**Rough fidelity per dimension:**
| Dimension | Est. fidelity | Basis |
|---|---|---|
| Triggers / databinding | **~75%** | 54 of ~225 attribute-gated watchers dead (24%); all 118 state watchers live; footstep timing semantics wrong; one port invention actively fights an authored feed |
| Volume / attenuation | **~40%** | Per-source gain curve wrong everywhere (linear vs squared), distance curve wrong shape for all 3D sounds, per-zone layer gains dropped, cull/steal/ducking side-effects |
| Looping | **~85%** | Authored loop regions ~= whole sample for everything looped today (worst artifact ~5% period error / wrap click); missing 1.1-3.9 s release fades on ~20 looped presets; one **latent** catastrophic loop once multi-zone lands |
| Pitch / timbre | **~25%** | ~83% of presets >= 1 semitone off (root-key/tuning dropped); zero filtering (brightness, distance muffling); doppler wrong sign and depth |
---
## 2. Ranked Findings (audible impact x confidence)
| # | Finding | Impact | Verdict | One-line effect |
|---|---|---|---|---|
| 1 | [Multi-zone preset collapse](#f1) | High | CONFIRMED | Key-splits/layers/stereo dropped: wrong klaxons, looping charge-hum instead of clunk-then-blip, explosions lose body and rumble |
| 2 | [Root-key & tuning dropped](#f2) | High | CONFIRMED | 91/115 + ~102/126 presets play >= 1 semitone off (worst -36 st; klaxons ~6x slow, footsteps 27% fast) |
| 3 | [Distance attenuation wrong](#f3) | High | CONFIRMED | Authored knee/rolloff curve dead; AL linear ramp makes mid/far battle audio far too quiet; cull/steal/ducking distance-blind |
| 4 | [Volume curve linear vs CC7 squared](#f4) | High | CONFIRMED | Every intermediate volume ~+6 dB too loud at mid-scale; authored dynamic range compressed |
| 5 | [FootStep trigger semantics](#f5) | High | CONFIRMED | Binary: per-frame root-height contact level vs threshold; port: fixed 150 ms pulse per clip transition — wrong step timing/counts |
| 6 | [ReportLeak dead — coolant warning](#f6) | High | CONFIRMED | Largest dead block (38/54 match watchers, 19 subsystems); leak klaxon can never sound |
| 7 | [Missile-lock alarm dead](#f7) | High | CONFIRMED | IncomingLock + DistanceToMissile on read-only pad; accelerating lock beeper can never start |
| 8 | [Dead-watcher coverage matrix](#f8) | High | PARTIAL | 54 of ~225 attribute-gated watchers on dead attributes (~24%); itemized worklist |
| 9 | [Brightness / low-pass chain dead](#f9) | Medium | CONFIRMED | AWE NRPN-21 filter path computed then discarded; no distance muffling, ctl-5 brightness inert |
| 10 | [Doppler triply wrong](#f10) | Medium | CONFIRMED | Authored cents model dead; AL doppler at wrong constants with sign-inverted velocity — approaching sources pitch DOWN |
| 11 | [Reverb dead](#f11) | Medium | CONFIRMED | Original: CC91=38 wet exterior / 0 dry cockpit (decomp-proven); port: bone-dry everywhere |
| 12 | [Front/rear + pan placement dead](#f12) | Medium | CONFIRMED | DirectPatch position enum and internal spatialization discarded; all cockpit/static sounds dead-center |
| 13 | [Loop regions & release envelopes](#f13) | Medium | CONFIRMED/PARTIAL | Today: minor wrap artifacts (worst ~2.4 Hz tick in beam hum) + instant cuts where 1.1-3.9 s releases were authored; latent boom-loop bug once multi-zone lands |
| 14 | [Zone gain/filter/envelope generators dropped](#f14) | Medium | PARTIAL | Per-zone attenuation (inverted SBK scale), authored resonant low-pass on charge hum, volume envelopes all lost |
| 15 | [ConfigureActivePress mispublished](#f15) | Medium | CONFIRMED | Weapon-configure ticker can never fire; field exists but is misnamed and unpublished |
| 16 | [Torso-twist servo whir dead](#f16) | Medium | CONFIRMED | Torso publishes no attributes; whir never starts, pitch tracking dead |
| 17 | [Entity attrPad family](#f17) | Medium | CONFIRMED | Instability alarm never sounds; impact sounds never scale with speed; ReduceButton feedback dead |
| 18 | [FireCountdownStarted = cook-off warning](#f18) | Medium | PARTIAL | Dead binding confirmed; it is the ammo COOK-OFF countdown (member exists: `cookOffArmed`), not a salvo cue |
| 19 | [Step-intensity invention fights authored feed](#f19) | Medium | PARTIAL | Unauthored curve + direct mixer writes clobbered by the live authored LocalVelocity feed every step (drops footsteps) |
| 20 | [Zoom blip unpublished](#f20) | Low | UNVERIFIED-LOW | Live member exists, one ATTRIBUTE_ENTRY missing |
| 21 | [Attack velocity / attack time dead](#f21) | Low | UNVERIFIED-LOW | Mechanism dead but authored data barely uses it today |
| 22 | [ITD / between-ears spatialization dead](#f22) | Low | UNVERIFIED-LOW | 4-channel pod model computed and discarded; matters mostly for pod-hardware target |
| 23 | [Benign inventions check](#f23) | Low | UNVERIFIED-LOW | Footstep pulse OK; AnimationState numbering unverified (17 authored triggers never fired); CollisionState 0/1 vs 4-state |
**No-gap confirmations (validated, no change needed):** patch numbers are SoundFont wPreset
values (the port's mapping is correct — never "fix" toward file index); the calibration rate 1000
is structurally equivalent to the original clock-tick rate (but the `DPLARG+0x10` read is baseless
and the KB claim should be corrected); the port's published-velocity smoothing matches the
binary's `AverageOf` filter chain in kind. See [Section 4](#validations).
---
## 3. Findings in Detail
### A. Sample bank / synthesis fidelity
<a name="f1"></a>
#### F1. Multi-zone preset collapse — key-splits, layers, stereo pairs all dropped (High, CONFIRMED)
The SF2 extractor (`tools/sf2extract.py:49-62`) keeps only the FIRST sample-bearing instrument
zone per preset, so the generated table (`game/reconstructed/audiopresets.cpp`) has
`sampleNum=1`/`samples[0]` everywhere — discarding authored multi-zone content that the engine
already supports (`PRESETINFO.samples[5]`, `GetVoiceCount=PRESET_getNumSamples`, SetupPatch
buffer-per-source; `engine/MUNGA_L4/L4AUDLVL.h:33-34,68-70`, `L4AUDLVL.cpp:30,130-167`).
Verified against the banks: **68/115 bank1 and 94/126 bank2 presets are multi-zone.**
- **Key-splits (7+7 presets)** — the authored MIDI note SELECTS the sample. `Warnings01` (bank2
p113) is an 8-way split (keyRanges 16-17/19-21/23-24/26-28/29-31/33-35/36-38/40-41, 8 distinct
klaxon samples, three looped); the weapon Loaded/Jam/Misfire family (bank1 p36,40,41,56,70,71,75)
splits at key 59|60: low clunk/charge zone (root 36) vs high ready-blip zone (root 84). The
captured streams author exactly these selector notes: five sequences send [36,36,84,84]
(`audit_capture.log:501,566,681-683`, plus one [36,36,70,70]); the warnings sequence (log:343)
sends 16,16,19,19,23... matching `Warnings01`'s split points exactly.
- **Layers (~55+84 presets)** — 2-4 simultaneous samples (LaserAFire01=3, MissileLaunch01=4,
AutoCanonFire01=3, MechExplosion01=16 zones in 4 key bands). Roughly two dozen bank1 /
three-dozen-plus bank2 presets mix looped sustain layers under one-shot attacks — every
LaserExplosion/BigExplosion/PPCExplosion has a looped tail-rumble layer the port drops.
- **Stereo pairs (6+3)** — pan 0/127 zones, some with different L/R samples (EngineAccel02
sid2/sid38), plus 5 bank1 L/C/R 3-zone presets.
**Net effect verified live:** on weapon-ready the port loops the charge hum at the wrong pitch
instead of clunk-then-blip (log:832-840: note-36 START then `SetupPatch ENTRY bank=1 patch=56
file=LaserLoaded01.wav`, table loop=LoopAtWill); all 8 cockpit warning klaxons play the same
sample; explosions/lasers lose their layered body and sustained rumble.
**Fix:** regenerate the table with ALL zones — per-zone WAV, keyLo/keyHi, loop mode, pan — into
`samples[0..n]` (cap 4 per the AWE comment for layers sharing a keyRange). In SetupPatch/PlayNote,
attach only zones whose keyRange contains `GetCurrentNoteValue()`, attach all matching layers, set
position/pan for 0/127 pairs. Table + SetupPatch work only; voice-count plumbing already exists.
<a name="f2"></a>
#### F2. Root-key and tuning metadata dropped — most of the bank plays at the wrong pitch (High, CONFIRMED)
The port plays every sample as if its root were MIDI 60 at 22050 Hz (`tools/sf2extract.py:24`
hardcodes SAMPLE_RATE=22050; the generator scan at lines 50-62 reads only opers 41/53/54;
`L4AUDIO.cpp:21-24` BTNotePitchFactor=2^((note-60)/12)). But the SF v1.0 banks carry full SBK
pitch metadata the extractor drops:
- **Generator 55** (SBK-only samplePitch, per awesfx `sfitem.c`/`parsesf.c`: root=amount/100 plus
fractional cents) — present on **all 603 sample zones**, constant per sample.
- **Generator 58** (overridingRootKey) on 183+247 zones; **coarseTune(51)** on 138+258 zones
(modal value -12); **fineTune(52)** on 30+9.
- EMU8000 v1 base rate is 44100 Hz (awesfx `sffile.c`: v1 samplerate hardcoded 44100), so a zone's
true data rate is 44100*2^(tune/1200). Recovered rates land exactly on 22050/44100/11025/5512 Hz
plus integer-semitone offsets — corroborating the model. There is **no rate generator**: 22050
is only the modal effective rate, not a bank-wide truth.
Per-zone port error is **constant across notes**: err_cents = (effRoot-72)*100 - tune; zero only
for the common root-60/coarse -12 configurations (why the note-pitch fix sounded right on those).
**Prevalence: 91/115 bank1 and 102/126 bank2 presets >= 1 semitone off.** Worst verified cases:
Death01/MechExplosion01 -36 st; Warnings01 -31 st (klaxons ~5.9x too slow at authored note 16);
ProjectileLoaded01 -30 st; TorsoTwistExt01/CoolantDumpExt01/LaserCSustain01-03 worst-layer +24 st;
LaserACharge01-04 +17 st; MissileLoaded01 -24 st; FootFallInt01 +4.2 st (footsteps ~27% fast).
Authored sequence notes equal zone roots exactly (36/84 splits; Warnings roots 16-40) — final
cross-check.
**Fix (algebraically exact, no engine change):** bake per-zone tuning into each extracted WAV's
declared rate = round(44100*2^(((60-effRoot)*100+tune)/1200)); OpenAL resamples from the declared
rate, so the existing note-60 pitch model reproduces original playback at every note. Note the
extreme recovered rates (~162 Hz to 176.4 kHz on a few Death01/torso zones) and the dependency on
per-zone WAVs (F1) for key-split/layered presets.
<a name="f13"></a>
#### F13. Loop regions and release envelopes (Medium, CONFIRMED with an important correction)
Two verified sub-findings, one of which corrects the naive reading:
1. **Whole-buffer looping is close to faithful today.** SetupPatch sets AL_LOOPING on the entire
buffer (`L4AUDLVL.cpp:158-164`) while authored loops are [dwStartLoop,dwEndLoop] sub-regions
that `sf2extract.py` reads but discards (line 64 vs 70). However, every looping sample in the
banks is sampleModes=1 (loop continuously; none are mode-3) with loop regions covering
**95-100%** of the sample — the whole sample effectively IS the loop. No attack transient
replays per cycle. Worst real artifacts among currently-looped zone0s: LaserBSustain family at
95.0% (348-sample lead-in + 106 tail per ~0.41 s => ~2.4 Hz discontinuity in the beam hum),
LaserCSustainExt01-04 (283+41/11657), EngineMotor02/EngineAccel02/06 (324+271/35747),
MechFireLoop01/SmallFireLoop01 (406+94/47586) — plus a wrap-point click (port wraps end->start
instead of loopEnd->loopStart).
2. **The genuine gap is Stop.** `StopNote` (`L4AUDLVL.cpp:216-217`) is
alSourceStopv+alSourceRewindv — an instant cut — but **~20 looping presets carry an authored
releaseVolEnv of 1.1-3.9 s** (EnginePower/Motor/Accel/Damage ~1.55 s, EngineAccel01 3.9 s,
TorsoTwist 1.2-1.4 s, LaserCharge 1.3 s, MissileTravel/Loading ~1.3 s, Coolant/Wind/Fire loops
~1.1 s) that the AWE32 applied as a fade on note-off while the loop continued. One-shots have
no authored release; instant stop is faithful there.
3. **Latent hazard:** the only catastrophic (<60%) loop regions sit in currently-dropped layers:
MechExplosion01/AuxExplosion01 share sample sid39 with loop [23517,23865] of 23875 (**1.5%** —
a ~16 ms sustain slice at the end of the boom). Today these presets are one-shot zone0
(ForceStatic, `audiopresets.cpp:805-808,949-952`); once multi-zone extraction (F1) lands,
whole-buffer looping would replay the full ~1.08 s boom every cycle. **Loop-region support must
ship with or before F1.**
**Fix:** extract loopStart/loopEnd per zone into the table; use AL_SOFT_loop_points (a **buffer**
property: `alBufferiv(AL_LOOP_POINTS_SOFT,...)` before attach) or split attack+loop buffers via
queueing. On Stop, ramp AL_GAIN over the per-preset authored release time before stopping.
<a name="f14"></a>
#### F14. Per-zone gain / filter / envelope generators dropped (Medium, PARTIAL)
`tools/sf2extract.py` (reads only gens 41/53/54) and the playback path drop everything the EMU8000
applied in hardware:
- **initialAttenuation** on 198 bank1 / 309 bank2 zones. SBK units are **INVERTED vs SF2** — 127 =
full volume, lower = quieter (observed range 90-127); used for layer balance (LaserBFire01's
panned side layers at 100 vs center at 120 — note: the sides are the *quieter* layers).
- **initialFilterFc** on 116 zones/bank, **initialFilterQ** on 56/23. Example: LaserLoaded01's
looped charge-hum zone — the very zone the extractor picks — is fc=57 Q=87, an authored resonant
low-pass the port plays unfiltered and therefore much brighter/harsher. No filter is ever
applied (`L4AUDIO.cpp:1067-1086` computes a cutoff, sets nothing).
- **Volume envelopes** (attackVolEnv 167/211 zones, releaseVolEnv 34/6, decayVolEnv 41/127) — see
F13 for the release consequence. SAMPLEINFO (`L4AUDLVL.h:22-29`) has no fields for any of this.
**Fix:** add per-zone gain scale (mind the inverted scale: gain ~ f(127-atten); verify the exact
dB mapping against awesfx before trusting "0..-9.6 dB" [T3]), bake or EFX-apply the static
low-pass, and map release to a Stop-path gain ramp.
<a name="f21"></a>
#### F21. Note velocity / attack time dead (Low, UNVERIFIED-LOW)
The original started every note with MIDI velocity = attackVolumeScale*127 and (when flagged) sent
AWE NRPN 11 attack time (decomp @004638a8, part_008.c:7144-7154; @00463ddc :7367-7392). The port
computes `midi_velocity`/`midi_attack_time` and discards both (`L4AUDIO.cpp:980-991`; PlayNote
takes no velocity); ctl 6/7 handlers store scales nothing reads. Mitigation: all 41 captured
AttackVolume events are value 1 (= default) and zero ctl=7 events, so today's audible loss is
small. Fix when convenient: multiply start-time AL_GAIN by attackVolumeScale (through the F4
squared curve) and implement attack time as a gain ramp (range 0..5.94 s per
`L4AUDHDW.h:63,67`).
### B. Mixing and spatial model
<a name="f3"></a>
#### F3. Distance attenuation: authored curve dead, wrong AL model live (High, CONFIRMED)
AUDIO.INI authors amplitude_rolloff=2.0 (exponent), knee=60, distance_scale=0.003,
clipping_radius=450 (AUDIOMR: 0.002): flat volume inside 60 units, then 1/(1+(0.003*(d-60))^2) —
still ~42% at the clip edge. The engine computes this into `distanceVolumeScale` on every source
execute (`AUDLOC.cpp:358-377` via `AUDSRC.cpp:822`), but its only consumer —
`volume_scale *= GetAudioLocation()->GetDistanceVolumeScale()` in
`Dynamic3DPatchSource::CalculateSourceVolumeScale` — is **commented out** behind an early return
(`L4AUDIO.cpp:1470-1496`, comment at :1493-1495). Instead: hardcoded
`alDistanceModel(AL_LINEAR_DISTANCE)` (`AUDIO.cpp:98`) with never-set
AL_REFERENCE_DISTANCE/AL_ROLLOFF_FACTOR (defaults 1.0) and AL_MAX_DISTANCE=clipRadius*clipScale
(`L4AUDIO.cpp:1109,1461,1997`) — a straight-line fade to zero.
Numbers (clippingScale=1): d=60: AL 0.87 vs authored 1.0; d=100: 0.78 vs 0.99; d=300: 0.33 vs
0.66 (-6 dB); d=440: 0.02 vs 0.44. **Distant battle sounds are drastically too quiet.**
Confirmed side-effects of the distance-free engine volume: (a) the LowAudioVolumeThreshold=0.3
transient-start cull (`AUDREND.cpp:197,223-237`) is distance-blind; (b) AudioWeighting voice-steal
(`AUDSRC.h:526-536`, `L4AUDRND.cpp:1175`) no longer favors near sources; (c) the CalculateMix
ducking chain (`L4AUDRND.cpp:684-811`, compression 0.92/8.5) treats far sources as full-presence.
**Fix (near one-liner, highest leverage in the audit):** `alDistanceModel(AL_NONE)`, restore the
commented distance multiply (the spatial model is already updated each Execute), apply
`distanceVolumeScale` in Static3D's gain write too, drop the AL_MAX_DISTANCE attenuation writes.
Restores curve + cull + steal + ducking simultaneously.
<a name="f4"></a>
#### F4. Volume curve: linear AL_GAIN vs the CC7 squared law (High, CONFIRMED)
The original's volume path ended in MIDI CC7 = ftol(volume_scale*127+0.5) on the AWE32
(decomp @00463bbc part_008.c:7205-7218; @004641e0 :7538-7583 — the 127.0/0.5 constants
byte-verified in the EXE; the engine's own retained MIDI source at `L4AUDIO.cpp:2146-2239`
confirms). The GM/SoundFont-standard CC7 curve is concave: amplitude ~ (v/127)^2, i.e.
40*log10(v/127) dB (standard-based inference for the binary Creative driver — flagged, not
decompilable). The port writes the identical volume_scale LINEARLY to AL_GAIN at **three** sites:
`L4AUDIO.cpp:1110` (DirectPatch), `:1459` (Dynamic3D), `:1995` (Static3D).
Result: every intermediate volume plays ~+6 dB louder than the arcade at mid-scale (0.5: -6 dB vs
-12 dB; 0.3: -10.5 dB vs -21 dB) — the authored dynamic range is compressed and quiet
ambience/servo layers sit far too high against weapons/engine.
**Fix:** square volume_scale (or apply the exact concave transform, optionally quantized to 127 CC
steps) at all three AL_GAIN sites. Do NOT square the master listener gain (a common factor; the
port's 0.6 is calibration, not authored).
<a name="f9"></a>
#### F9. Brightness / low-pass filter chain dead — no distance muffling, ctl-5 inert (Medium, CONFIRMED)
The original continuously drove the AWE32 initial-filter-cutoff NRPN 21 (100-8000 Hz per
`L4AUDHDW.h:65-68`, scaled by the patch's authored MaxMIDIFilterCutoff) from ExecuteModel —
decomp-verified per source type: DirectPatch = brightnessScale (gated on useBrightness;
@00463bbc, part_008.c:7191-7203); **Dynamic3D = highFreqCutoffScale x brightnessScale, ungated,
all four quadrant channels** (part_008.c:7496,7589-7604) — every moving 3D sound got duller with
distance; Static3D = brightnessScale only (part_008.c:7831-7884).
The port computes the values and applies nothing: `L4AUDIO.cpp:1067-1086` only updates the
`lastMIDIFilterCutoff` bookkeeping member; Dynamic3D's ExecuteModel has no filter path; Static3D's
NRPN block is inside the comment spanning :2020-2245; **no alFilter/EFX call exists anywhere**
(only stock SDK headers). The authored distance HF rolloff (AUDIO.INI:23-25: exponent 2.0, knee
60, scale 0.0035; AUDIOMR 0.003) is still computed per frame into `highFreqCutoffScale`
(`AUDLOC.cpp:386-405`) — `GetHighFreqCutoffScale()` has zero callers. Brightness ctl 5 (2 authored
bindings, log:199,285), use_brightness_scale, per-patch maxMIDIFilterCutoff, and the reconstructed
footstep Brightness-mixer feed (`mech2.cpp:318`) all drive a dead sink. Everything plays
spectrally full-bright at all distances.
**Fix:** one EFX AL_FILTER_LOWPASS per source; AL_DIRECT_FILTER with GAINHF from
highFreqCutoffScale x brightnessScale (Dynamic3D), brightnessScale alone (Static3D/DirectPatch,
respecting useBrightness), mapped through the AWE curve (~100 + cutoff*7900/127 Hz).
<a name="f10"></a>
#### F10. Doppler: authored model dead, AL doppler wrong constants AND wrong sign (Medium, CONFIRMED)
Triply wrong, decomp-verified intent:
1. The authored model (AUDIO.INI:30-31 doppler_range=600 cents, speed_of_sound=250 u/s) is
computed every spatial update into `dopplerCents` = 600*(1-250/(250-v)) with near-ear fade and
+/-c clamp (`AUDLOC.cpp:451-508`) — and **the decomp proves the original consumed it**: the
dynamic patch source ExecuteModel adds audioLocation+0x58 (dopplerCents) to
CalculateSourcePitchOffset before the MIDI pitch-bend clamp (part_008.c:7466). In the port,
`GetDopplerCents()` has zero callers.
2. Instead AL doppler is enabled with hardcoded `alDopplerFactor(0.3f)` (`AUDIO.cpp:99`) and
`alSpeedOfSound` never called (default 343.3) — neither constant nor curve shape matches.
3. The velocity feeds are sign-inverted relative to the position frame: AL_POSITION = source
relative to head, but AL_VELOCITY = **negated** source world velocity with head velocity never
subtracted (`L4AUDIO.cpp:1429-1433,1458,1460`); listener velocity is also negated
(`AUDIO.cpp:155-158`) but cancels under AL_SOURCE_RELATIVE.
Net: an approaching missile/mech pitches **DOWN** (~-88 cents at 60 u/s, ~-145 at 100 u/s) where
the original gave +116 cents at 60 u/s approaching, and the player's own motion produces no
doppler at all.
**Fix (faithful, per the decomp):** disable AL doppler and add GetDopplerCents() into the
Dynamic3D pitch-cents chain (2^((cents_chain + dopplerCents)/1200) x note factor). The original
applied doppler only on the dynamic multi-channel path — keep static/direct sources doppler-free.
<a name="f11"></a>
#### F11. Reverb: authored wet-exterior / dry-cockpit split dead (Medium, CONFIRMED)
Decomp-proven original behavior: the 3D patch-source StartImplementation (FUN_00463ddc,
part_008.c:7278-7394) reads global_reverb_scale=0.3 (AUDIO.INI:47, stored at head+0x4C) and sends
MIDI CC91 = 38 on all four AWE32 channels; DirectPatchSource (FUN_004638a8, part_008.c:7104-7140)
sends CC91 = 0 — deliberate wet-exterior vs dry-cockpit contrast. In the port all send sites are
commented out (`L4AUDIO.cpp:949, 1253-1263, 1759-1771`), `GetGlobalReverbScale` has no live
callers, and no EFX effect/aux slot is ever created. Everything plays bone-dry; the arena-space
cue separating outside sounds from cockpit UI is gone. (Note: the code was already commented in
the repo's initial commit — an inherited gap from the WinTesla conversion, still a fidelity gap.)
**Fix:** one EFX EAXReverb aux slot at init (gain from global_reverb_scale, AWE32-like preset);
AL_AUXILIARY_SEND_FILTER on Dynamic3D/Static3D sources only; Direct sources dry.
<a name="f12"></a>
#### F12. Front/rear + pan placement: all cockpit and static sounds dead-center (Medium, CONFIRMED)
The original routed every DirectPatchSource by its authored 6-value position enum
(Front/Rear/FL/FR/RL/RR, `L4AUDIO.h:355-362`): positions 0/2/3 to the FRONT sound card, 1/4/5 to
the REAR (decomp @00463848, part_008.c:7051-7062; the pod required two cards, AWE_FRONT/AWE_REAR),
with MIDI pan CC10 = 0x3F center / 0x00 left / 0x7F right (@004638a8, part_008.c:7119-7140). The
port reads audioPosition from the stream (`L4AUDIO.cpp:795`) but every use is commented out
(:854-868, :902-916, :952-966); SetupPatch pins all sources AL_SOURCE_RELATIVE at (0,0,0)
(`L4AUDLVL.cpp:150-156`) and DirectPatch ExecuteModel never writes AL_POSITION. Similarly,
Static3DPatchSource with useInternalSpatialization (the BuildFromPage default) computes its
quadrant spatialization at start (:1613-1641) but consumes it only in commented code; the live
position member is written solely by the collision trigger (`L4AUDWTR.cpp:149`) — all other
internal statics also play centered. (Caveat: which authored content is off-center is inferred
from the enum + original routing code; the mechanism centers ALL of it regardless.)
**Fix:** since sources are already AL_SOURCE_RELATIVE, set AL_POSITION per enum in
DirectPatchSource::StartImplementation (front=-Z, rear=+Z, left=-X, right=+X; MUNGA azimuth 0
maps to +Z=rear per the CalculateSpatialization +180 shift). For internal-spatialization statics,
derive AL_POSITION from the already-computed azimuth. Keep distance ~1 so the distance model does
not attenuate them.
<a name="f22"></a>
#### F22. ITD / between-ears model dead (Low, UNVERIFIED-LOW)
itd_difference=0.0015 and distance_between_ears=2.0 feed the L4AudioSpatialization 4-channel model
(per-quadrant delays, transient ITD pitch offsets, the between-ears front/rear power crossfade) —
all computed, all discarded (Get*ITDPitchOffset: zero callers; the quadrant-gain consumers are the
commented MIDI sends). Lost: interaural time cues and the smooth blend when a source passes
through the head (it now snaps across the stereo field). Faithful minimum: blend AL_POSITION
toward the origin inside distance_between_ears/2; OpenAL Soft HRTF supersedes the ITD constant for
desktop; full quadrant gains only matter when driving the pod's physical 4-speaker pairs.
### C. Triggers and databinding
<a name="f8"></a>
#### F8. Coverage matrix: ~24% of attribute-gated watchers are dead (High, PARTIAL — corrected counts)
From the 35 s capture (118 statecfg + 54 matchcfg + 23 trigcfg + 29 scalecfg = 224 attribute-gated
configs; attrbind=227, attrnull=53): **53 (~24%) sit on dead attributes**, plus a 54th outside the
denominator (ControlsMapper.TargetRangeExponent, class-81 delta trigger, log:330-331). Breakdown:
45/54 match watchers (38 ReportLeak on 19 subsystems — F6; 4 FireCountdownStarted — F18; 1
Torso.MotionState — F16; 2 Entity.IncomingLock — F7, whose match=0 Stop fires once spuriously),
5/29 scale watchers (2 CollisionSpeed, 1 DistanceToMissile, 1 UnstablePercentage, 1
Torso.SpeedOfTorsoHorizontal), 3/23 trig watchers (UnstablePercentage, ReduceButton,
SpeedOfTorsoHorizontal). Of the 9 ConfigureActivePress watchers, 7 are on the -1 pad and 2
(Avionics, Myomers) bind real members reading the authored idle -1.
Two dead mechanisms: the engine inert pad for unpublished names (`WATCHER.cpp:118-137`) and
`Mech::attrPad`, a shared read-only-0 Scalar aliased by five Entity attrs (ptr 0E514DD4;
`mech.cpp:708,730,736,738,740`). **All 118 state watchers bind real StateIndicators.** Liveness
verified: LocalVelocity (120 trigger polls), FootStep (14 matchfires), GeneratorOn x4,
Myomers.SpeedEffect, HeatSink.CurrentTemperature, PercentDone x5, WeaponState-driven sequences
started 21/4/2 times. Entity.AnimationState's 17 authored trigger states produced zero fires
(consistent with autodrive lacking falls/jumps — unverified, see F23).
**Fix path:** publish missing attributes in owning subsystems' ATTRIBUTE_ENTRY tables (the proven
AmmoState/GeneratorState pattern) and replace the five attrPad aliases with driven members. Keep
`scratchpad/audio_coverage.py` as the regression check; include TargetRangeExponent in the
worklist.
<a name="f6"></a>
#### F6. ReportLeak: the coolant-leak warning can never sound (High, CONFIRMED)
All 19 subsystems that authored a ReportLeak Logical match-watcher pair (Condenser1-6,
GeneratorA-D, Avionics, Myomers, PPC_1/2, ERMLaser_1-3, SRM6_1/2) bind ReportLeak to NULL -> the
constant-zero pad. The 38 pad-bound match watchers (1->Start, 0->Stop of the looped leak-warning
sequence 0E4ACE50 — 3-note pattern 16/19/23, tempo 160; log:343) can never fire Start; the 19
match=0 halves each fire one spurious Stop at prime. No other authored path targets the sequence;
its seqstart count is 0 for the whole session. **Largest single block of dead authored audio
(38 of 54 match watchers).** The underlying condition already exists in the port (`heat.cpp:418`
publishes CoolantMassLeakRate/coolantDraw for the LeakGauge).
**Fix:** register ReportLeak on the subsystem attribute tables per the decomp's original binding
(read the binary's subsystem attribute table for the member it pointed at — likely the
leak-rate/leaking flag — rather than inventing a threshold).
<a name="f7"></a>
#### F7. Missile-lock alarm impossible (High, CONFIRMED)
Entity.IncomingLock (matchcfg 1->Start / 0->Stop of looped beeper 0E4AC548, note 58; log:299,
302-306) and Entity.DistanceToMissile (scalecfg ctlID=12 = **Tempo**, mapping distance 100..800 to
tempo 600..10 — the beep accelerates as the missile closes; log:300-301) both bind to
`Mech::attrPad` (read-only 0). The original backed these with real distinct members — **binary
attribute table verified: IncomingLock @mech+0x3FC, DistanceToMissile @mech+0x400**, initialized
in the mech reset (part_012.c:9446-9447) — a genuine regression, not dead content.
**Fix:** real backing members driven by the missile/threat system: IncomingLock set while an
inbound lock/launch on the player exists; DistanceToMissile = range to nearest inbound missile
(far default when none). The authored watchers then reproduce the accelerating alarm unchanged.
<a name="f5"></a>
#### F5. FootStep is a contact LEVEL, not a clip-transition pulse (High, CONFIRMED)
Direct disassembly of BTL4OPT.EXE (both functions fall in decomp-export gaps): inside the mech's
per-frame performance, footStep@mech+0x394 = (rootJointTranslation.y <= *footStepThreshold) ? 1:0,
where the threshold pointer is SequenceController+0x20 = &animation_data[2] — the ANI header
word[2] that `JMOVER.cpp:1415` parses and nothing else reads — and the root joint is the gamedata
"jointlocal" joint (mech+0x5C8). Evaluation is skipped (value retained) while the channel's anim
state is 0/1. Present in both performance variants: master perf entry 0x4a9b5c (code
@0x4a9e80-0x4a9eb6, leg channel) and alternate entry 0x4ab9d8 (@0x4aba86-0x4abab9, body channel).
The port instead pulses footStep=1 for a fixed 150 ms on every locomotion clip TRANSITION
(`mech2.cpp:265-270`, decay `mech4.cpp:5219-5230`) and its SequenceController never captures
animation_data[2] (`seqctl.cpp:153` skips hdr[2]). Step sounds fire at clip boundaries with fixed
width instead of authored root-height contact crossings — wrong counts for clips whose root Y
crosses the threshold twice or never.
**Fix:** in the port's authoritative per-frame path, while leg-channel gait state not in {0,1},
set footStep from rootY vs *footStepThreshold (captured at SelectSequence from &hdr[2]); remove
the 150 ms pulse/decay; replicate the state-0/1 value retention; re-export the two gap functions
(0x4a9b5c, 0x4ab9d8) into the decomp.
<a name="f16"></a>
#### F16. Torso-twist servo whir entirely dead (Medium, CONFIRMED)
Torso.SpeedOfTorsoHorizontal (scalecfg pitch -200..+200 cents over 0.5..0.9 + trigcfg start/stop
at 0.25) and Torso.MotionState (matchcfg ==2 -> Start@1.5) bind NULL (log:289-297) because the
reconstructed Torso publishes no attributes (`torso.cpp:143-144` default-constructs an empty
AttributeIndex). TorsoTwistInt01/Ext01/Stop01 are unreachable; none played. **Correction:**
hud.hpp:103's "id 7 -> @0x1E4" is the HUD class's own table (a HUD read-out), NOT a Torso member —
on the binary Torso, +0x1E4 is currentElevation. Do not publish the yaw rate there.
**Fix:** register SpeedOfTorsoHorizontal on Torso as |yaw rate| (binary backing member plausibly
twistVelocity@0x1E8 or effTwistRate@0x244 — confirm the Torso attribute table in the decomp first)
and MotionState as the torso motion enum (authored trigger value 2; meaning inferred [T4]).
<a name="f17"></a>
#### F17. Entity attrPad family: instability alarm, impact scaling, ReduceButton (Medium, CONFIRMED)
Three authored Entity bindings alias the shared read-only attrPad while the binary bound each to a
distinct live member (binary attr table @0x50bea0: UnstablePercentage->+0x3F0,
CollisionSpeed->+0x4B4, ReduceButton->+0x340):
1. **UnstablePercentage** — volume scale (0..1) and start trigger (thresh 0.01) both stuck; the
instability/gyro-stagger alarm never sounds. The driver is the mech+0x3F0 sway/overspeed
accumulator the port already flags as unreconstructed for the gyro — one model fixes both.
2. **CollisionSpeed** — authored AttackVolume [0.9,1] and Brightness [0.7,1] over impact speed
[0,25] pinned at minimum; impacts play (live CollisionState) but never scale with how hard you
hit. (Original +0x4B4 writer not located in the export; intent rests on the authored scales +
distinct member.)
3. **ReduceButton** — click-feedback trigger can never fire (whether the original cab drove it is
unverified; the dead binding is fact).
**Fix:** real members: instability fraction from the 0x3F0 model; collisionSpeed = |impact
velocity| stored when collisionTemporaryState goes 0->1 (`mech4.cpp:5332-5382` already has it);
ReduceButton wired to the mapper like the Look* attrs.
<a name="f15"></a>
#### F15. ConfigureActivePress: weapon-configure ticker can never fire (Medium, CONFIRMED)
Binary ground truth: ConfigureActivePress is a **MechSubsystem-BASE** attribute (id 2, descriptor
@0x50de5c -> subsystem+0x110, registered by the MechSubsystem class stub @0x4acf10). Its only
runtime driver is MechWeapon's ConfigureMappables handler @004b9550: **0 while the configure
button is held, -1 on release** — a held-session flag, not a button index. The port misnames the
correctly-driven weapon field `vitalSubsystemIndex` ("no confirmed reader" — the reader is the
authored audio trigger watchers at thresh=-1), omits the attribute from the chain so every weapon
binds the constant -1 pad (log:529-531), and appends undriven duplicates on Myomers/Sensor at
invented offsets 0x358/0x328 (layout departure; behaviorally harmless).
**Fix:** publish ConfigureActivePress at the MechSubsystem level bound to the existing +0x110
field (rename vitalSubsystemIndex -> configureActivePress); drop or ignore the Myomers/Sensor
duplicates.
<a name="f18"></a>
#### F18. FireCountdownStarted is the ammo COOK-OFF warning — and it's dead (Medium, PARTIAL, corrected)
AmmoBinSRM6_1/2.FireCountdownStarted binds NULL -> pad (log:674-679, 721-726); its 4 authored
start/stop match watchers can never fire. **Corrected identification:** the binary's AmmoBin
attribute table (@0x512600) maps FireCountdownStarted -> AmmoBin+0x18C — the member the port
already reconstructs as **`cookOffArmed`** (set when the heat alarm hits FAILURE, FUN_004bd394;
armed via HandleMessage(1), FUN_004bdb94; cleared on CookOff/empty). It is the ammo-explosion
countdown warning, NOT an SRM salvo launch cue — do not touch projweap.cpp.
**Fix:** one Logical ATTRIBUTE_ENTRY on AmmoBin pointing at the existing cookOffArmed member.
<a name="f19"></a>
#### F19. Step-intensity invention fights a live authored feed (Medium, PARTIAL, corrected)
The mech2.cpp foot-plant broadcast (`mech2.cpp:272-338`) is unauthored in its curve (speed/25 with
a 0.55 floor) and delivery (patch-sniffing the footfall mixers directly, zeroing the sibling
input). **The premise that the original feed was lost game code is wrong:** the authored stream
wires LocalAcceleration -> ctl100 ([0,10]->[0,1]) and LocalVelocity -> ctl101 ([0,0.6]->[0,0.4])
scale watchers into the authored splitter, and the ctl101 watcher **fires on every captured
footstep** (log:1591, 2147, 2459, ...), zeroing the invention's just-written mixer input right
after the FootStep matchfire — in log:1574-1598 this drops the footstep start entirely
("[spatial] DROP transient start src=0E6BC948 vol=0").
**Fix:** remove the game-code broadcast; make the port's published LocalVelocity/LocalAcceleration
attributes carry the motion components the authored AudioMotionScale watchers extract (one
currently reads 0 while linear speed is 30.7 in the same poll — the port publishes the wrong/flat
component) and let the wholly-authored scale->splitter->mixer chain drive footfall volume and
brightness.
<a name="f20"></a>
#### F20. Zoom blip: TargetRangeExponent unpublished (Low, UNVERIFIED-LOW)
ControlsMapper.TargetRangeExponent binds NULL (log:330-331) so the authored AudioScalarDeltaTrigger
(blip per zoom step) is dead — yet the live member exists and slews every frame
(`btl4mppr.cpp:313-314, 391-407`). Pure bookkeeping: add the ATTRIBUTE_ENTRY (requires giving
BTL4ControlsMapper its own AttributePointers[] chained to MechControlsMapper's index).
<a name="f23"></a>
#### F23. Benign-invention check (Low, UNVERIFIED-LOW)
(1) FootStep 150 ms pulse fired the authored watcher fine in-session (superseded by F5 anyway).
(2) **AnimationState numbering is the one worth closing:** none of the 17 authored trigger states
{1,2,5,8-11,14-17,20-23,26,27} fired in 35 s of walking/turning; if the port's runtime clip
numbering differs from the binary's AnimationState numbering, all 17 sounds are silently mis-keyed
— diff the binary's state writer vs the port's clip ids, then force a fall/jump and confirm.
(3) CollisionState is driven 0/1 while authored as 4-state (NoCollision/InitialHit/Slide/Rest) —
nothing lost in THIS stream (listens on 1) but Slide/Rest scrape audio elsewhere would be dead.
(4) Master listener gain 0.6 (env-tunable) is a uniform port knob; no relative distortion.
---
<a name="f38"></a>
### F38 — `LoopAtWill` was mapped to "loop forever" (Gitea #51) — FIXED 2026-07-25
**Symptom (playtest night 3, SAURON):** "coolant flush sound glitched and perma" — a coolant sound
started and never stopped; a client restart cleared it.
**Defect.** `PatchLevelOfDetail::SetupPatch` (`L4AUDLVL.cpp`) decided looping from the SAMPLE flag
alone: `AL_LOOPING = (info.loop != ForceStatic)`. That armed **224 of the 603 authored samples** as
OpenAL sources that never end on their own — including unmistakable one-shots (laser fire, explosions,
button clicks). Any such sound whose note-off never arrives plays forever. Measured **1946**
`LoopAtWill × Transient` arming events in a single short session.
**Why the sample flag is not the decision** [T1]:
| evidence | source |
|---|---|
| `LoopAtWill` = "will play once **or loop as desired**"; `ForceStatic` = "plays only once **even if looped**" (a veto); `LoopAlways` = "ramp up and then down" | `L4AUDLVL.h:14-19`, the engine's own comments |
| `LoopAtWill` is enum value **0** — the DEFAULT an unauthored sample receives | `WTPresets.cpp:37` (`none.loop = LoopAtWill`) |
| `LoopAlways`, the real always-loop, is used by **zero** shipped samples | `audiopresets.cpp` (0 occurrences) |
So "as desired" is the **source's** authored `AudioRenderType``Transient` (one-shot) vs
`Sustained` (held) — streamed from `AUDIO*.RES` (`AUDLVL.cpp:28`) and already consulted by the
renderer (`L4AUDRND.cpp:567`, `AUDREND.cpp:184`).
**Fix.** `SetupPatch` takes the source's render type; the rule becomes `LoopAlways`→1,
`ForceStatic`→0, `LoopAtWill``sustained`. Threaded from all three `L4AudioSource` call sites
(Direct/Dynamic3D/Static3D, `L4AUDIO.cpp:966/1314/1875`).
**Measured, not assumed** (`BT_LOOP_AUDIT=1`, `scratchpad/loopaudit.py`):
| sample flag | source type | loop | events |
|---|---|---|---|
| LoopAtWill | Transient | 0 (**was 1**) | 1946 |
| LoopAtWill | Sustained | 1 (unchanged) | 65 |
| ForceStatic | Transient | 0 (unchanged) | 292 |
The engine loops (`EngineAccel07_z0..z2`, `EngineMotor01`, `EnginePower01`) are all
`LoopAtWill / Sustained` and **preserved**. All 24 reclassified samples are genuine one-shots:
laser charge/fire/explosion/loaded, missile loading, engine shift, coolant pressure inc/dec.
**A/B proof** (`scratchpad/loopab.py`, same session both arms, only `BT_LOOP_LEGACY` differs):
```
[legacy] still playing loop=1 after all releases: EnginePower01, CoolantPresInc03_z2, CoolantPresDcr03_z2
[fixed] still playing loop=1 after all releases: EnginePower01
```
The two stuck coolant sources ARE the reported symptom. Note they were eventually reclaimed when a
later trigger reused the source, which is why the bug was intermittent — the "perma" case is when
nothing else retriggers that source. The fix removes the mechanism.
**Residual risk / how to A/B in the field.** `BT_LOOP_LEGACY=1` restores the old rule without a
rebuild. The layers to listen to are the beam sustains: `LaserASustain*` / `LaserCSustain*` are
authored `LoopAtWill` on a **Transient** source, so they now end with the sample rather than looping
until note-off. If a beam sounds short, that is the knob. [The render type is T1 authored data; the
*interpretation* that `LoopAtWill` defers to it is a strong reading of the enum + defaults, not a
disassembled statement — flagged accordingly.]
**Likely bearing on #32** (audio cutting in and out late in a match): a stuck looping source holds
its pool slot for the rest of the round, so eliminating 1946 potential leaks per session should
reduce source-pool starvation.
---
<a name="validations"></a>
## 4. Validations and KB corrections (no fidelity gap)
- **Patch numbering = wPreset (settled).** The original SetupPatch @00465ba8 sends the authored
patch_ID verbatim as a MIDI Program Change (+ CC0 bank, + CC121 reset); the SBK resolves program
N by wPreset. `tools/sf2extract.py` already keys on phdr wPreset. Live traces confirm (patch 83
-> ProgramButton01, patch 56 -> LaserLoaded01). **Any future tooling must key on wPreset, never
phdr record order** — bank1 has 38 presets where wPreset != file index. Record in
`context/decomp-reference.md`.
- **Calibration rate origin (KB correction).** FUN_0044e19c is
ApplicationManager::GetFrameRate(); the original renderer rate was the system CLOCK TICK rate
(18.2065 Hz / 28 Hz), i.e. audio "frames" were clock ticks. The port's `btl4app.cpp:283-303`
DPLARG+0x10 read is garbage-with-a-clamp; 1000 (Windows ticks/ms) is structurally equivalent and
correct, but delete the bogus read and pass SystemClock::GetTicksPerSecond() with a comment.
- **Published-velocity smoothing is faithful in kind.** Mech::Execute (FUN_004ab430) runs the
localVelocity components through AverageOf running-average filters (part_012.c:15169-15179);
annotate the port's smoothing with this evidence -> T1.
---
## 5. Recommended Fix Order
### Quick wins (small, isolated, high leverage)
1. **F3 distance:** `alDistanceModel(AL_NONE)` + un-comment the distance multiply + apply in
Static3D + drop AL_MAX_DISTANCE attenuation writes. Near one-line; also fixes cull, voice
steal, and ducking.
2. **F4 volume curve:** square volume_scale at the three AL_GAIN sites.
3. **F10 doppler:** alDopplerFactor(0); add GetDopplerCents() into the Dynamic3D pitch chain.
4. **F15 ConfigureActivePress:** publish the base attribute on the existing +0x110 member.
5. **F18 cook-off:** one ATTRIBUTE_ENTRY -> cookOffArmed.
6. **F20 zoom blip:** one ATTRIBUTE_ENTRY -> targetRangeExponent.
7. **KB hygiene:** record wPreset confirmation; fix the calibration-rate claim; delete the DPLARG
read.
### Structural — bank regeneration (ship as one unit)
8. **F1 + F2 + F13 + F14 extractor rewrite:** emit ALL zones (per-zone WAV, keyLo/keyHi, loop
points, pan, attenuation), bake per-zone tuning into each WAV's declared rate
(44100*2^(((60-effRoot)*100+tune)/1200)), extend SAMPLEINFO, key-range selection in
SetupPatch/PlayNote, AL_SOFT_loop_points (alBufferiv). **Loop regions must land with or before
multi-zone** (the MechExplosion 1.5% loop layer otherwise becomes a repeating-boom bug).
This one unit fixes most of the pitch and layering fidelity at once.
9. **F13 release:** gain-ramp on Stop over the per-preset authored releaseVolEnv.
### Structural — spatial/EFX
10. **F9 filters:** EFX AL_FILTER_LOWPASS per source (brightness x distance HF rolloff per the
per-type decomp semantics).
11. **F11 reverb:** one EAXReverb aux slot; 3D sources wet (0.3), Direct dry.
12. **F12 placement:** AL_POSITION from the DirectPatch position enum; internal-spatialization
statics positioned from their computed azimuth. (F22 ITD/HRTF optional after.)
### Structural — attribute reconstruction (decomp-guided, per finding)
13. **F6 ReportLeak** (biggest dead block; read the binary's subsystem table for the member).
14. **F7 IncomingLock/DistanceToMissile** (members @+0x3FC/+0x400; drive from the missile system).
15. **F5 FootStep contact level** (+ re-export the two decomp gap functions).
16. **F19 footstep feed:** publish correct motion components; delete the mech2.cpp broadcast.
17. **F16 Torso attrs** (confirm the binary Torso table first), **F17 attrPad family**
(instability model @0x3F0, collisionSpeed, ReduceButton).
18. **F23(2) AnimationState numbering audit**, then F21 attack velocity/time as polish.
**Regression harness:** re-run the audit capture after each block and assert with
`scratchpad/audio_coverage.py` that the dead-watcher count monotonically drops; A/B pitch/volume
spot checks against the recovered-rate table for the bank regeneration.