mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
6e6a0b3ed8
Closes the four gaps named in issue #103 and lands the separable, verified parts of PR #102. See docs/internal/SPU_FIDELITY_103.md for the per-piece disposition and the oracle-verification queue. SPU DSP (issue #103), clean-room from hardware documentation: - Reverb: the documented 22050 Hz engine — vLIN/vRIN input, same-side and crossed different-side IIR reflections, 4-tap comb early echo, both all-pass filters, vLOUT/vROUT output, and the work-area address advance. Gated on SPUCNT bit 7 for WRITES only; reads, IRQ checks, address advance and output always run, as on hardware. Addressing is confined to [mBASE, 0x80000). Per-voice sends from EON, CD send from SPUCNT bits 0+2. - Noise: 16-bit LFSR with the documented parity feedback, clocked at the SPUCNT bits 8-13 rate; NON voices substitute it for their ADPCM sample. - Volume sweeps: all 24 voice L/R plus main L/R. Bit 15 selects sweep vs direct; sweep is a live envelope on the same rate machinery as ADSR, and a guest read of a sweeping register returns the live level. - Capture buffers: CD L/R and voice 1/3 output written to SPU RAM, so a game that parks the IRQ address there still gets its interrupt. - SPU IRQ (I_STAT bit 9): checked at every SPU RAM access class — FIFO, DMA both directions, ADPCM block fetch, capture writes, reverb work-area accesses — gated on SPUCNT bit 6, mirrored in SPUSTAT bit 6, acked by clearing SPUCNT bit 6. The CD-only fast path in spu_render was removed rather than extended: the SPU now has per-frame work that must run in every path, and an FMV (XA audio with zero active voices) is exactly the CD-with-reverb case. This is a clean-room implementation from the psx-spx/nocash register map and documented algorithm. Beetle's source was deliberately not consulted: it is GPL-2.0-or-later and this project is PolyForm Noncommercial. PR #16 was parked for exactly that reason and PR #13's reverb was separately wrong (it gated on bit 15 of dAPF1, an address offset, instead of SPUCNT bit 7). The one place this knowingly deviates from hardware is the 22.05 -> 44.1 kHz reconstruction filter, which the documentation does not specify; it is isolated in a single function and flagged for oracle comparison. CAUSE.IP2 (from PR #102): IP2 is combinational on R3000A — it mirrors the interrupt line and falls the instant the guest acks I_STAT or masks the source. It was being OR'd in at delivery and never cleared, in TWO places (the compiled path and psx_interpreter.c, the latter missed by PR #102), leaving a phantom pending interrupt that can spin a kernel dispatcher looping on CAUSE.IP & SR.IM. psx_irq_refresh_cause_ip2() is now the sole writer of bit 10, refreshed at raise, I_STAT ack, I_MASK write, HLE context restore and power-on. Verified against the Beetle oracle's irq.cpp/cpu.cpp rather than asserted, and covered by test_cause_ip2_combinational. Mid-dispatch audio pump (from PR #102), with a fix: routed through a gate that mirrors sdl_audio_update's mute/sink decision. Pumping unconditionally from the VBlank edge, as submitted, would have pushed real audio during every turbo-load hard mute and defeated the freeze-in-place mute model. SPU RAM DMA readback (from PR #102): DMA4 in the SPU->RAM direction wrote literal zeros; SPU RAM is readable memory and titles carry state through it across an Exec boundary. Also fixes an unrelated build race found on the way: tools/embed_spirv.py wrote its intermediate SPIR-V to <source>.spv, so the two runtime targets that embed the same shaders raced on one path and one deleted the file the other was reading. Co-Authored-By: Alexandros Mandravillis <Alexbeav@live.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
130 lines
5.7 KiB
C
130 lines
5.7 KiB
C
/*
|
|
* audio_trace.h - always-on audio observability rings (PCM taps + event ring).
|
|
*
|
|
* Port of the snesrecomp audio-campaign measurement harness (audio_trace.{c,h},
|
|
* commit 67ead27 lineage) to the PSX pipeline, following the cross-system tap
|
|
* model in F:/Projects/_audio_round2/ROUND2_PLAN.md: always-on ring buffers
|
|
* recording every sample/event from process start, dumped AFTER the fact via
|
|
* TCP debug commands — never arm-then-reproduce.
|
|
*
|
|
* Taps (PSX pipeline):
|
|
* AUDIO_TAP_SPU_OUT "T1" — canonical spu_render() output @44100, recorded
|
|
* inside spu_render so every caller (pump, fade tail) is
|
|
* covered, PRE any host-side fade/mute gain.
|
|
* AUDIO_TAP_CD_IN "T2" — decoded XA/CD-DA PCM pushed onto the SPU CD
|
|
* input bus (already resampled to 44100 by the producer).
|
|
* AUDIO_TAP_HOST "T3" — the exact bytes handed to the host audio API
|
|
* (post fade ramps; what SDL_QueueAudio receives).
|
|
*
|
|
* The same module compiles into psx-beetle, where AUDIO_TAP_SPU_OUT records
|
|
* the libretro audio_batch callback — Beetle's fully-mixed reference SPU
|
|
* output — giving symmetric `audio_*` commands on ports 4370 and 4380.
|
|
*
|
|
* Threading: each tap has a single writer. Readers (debug-server thread)
|
|
* take a stable [oldest, head) snapshot; the PCM rings hold ~95 s at 44100,
|
|
* so a dump cannot be lapped mid-read in practice. Indices are C11 atomics
|
|
* (release on publish, acquire on read).
|
|
*/
|
|
#ifndef PSX_AUDIO_TRACE_H
|
|
#define PSX_AUDIO_TRACE_H
|
|
|
|
#include <stdint.h>
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
enum {
|
|
AUDIO_TAP_SPU_OUT = 0,
|
|
AUDIO_TAP_CD_IN = 1,
|
|
AUDIO_TAP_HOST = 2,
|
|
AUDIO_TAP_VOICES = 3, /* T0: 24-voice sum only (pre CD mix, pre main) */
|
|
AUDIO_TAP_COUNT = 4
|
|
};
|
|
|
|
/* Event kinds. REG_WRITE covers every SPU register store (addr/value), which
|
|
* subsumes KEYON/KEYOFF/pitch/volume writes — the semantic KON ring stays in
|
|
* spu.c. The rest instrument the host delivery path. */
|
|
enum {
|
|
AUDIO_EV_NONE = 0,
|
|
AUDIO_EV_REG_WRITE = 1, /* a=SPU reg addr, b=value */
|
|
AUDIO_EV_RENDER = 2, /* a=frames rendered, b=host queued bytes before */
|
|
AUDIO_EV_PUMP_SKIP = 3, /* backpressure skip; a=queued bytes */
|
|
AUDIO_EV_UNDERRUN = 4, /* host queue empty at pump entry; a=queued bytes */
|
|
AUDIO_EV_MUTE = 5, /* turbo mute engaged; a=fade tail frames */
|
|
AUDIO_EV_UNMUTE = 6, /* pumping resumed; a=fade-in frames pending */
|
|
AUDIO_EV_CD_PUSH = 7, /* a=guest-cycle clock low32, b=ring fill after */
|
|
AUDIO_EV_DMA_WRITE = 8, /* SPU RAM DMA; a=words, b=transfer addr after */
|
|
AUDIO_EV_XA_ZERO = 9, /* zero-run in decoded XA PCM; a=lba,
|
|
b=(stage<<28)|(start_frame<<14)|run_len.
|
|
stage 0=post-ADPCM-decode (native rate),
|
|
stage 1=post-resample+volume (44100). */
|
|
AUDIO_EV_SINK_DROP = 10, /* turbo host sink; a=guest SPU frames discarded */
|
|
AUDIO_EV_DMA_READ = 11, /* SPU RAM -> CPU DMA; a=words, b=dest RAM addr */
|
|
};
|
|
|
|
typedef struct {
|
|
uint64_t seq; /* monotone event counter */
|
|
uint64_t sample_idx; /* AUDIO_TAP_SPU_OUT frames produced when recorded */
|
|
uint32_t frame; /* host-noted frame counter (vblank / retro_run) */
|
|
uint16_t kind;
|
|
uint16_t reserved;
|
|
uint32_t a;
|
|
uint32_t b;
|
|
} AudioTraceEvent;
|
|
|
|
typedef struct {
|
|
/* Per-tap PCM production. */
|
|
uint64_t tap_frames[AUDIO_TAP_COUNT]; /* frames ever recorded */
|
|
uint64_t tap_nonzero[AUDIO_TAP_COUNT]; /* frames with |L| or |R| > 0 */
|
|
uint64_t tap_audible[AUDIO_TAP_COUNT]; /* frames with |L| or |R| > 256 (~ -42 dBFS) */
|
|
int32_t tap_peak[AUDIO_TAP_COUNT]; /* running peak |sample| */
|
|
/* Host pump behavior. */
|
|
uint64_t pump_calls;
|
|
uint64_t pump_skips; /* backpressure early-outs */
|
|
uint64_t underruns; /* pump entered with 0 queued bytes (post first audio) */
|
|
uint32_t queue_hiwater; /* max queued bytes observed at pump entry */
|
|
uint32_t queue_lowater; /* min queued bytes observed at pump entry (post first audio) */
|
|
uint64_t mute_events;
|
|
uint64_t unmute_events;
|
|
/* Event ring. */
|
|
uint64_t events_total;
|
|
} AudioTraceStats;
|
|
|
|
void audio_trace_init(void);
|
|
|
|
/* Record `frames` interleaved stereo s16 frames into tap `tap`. */
|
|
void audio_trace_pcm(int tap, const int16_t *stereo, int frames);
|
|
|
|
/* Record a pipeline event (timestamps applied internally). */
|
|
void audio_trace_event(uint16_t kind, uint32_t a, uint32_t b);
|
|
|
|
/* Host frame tag for event timestamps (call once per vblank / retro_run). */
|
|
void audio_trace_note_frame(uint32_t frame);
|
|
|
|
void audio_trace_get_stats(AudioTraceStats *out);
|
|
uint64_t audio_trace_tap_total(int tap);
|
|
uint64_t audio_trace_events_total(void);
|
|
|
|
/* Taps default to 44100 Hz; the host tap may run at the device rate in
|
|
* bridge/pull mode. The WAV dumper stamps this rate — a mislabeled header
|
|
* silently poisons every offline A/B. */
|
|
void audio_trace_set_tap_rate(int tap, uint32_t rate);
|
|
uint32_t audio_trace_tap_rate(int tap);
|
|
|
|
/* Copy the most recent `max` events, oldest first. Returns count copied. */
|
|
uint32_t audio_trace_events_get(AudioTraceEvent *out, uint32_t max);
|
|
|
|
/* Write a tap slice as a stereo s16 WAV at the tap's rate. start=-1,count=0
|
|
* dumps the whole currently-buffered ring. `start`/`count` are absolute frame
|
|
* indices on the tap's production timeline. Returns frames written, or -1 on
|
|
* error (unwritable path / empty ring / slice already evicted). */
|
|
int64_t audio_trace_dump_wav(int tap, const char *path,
|
|
int64_t start, uint64_t count);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* PSX_AUDIO_TRACE_H */
|