Files
psxrecomp/runtime/include/audio_trace.h
T
Matthew Stanley 8dbe04e6c5 spu/cdrom: live voice loop-address + CD-audio volume matrix (X5 +11dB fix)
Two hardware-faithfulness fixes found by the always-on tap rings + Beetle
oracle A/B on fingerprint-matched attract music (tools/audio_pair_segments):
the recomp played the identical track at -3.8 dBFS vs the oracle's
-14.1 dBFS — +10.3 dB hot, clipping continuously.

1. spu.c: the voice repeat/loop address register (voice reg 7) is LIVE
   state on real hardware — writing it after KEYON retargets where the
   next END+REPEAT block jumps (Beetle spu.cpp:1150/333), and the block
   auto-latch (flag bit 2) updates the guest-visible register (386/1302).
   The recomp cached repeat_addr at KEYON only, so X5's driver idiom —
   KEYON, then point the loop register at a silent tail block; no KEYOFF
   ever — left voices looping the LOUD sample body forever. Measured: 3
   stuck voices re-looping ~1400x/s, 160k SPU events/min vs the oracle's
   633, zero game KEYOFF writes (the game never needs one), +11 dB.

2. cdrom.c: implement the CD controller's decode volume matrix
   ([src][port], 0x80 unity, pending banks + apply latch at 1F801803.3
   bit 5 — mapping copied from Beetle cdc.cpp:1483-1501, applied after
   the XA resample per PS1 hardware tests). X5 drives it constantly:
   0x73-0x7E steady, ramped to 0x00 at scene changes (measured via the
   new beetle `cdc_volume` command + PS_CDC::PSXRecomp_GetDecodeVolume
   peek). Without it music fades never happened and steady level sat
   ~0.6 dB hot.

Also: AUDIO_TAP_VOICES (T0 voice-sum tap) for source-by-source mix
decomposition, per-tap WAV rates, bridge stats in audio_stats.

Measured after (MMX5 attract, 20-25 s windows):
  spu_out RMS: -3.8 dBFS (clipping)  ->  -15.5 dBFS   [oracle -14.1]
  music-only chain already matched the oracle at -14.8 dBFS, confirming
  decoder + mixer gain equivalence; the entire excess was stuck voices.
  underruns: 2 events / 98 s (delivery fix holding).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 01:54:46 -07:00

124 lines
5.2 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=frames pushed, b=CD ring fill after */
AUDIO_EV_DMA_WRITE = 8, /* SPU RAM DMA; a=words, b=transfer addr after */
};
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 */