From 0f7fbcecdf44b38d50290b5c64c07ecabafb2646 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Tue, 8 Sep 2026 07:56:02 -0400 Subject: [PATCH] =?UTF-8?q?docs(audio):=20D207=20M-78b=20=E2=80=94=20user?= =?UTF-8?q?=20Bunker=20repro;=20player-gun=20double-buffer=20lead?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User repro: player gunshots specifically go permanently muted mid-firefight and stay muted after the alarm ends; all other SFX fine. Not generic pool starvation — a stuck-state bug in the gunfire.c:3178 two-slot double buffer (audioHandle / field_A48). Leading mechanism: asynchronous sndDeactivate posts to the 64-slot SFX event queue; under firefight+alarm load the queue saturates, alEvtqPostEvent drops the deactivate, the state never disposes, the slot never clears — once both slots stick, no player gunshot can play. All three pool sizes are byte-match stock GE => port timing bug, not a value to bump. Next: read the user's audiotrace.log to confirm evtq saturation + uncleared slots. Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR --- docs/dev/findings.md | 48 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 13f60b4b..42707998 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -8183,3 +8183,51 @@ listening pass on a real playthrough is still owed before calling it closed. `powershell -Command "& bash.exe script.sh"`. Every `GE_*` probe silently read as unset, which looks exactly like "the probe is broken". `cmd.exe` inherits normally, so `audiodebug.ps1` generates a `.bat` and runs that. + +### M-78b — user repro on Bunker: symptom is specific and permanent + +Natural Bunker alarm trigger (spawn, go forward into the room ahead, shoot +one of the two guards → the other trips the alarm), then sustained firefight. +User by-ear result: + +- Most SFX keep playing during the klaxon. +- **Eventually the *player's* gunshots stop being emitted — and stay muted + even after the alarm ends.** Enemy gunfire and all other SFX continue fine + during and after. + +This is NOT generic pool starvation (that would drop random sounds +transiently). It is specific to the player-weapon fire sound and it is +*permanent* — a stuck-state bug, not a transient voice shortage. + +**Structural lead (static, unconfirmed — needs the capture).** The player +fire sound is played through a **two-slot double buffer** in +`bondfirefunc` / `gunfire.c:3178-3202`: `handptr->audioHandle` and +`handptr->field_A48`. Each trigger interval: `sndDeactivate` whichever slot +is currently *playing*, then `sndPlaySfx(..., &slot)` into whichever slot is +*NULL* (`audioHandle` first, else `field_A48`). If **both** slots are ever +left non-NULL-but-not-playing at once, the `else if (field_A48 == 0)` guard +fails and **no further player gunshot can play, ever** — matches the symptom +exactly. + +A slot is cleared only by `sndUnlinkClearSound` (via `sndDisposeSound`) +following the state's back-link `state->state->link.next`. `sndDeactivate` +is **asynchronous** — it posts `AL_SNDP_DEACTIVATE_EVT` to the 64-slot SFX +event queue (`MUSIC_SFX_SEQ_CONFIG_MAX_EVENTS = 0x40`). If that queue is +saturated (heavy firefight + the alarm's self-retrigger posts + every +ricochet/impact/yelp posting PLAY/PLAY_EVT/STOP/DEACTIVATE), `alEvtqPostEvent` +**drops the event silently** → the deactivate never runs → the state never +disposes → the slot never clears. Once both slots are stuck, permanent mute. +The `[VOICES] ... evtq=N/64` probe already in `snd.c` will show queue +saturation; `[SLOTWRITE]` + `[VOICE+]/[VOICE-]` + `sndPlaySfx t=` trace the +two player-gun slots. All three counts (8 voices / 64 states / 64 events) are +byte-match stock GE — so if this is the mechanism it is a port *timing* bug +(the SFX event queue drained too infrequently vs N64's RCP cadence, D204 +family), not a value to bump. + +**Next:** read the user's `build-pc/audiotrace.log` — confirm (1) `evtq` +approaches 64, (2) a `[AUDIOTRACE] sndDeactivate: state=X` with no following +`[VOICE-] state=X` / dispose, (3) two `[SLOTWRITE] slot=` and +`slot=` with no clear afterwards. Then the fix is port-side: drain +the SFX event queue more often, or (cheaper, D202-family-consistent) a +bounded self-heal in the double-buffer — if a slot has pointed at a +non-playing state for > N ms, force-clear it.