Commit Graph

5 Commits

Author SHA1 Message Date
Matthew Stanley 64b084e2a7 load: add opt-in event horizon, warm routes, and audio sink 2026-07-14 22:44:31 -07:00
Matthew Stanley 9306db59b3 cdrom: continuous disc clock — never freeze the read stream on INT ack
The read stream froze read_delay whenever the guest had an unacked
controller INT (and while the sector buffer was undrained). Real drives
never pause: measured on MMX4's attract/title XA music, the freeze
accumulated ~1M guest cycles/second (~3% of disc time), every XA sector
arrived 32-92 samples late, and the XA->SPU cd_ring starved ~147 samples
5-6x per second — audible as a 6 Hz crackle/dropout flutter in all
XA-streamed music (user report: MMX4 Zero ice-stage attract demo).

Faithful model, mirrored from the in-tree Beetle oracle (cdc.cpp
HandleSectorRead / SetAIP / CheckAIP / ClearAIP):

- Disc time NEVER pauses while reading; sectors are read on exact
  cycle schedule. XA-ADPCM realtime audio flows to the SPU decoder
  unconditionally (this alone removes the crackle).
- A data sector that comes due while the guest still has an unacked
  INT overwrites the sector buffer on schedule (hardware clobbers its
  FIFO the same way) and its data-ready INT1 pends ONE deep, presented
  the moment the guest fully acks (Beetle CheckAIP). A newer sector
  replacing an unpresented notification is counted + traced 'P'
  (Beetle "Previous notification skipped").
- Drive-state changes (Read/Play/Pause/Stop/Seek paths through
  start/stop_read_stream) cancel the pended notification (ClearAIP).
- The active-DMA refill path (deliver without new INT while a
  multi-sector CD DMA drains) is unchanged.

Verification on MMX4 (SLUS-00561) title/attract XA:
- CD_PUSH guest-cycle spacing: exactly 1,806,336 cycles (8 sectors at
  2x), min == max over 148 pushes (was 1.83-1.88M, systematically late).
- read_hold_cycles == 0 (was ~30.8M per 30 s) — kept as an always-on
  regression tripwire in cdrom_state.
- int1_pended == 0 during music: X4 acks fast; the pending path only
  arms for genuinely slow acks.
- spu_out/host tap rings: 6 Hz zero-gap storm (86-144 gaps/95 s of
  147-265 samples) eliminated; only real content silences remain.
- cd_underflow_frames stays 0 during playback.

Always-on observability added (ring-buffer doctrine):
- XA zero-run scanner in the decode path -> AUDIO_EV_XA_ZERO events
  (stage 0 = post-ADPCM, stage 1 = post-resample+volume, with LBA).
- CD_PUSH audio events now stamp the global guest-cycle clock (the
  spu_out sample stamp is pump-quantized and cannot measure cadence).
- cdrom_state: read_hold_cycles/read_hold_events (must stay 0),
  int1_pended / int1_lost / int1_pending_now.
- Savestate: pending_dataready fields appended to CDROM_SNAP_FIELDS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 13:55:52 -07:00
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
Matthew Stanley 9cf627113a audio: clock-domain DRC bridge for SPU output (fixes sustained crackle)
Replace the push-model SDL_QueueAudio output with the shared band-limited
polyphase resampler + P-only fill servo (recomp_audio_drc.h), fed by an SPSC
ring under SDL_LockAudioDevice and drained by a pull callback. The SPU renders
44100/60 per wall-clock frame while the host consumes on its own crystal; the
unsynchronized clocks drifted the queue to underrun (silence gaps = sustained
crackle). The bridge nudges the resample ratio <=+/-0.5% to hold the ring near
target so the queue never runs dry.

Measured on MMX6: legacy push ~1.5-2.5 underruns/s (queue draining to 0ms) ->
bridge 0 underruns, fill held ~50ms. PSXRECOMP_AUDIO_LEGACY=1 keeps the old
push path for A/B; PSXRECOMP_AUDIO_PROBE=1 prints the fill/underrun trend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 00:50:55 -07:00
Matthew Stanley 5a5922d98f audio: always-on PCM tap rings + oracle WAV capture (SNES-campaign port)
Measurement infrastructure for the audio-quality campaign (MMX5 complaint),
ported from snesrecomp's audio_trace/oracle methodology and the
_audio_round2 tap model. No behavior change to the audio path itself.

- audio_trace.{c,h}: three always-on 95 s PCM rings — T1 spu_out (canonical
  spu_render output), T2 cd_in (XA/CD-DA PCM entering the SPU CD bus),
  T3 host (exact post-fade bytes handed to SDL_QueueAudio) — plus a 512k
  event ring (every SPU reg write stamped with the SPU sample clock;
  RENDER/PUMP_SKIP/UNDERRUN/MUTE/UNMUTE/CD_PUSH/SPU-DMA), audible-vs-
  silent counters (-42 dBFS gate), queue watermarks, and a server-side
  44100 Hz WAV dumper (rate exactness matters: snesrecomp da33c06).
- psx-beetle: the libretro audio callbacks stop discarding Beetle's fully-
  mixed reference SPU PCM and record it into the same tap ring. Port 4380
  gains audio_stats/audio_wav/audio_events with the identical wire protocol
  as the runtime (rule 16 symmetric harness) — the oracle WAV capture.
- debug_server: audio_stats / audio_wav / audio_events on the runtime port.
- tools: audio_ab_diff.py + audio_spectral_ab.py (drift-tolerant offline
  A/B: drift ppm, onsets, cents, timbre, click rate) and audio_analyze.py
  (first-dirty-tap cross-tap decision tree) ported from snesrecomp/
  _audio_round2; audio_capture_ab.py dumps both processes symmetrically.

Probes query history; nothing is armed at probe time.
2026-07-10 00:00:09 -07:00