fix(audio): Campaign A slice A1 — retail's sound probability gate (#355)
The SoundTable probability field is a Bernoulli play/skip gate applied at the play site (SoundManager::PlayProbability @0x005500E0), not a selection weight — and variant selection (SoundManager::GetSound @0x00550680) is a uniform index over (n-1) that ignores probability entirely. SoundCookbook did the opposite: a cumulative-distribution walk weighted BY probability, short-circuiting single-entry lists before rolling at all. A dat census says 4,183 of 4,184 entries are single-entry and 686 of those carry probability < 1.0, so the gate was categorically absent: Speak1 idle chatter authored at 0.05 fired every trigger (~20x too often), wound/attack/ swoosh variants never dropped, and six 0.0001 entries always played. Split into retail's two steps (PickVariant + PlayProbability, composed by Select) over a new ISoundRandom modelling both retail roll ranges: the variant roll clamped below 1.0 (0x00797D48) and the gate's 1/32767 grid, which is why 0.0001 resolves to ~1.2e-4. PickVariant reproduces retail's (n-1) off-by-one verbatim per the port-faithfully rule — the last variant of a multi-entry sound is unreachable, costing exactly one wave (0x0A00051E) in the shipped dats. Also removes invented mechanism this review disproved: the dead Core SoundEntry/ISoundCache scaffold (PitchMin/PitchMax, Loop, Is3D — retail never calls SetFrequency, never sets the loop flag, and creates every gameplay buffer 2D), the engine's pitch plumbing, the int 0..7 priority cast (the dat field is a float in [0,1]; 4,100 entries collapsed to 0), and the clamp-at-the-field on volume (an unbounded gain retail clamps only after the distance divide). Tests rewritten as conformance against the disassembled values, replacing a self-referential suite that pinned the wrong model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ffa5087527
commit
c69b3bde04
8 changed files with 445 additions and 161 deletions
|
|
@ -24,6 +24,47 @@ What does NOT go here:
|
|||
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
|
||||
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
|
||||
|
||||
## #355 — Sound probability was never applied: every gated cue played on every trigger
|
||||
|
||||
**Status:** CLOSED 2026-08-08 (Campaign A slice A1) — user gate finding
|
||||
("we get incorrect ambient and stuff like that"), root-caused during the
|
||||
six-lane audio review.
|
||||
|
||||
Retail's `SoundTable` entries carry a `probability` field that is a **Bernoulli
|
||||
play/skip gate** applied at the play site (`SoundManager::PlayProbability` @
|
||||
`0x005500E0`: `rand() * (1/32767) < probability`, else silence), entirely
|
||||
separate from variant selection (`SoundManager::GetSound` @ `0x00550680`:
|
||||
`idx = (int)(roll * (n - 1))`, which ignores probability).
|
||||
|
||||
`SoundCookbook.Roll` instead treated probability as a cumulative selection
|
||||
weight AND short-circuited single-entry lists before rolling at all:
|
||||
|
||||
```csharp
|
||||
if (entries.Count == 1) return entries[0]; // probability never consulted
|
||||
```
|
||||
|
||||
An independent walk of the shipped dats found 4,183 of 4,184 entries are
|
||||
single-entry lists, and **686 of those carry probability < 1.0** — so the gate
|
||||
was categorically absent from the client. Loudest symptom: `Speak1` creature
|
||||
idle chatter (49 entries authored at 0.05) fired ~20× too often; wound / attack
|
||||
/ swoosh variants never dropped; six entries authored at 0.0001 played on every
|
||||
trigger. Affected 20 of 123 SoundTypes.
|
||||
|
||||
Fixed by splitting the model into retail's two steps
|
||||
(`SoundCookbook.PickVariant` + `PlayProbability`, composed by `Select`) over a
|
||||
new `ISoundRandom` that reproduces both of retail's roll ranges — the variant
|
||||
roll clamped below 1.0 (`0x00797D48`) and the gate's 1/32767 grid, which is
|
||||
what makes a 0.0001 probability resolve to ~1.2e-4 rather than 1e-4.
|
||||
`PickVariant` deliberately reproduces retail's `(n-1)` off-by-one (the last
|
||||
variant of a multi-entry sound is unreachable; blast radius in the shipped dats
|
||||
is exactly one wave, `0x0A00051E`).
|
||||
|
||||
Evidence: `docs/research/2026-08-08-audio-retail-dat-layer.md` §2 (census +
|
||||
disassembly, both elided by Binary Ninja — BN also renders `PlayProbability`'s
|
||||
branch **inverted**, so porting its rendering would have played sounds exactly
|
||||
when retail stays silent). Campaign:
|
||||
`docs/plans/2026-08-08-audio-parity-campaign.md`.
|
||||
|
||||
## #354 — Spell-bar drag reorder did not work: lifting a favorite canceled the drag before the drop could land
|
||||
|
||||
**Status:** CLOSED 2026-08-08 — user gate finding ("I should be able to
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ global kill switch.
|
|||
|
||||
| Slice | Status | Commit | Gates |
|
||||
|---|---|---|---|
|
||||
| A1 | — | — | — |
|
||||
| A1 | **COMPLETE** 2026-08-08 | (this commit) | 42 Core audio tests; full Release suite 11,563 passed / 4 skipped / 0 failed. Closes #355. |
|
||||
| A2 | — | — | — |
|
||||
| A3 | — | — | — |
|
||||
| A4 | — | — | — |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue