forked from Ports/goldeneye-pc-port
- findings.md: D202 §M-67 (static analysis exhausted, GE_VOICEDUMP probe motivation), §M-68 (decisive runtime A/B — all three user complaint classes play the exact ROM samples at the exact ROM pitches) and new §M-69 (320-request full-corpus exact match: 286/320 CC >= 0.85; the initial 44 'BAD' were tooling artifacts — voicedump stamps are block-END times, coarse-slide missed sharp onsets, refs unclipped at voice death; remaining failures = dropped allocations / shared-wavetable wire attribution, not corruption) + index row. - GE-ENV-PROBES.md: GE_VOICEDUMP entry (incl. block-END-stamp gotcha); [WIRE]/[VOICEMAP]/[EVT]/[STOP-EVT] documented under GE_AUDIOTRACE. - AUDIO-PLAN.md: status banner — Phases 0-2 done (D199/D200/D201/D204), Phase 3 verification is the active track.
203 lines
11 KiB
Markdown
203 lines
11 KiB
Markdown
# AUDIO-PLAN — Phase 3 / ROADMAP B3 (libaudio → SDL)
|
||
|
||
Status: plan of record for the audio track, 2026-09-05. Companion to
|
||
`ROADMAP-1.0.md` §B3 ("audio (Phase 3)", "audio track parallel from M-50 →
|
||
v0.4.0"). Independent of B1/B2 — can start immediately.
|
||
|
||
**Status update (M-69):** Phases 0–2 below are DONE — the mixer landed as
|
||
D199 (`port/src/mixer.c` macro-swap), D200/D201 fixed the two pointer-width
|
||
landmines it exposed, and D204 fixed a permanent ~2 % tempo drift. Phase 3
|
||
(verification) is now the active track: D202's sample-correctness question is
|
||
closed negative through M-69 (full-corpus per-voice exact match — 286/320
|
||
requests CC ≥ 0.85, every allocated voice plays the exact ROM sample at the
|
||
exact ROM pitch, no corruption). Remaining: user by-ear pass + N64 A/B on the
|
||
door-loop cap (M-66b), then D202 probe removal + close; reverb/mix-balance
|
||
tuning is still owed. See findings.md D199–D204.
|
||
|
||
---
|
||
|
||
## Where things stand (verified 2026-09-05)
|
||
|
||
More is already in place than "audio not started" suggests:
|
||
|
||
- **The whole CPU-side audio engine already compiles and runs.**
|
||
`src/audi.c`, `src/snd.c`, `src/music.c` are in `SRC_ENGINE`; IDO libaudio
|
||
(`src/libultra/audio/*.c`) + Rare's naudio "New" driver
|
||
(`src/libultrare/audio/*.c`: drvrNew/env/reverb) are in the link. The
|
||
`amMain` audio thread runs per VI-retrace; D54/D54b already killed its
|
||
SIGSEGVs — so synthesis bookkeeping is live (D77: "music runs in code but
|
||
no audible output").
|
||
- **The scheduler side needs zero changes.** `osSpTaskStartGo`
|
||
(`port/src/libultra.c:1237`) already has an M_AUDTASK branch that completes
|
||
the task and posts `OS_EVENT_SP`, so `amMain`'s reply-message handshake
|
||
works. The branch is currently a no-op — exactly where the audio ucode's
|
||
job used to go.
|
||
- **The two TODO stubs are the actual gap:** `osAiSetNextBuffer()` /
|
||
`osAiGetLength()` in `port/src/libultra.c:852-862` discard everything
|
||
(`osAiGetLength` returns 0), and `port/src/audio.c` never opens an SDL
|
||
device. `main.c:141-142` already calls `audioInit()` / `mixerInit()`.
|
||
- **The PD reference is local** at `C:\Users\james\Source\Repos\pd_port`,
|
||
with the full audio stack: `port/src/audio.c` (75 lines, SDL device +
|
||
queue), `port/src/mixer.c` (~720 lines, software RSP-audio-ucode), and the
|
||
wiring trick in `include/PR/abi.h`.
|
||
|
||
## Architecture — PD's macro-swap trick, adapted
|
||
|
||
On N64, `alAudioFrame()` only *builds* an acmd list; the RSP audio ucode
|
||
(`aspMain`) executes it. PD's PC port never runs that ucode: under
|
||
`#ifndef PLATFORM_N64`, `include/PR/abi.h` swaps each acmd-building macro
|
||
`aXxx(pkt, ...)` for a call to a CPU implementation (`aXxxImpl`) operating on
|
||
a software DMEM array. libaudio/naudio compile unmodified; every "command"
|
||
executes inline at build time; the M_AUDTASK that reaches the scheduler is an
|
||
empty formality (PD's `pdsched.c` audio branch is literally empty).
|
||
|
||
**Critical catch — GE and PD use different acmd ABIs** (the AGENTS.md
|
||
"same family ≠ identical format" case):
|
||
|
||
| | GE (`include/PR/abi.h`) | PD (naudio "New") |
|
||
|---|---|---|
|
||
| `aADPCMdec` | `(pkt, flags, statePtr)` — params from an `aSetBuffer` context + `ADPCM_STATE[16]` | `(pkt, state, flags, count, inofs, outofs)` — inline args |
|
||
| `aLoadBuffer` / `aSaveBuffer` | `(pkt, dramAddr)` only — dest/count from the `aSetBuffer` context | explicit dest + count args |
|
||
| `aInterleave` | `(pkt, lDmem, rDmem)` | no args |
|
||
| Opcodes | 16: adds `aSetBuffer`, `aPan`, `aSegment`; no `aDisable` / `aPlayMP3` | 15 incl. MP3 (PD-specific) |
|
||
|
||
GE's context model (from the call sites in `src/libultra/audio/`):
|
||
`aSetBuffer(flags, dmemin, dmemout, count)` sets a working-buffer context;
|
||
subsequent `aLoadBuffer(dramAddr)` / `aADPCMdec(flags, statePtr)` /
|
||
`aResample(flags, pitch, statePtr)` operate within it; the tail is
|
||
`aInterleave(AL_MAIN_L_OUT, AL_MAIN_R_OUT)` → `aSaveBuffer(f->dramout)`.
|
||
|
||
So this is **not** "copy `mixer.c`": port PD's mixer as the *DSP algorithm
|
||
reference* (ADPCM decode, linear resample, mix math, env-mixer / pole-filter
|
||
are the same Rare/Nintendo DSP blocks) and write GE-ABI wrappers that
|
||
maintain the `aSetBuffer` context and read params from the state structs.
|
||
|
||
**GE's full emittable opcode set** (static audit of every call site in
|
||
`src/libultra/audio/*.c` + `src/libultrare/audio/*.c`): ClearBuffer,
|
||
LoadADPCM, DMEMMove, SetBuffer, LoadBuffer, SetLoop, ADPCMdec, Resample,
|
||
Interleave, SaveBuffer, Mix, SetVolume, EnvMixer, PoleFilter, Segment — plus
|
||
`aPan`, defined in abi.h but apparently uncalled. PD's mixer covers the DSP
|
||
core for all of them except the trivial bookkeeping ones: SetBuffer /
|
||
LoadBuffer / SaveBuffer are context tracking + memcpys; Segment is a no-op on
|
||
PC (`amDmaCallback` already resolves ROM addresses against the mapped image —
|
||
`osVirtualToPhysical` is identity, `libultra.c:1295`).
|
||
|
||
## Phases
|
||
|
||
### Phase 0 — Ground truth (research, ~half day, delegable)
|
||
|
||
1. **Disassemble GE's `aspMain` microcode from the ROM** and document each
|
||
opcode's exact semantics in findings.md §F (new Dxx): flag-bit meanings
|
||
(`A_FIRST`, `A_CONTINUE`, …), count units (bytes vs samples), where the
|
||
interleaved output lands before `aSaveBuffer`, how `aSetLoop` state gets
|
||
restored, `ENVMIX_STATE[40]` layout. This is the audio analogue of using
|
||
`rsp/graphics/gmain.s` as GBI ground truth — we don't run the ucode, but
|
||
it is the spec. (`port/src/ucode.c`'s header already flags this "DECISION
|
||
NEEDED in Phase 3"; CPU-bypass means the dummy `aspMainTextStart` /
|
||
`aspMainDataStart` markers stay forever — document that decision there.)
|
||
2. **Opcode inventory probe** (port-only, zero game-code change): a
|
||
`GE_AUDIOTRACE` env var in the existing M_AUDTASK branch of
|
||
`osSpTaskStartGo` that walks `t->t.data_ptr` for `data_size/8` words and
|
||
histograms `w0 >> 24`. Confirms the static call-site audit and catches
|
||
anything emitted dynamically.
|
||
|
||
### Phase 1 — Device plumbing (small, lead work)
|
||
|
||
- Implement `port/src/audio.c::audioInit`: SDL device at **22050 Hz** (GE's
|
||
`OUTPUT_RATE = 0x5622`, not PD's 22020), `AUDIO_S16SYS`, stereo. Keep the
|
||
existing `Audio.BufferSize` / `Audio.QueueLimit` config registrations.
|
||
- Wire `osAiSetNextBuffer` → `SDL_QueueAudio` guarded by `queueLimit`;
|
||
`osAiGetLength` → `SDL_GetQueuedAudioSize()`. Faithful mapping: N64's
|
||
`AI_LEN_REG` ("bytes synthesized but not yet played") ≙ SDL queued-bytes,
|
||
and the game's self-tuning `frameSamples = g_FrameSize −
|
||
osAiGetLength()>>2` math (`audi.c:527`) keeps the virtual FIFO at ~736
|
||
samples (NTSC) with no game-code changes. Queue inline in
|
||
`osAiSetNextBuffer` (same thread, once per frame) rather than PD's deferred
|
||
`audioEndFrame`. Overflow safety is already self-limiting: a full queue
|
||
makes `frameSamples` clamp to `g_MinFrameSize`, and the `queueLimit` check
|
||
drops rather than crashes.
|
||
- Add a `GE_AUDIODEBUG` probe logging queue depth + synthesized samples every
|
||
60 frames; catalog it in `docs/GE-ENV-PROBES.md`.
|
||
- **Exit:** links 243/243, `/linkcheck` clean; headless `-level_09` 60 s
|
||
crash-free; debug probe shows the queue filling while intro music "plays"
|
||
(still silent — acmds don't execute yet).
|
||
|
||
### Phase 2 — The mixer (core work, ~1–3 days)
|
||
|
||
- Port PD's DSP cores into `port/src/mixer.c`: ADPCM decode (coefficient-book
|
||
handling via `aLoadADPCM`), linear resampler, stereo mix with `0x8000`
|
||
scaling, env-mixer + pole-filter (reverb/chorus driven by
|
||
`CUSTOM_FX_PARAMS_N`, `audi.c:169`), set-volume ramps. Add the GE-ABI
|
||
context layer: a static `{dmemin, dmemout, count}` updated by
|
||
`aSetBuffer`, consumed by LoadBuffer / SaveBuffer / ADPCMdec / Resample; a
|
||
shared software DMEM array across Impls. Replace the current 31-line stub
|
||
interface (`port/include/mixer.h`) with PD-style `aXxxImpl` declarations.
|
||
- Edit `include/PR/abi.h` with an `#ifdef PORT` split (N64 path
|
||
byte-identical): PC includes a port `mixer.h` whose macros match **GE's**
|
||
arg arities. This is the one non-`port/` edit — same documented
|
||
ABI/layout-exception class as D78 / D135, logged in §F with its own label.
|
||
- Unknown / unused opcodes (`aPan`, `aSegment`): log-and-no-op under
|
||
`GE_AUDIOTRACE`; implement only if Phase 0 shows them live.
|
||
- All DSP runs on the existing `amMain` thread inside `alAudioFrame` — no new
|
||
locks, no scheduler changes.
|
||
- **Exit:** opcode histogram fully covered; headless run clean; queue probe
|
||
non-zero.
|
||
|
||
### Phase 3 — Verification & tuning (user-gated listen tests)
|
||
|
||
- Listen checklist: menu music → briefing → in-level music transitions →
|
||
gunfire / explosions → **positional SFX** (walk around a source) → weapon
|
||
one-shots → level changes → **mission-fail fade** (the D152 `s_imLock`
|
||
scenario — retest killing Trevelyan in Facility with live audio, since the
|
||
fade hammers the event queue; the D152 self-healing lock mitigation is
|
||
already in place but must be re-proven under load).
|
||
- Compare reverb / chorus character against N64 reference footage (that's
|
||
what `CUSTOM_FX_PARAMS_N` is for).
|
||
- Regression: `GE_PCDUMP` framediff vs goldens unchanged (audio must not
|
||
touch rendering) + 60 s `-level_09` soak.
|
||
- Docs: findings §F entry, porting-notes quirk ("classic-libultra vs
|
||
naudio-New acmd ABI delta"), ROADMAP B3 status, HANDOFF update.
|
||
|
||
## Risks
|
||
|
||
1. **Ucode-semantics mismatch** (count units, flag bits) → garbled or
|
||
wrong-tinted audio, not crashes; mitigated by Phase 0 disassembly +
|
||
iterative listen checks. Main schedule risk.
|
||
2. **Timing coupling with B1** (D193 family): low — audio frame size is
|
||
feedback-driven off `osAiGetLength`, robust to scheduler jitter; re-verify
|
||
after B1 lands.
|
||
3. **Performance:** ~736 samples/frame of CPU DSP is trivial on x86-64 (PD
|
||
shipped exactly this).
|
||
4. **D152 lock storm under live audio** — covered by the explicit Phase 3
|
||
retest.
|
||
|
||
## Delegation shape & effort
|
||
|
||
Per `docs/dev-process.md`:
|
||
|
||
- Phase 0 disassembly → Claude research subagent (judgment-heavy, read-only +
|
||
findings write-up).
|
||
- Phase 1 → lead or mechanical worker (build-verifiable).
|
||
- Phase 2 DSP-core port → candidate for the local Qwen once the semantics doc
|
||
exists (mechanical, diff-checkable against PD), with the ABI mapping and
|
||
review kept by the lead.
|
||
- Phase 3 listen tests → user.
|
||
|
||
Total ~3–6 focused sessions. Milestone: ROADMAP "audio track (parallel from
|
||
M-50) → v0.4.0".
|
||
|
||
## Key file map
|
||
|
||
| File | Role in this plan |
|
||
|---|---|
|
||
| `src/audi.c` | Game audio manager — compiles unmodified; source of `OUTPUT_RATE`, frame-size math, `CUSTOM_FX_PARAMS_N` |
|
||
| `include/PR/abi.h` | GE's acmd accessors (16 opcodes, context ABI) — the one non-`port/` edit (`#ifdef PORT` split) |
|
||
| `src/libultra/audio/*.c`, `src/libultrare/audio/*.c` | CPU synth + naudio New driver — compile unmodified; call sites define the opcode set |
|
||
| `port/src/audio.c` | SDL device + queue (implement Phase 1) |
|
||
| `port/src/mixer.c` | Software acmd interpreter + software DMEM (implement Phase 2) |
|
||
| `port/include/mixer.h` | Stub interface → replace with `aXxxImpl` declarations |
|
||
| `port/src/libultra.c:848-863` | `osAi*` shims — wire to audio.c (Phase 1) |
|
||
| `port/src/libultra.c:1237` | M_AUDTASK branch — stays a completing no-op; hosts `GE_AUDIOTRACE` |
|
||
| `port/src/ucode.c` | Dummy `aspMain*` markers stay forever under CPU-bypass — document decision |
|
||
| `C:\Users\james\Source\Repos\pd_port\port\src\{audio,mixer}.c`, `include\PR\abi.h` | PD ground truth: device glue + DSP cores + the macro-swap pattern |
|