#ifndef PSXRECOMP_SPU_H #define PSXRECOMP_SPU_H #include #ifdef __cplusplus extern "C" { #endif void spu_init(void); /* Guest-clocked SPU: render every whole sample owed up to the current guest * cycle (768 cycles per 44.1 kHz sample) into the output queue. Every SPU * register/DMA/CD-input access does this first; the host pump calls it, then * drains the queue. */ void spu_catch_up(void); uint32_t spu_output_available(void); uint32_t spu_output_pop(int16_t* out_stereo, uint32_t max_frames); void spu_output_drop(uint32_t frames); /* oldest first */ void spu_output_flush(void); uint64_t spu_output_overflow_drops(void); /* Host gate: while it returns nonzero (rollback resimulation, FMV-skip mute) * rendered samples advance SPU state but are not queued. */ void spu_set_output_gate(int (*suppressed)(void)); typedef struct SpuDebugInfo { uint32_t ctrl; uint32_t active_mask; int16_t main_l; int16_t main_r; int16_t cd_l; int16_t cd_r; uint32_t key_on_count; uint64_t render_frames; uint64_t nonzero_frames; int32_t last_peak; int32_t peak; uint32_t cd_frames; uint64_t cd_push_frames; uint64_t cd_overflow_frames; uint64_t cd_underflow_frames; } SpuDebugInfo; void spu_debug_info(SpuDebugInfo* out); /* ---- Per-voice register snapshot. Mirrors the fields the Beetle oracle * exposes via PS_SPU::GetRegister(GSREG_V0_*) so both backends produce a * structurally-identical spu_voices payload that diff tooling can compare. */ typedef struct SpuVoiceState { int active; /* our internal "is voicing" flag (no ADSR yet) */ uint16_t vol_ctrl_l; /* raw voice reg 0 */ uint16_t vol_ctrl_r; /* raw voice reg 1 */ uint16_t pitch; /* raw voice reg 2 */ uint16_t start_lo; /* raw voice reg 3 (start_addr >> 3) */ uint16_t adsr_lo; /* raw voice reg 5 */ uint16_t adsr_hi; /* raw voice reg 6 */ uint16_t loop_lo; /* raw voice reg 7 (loop_addr >> 3) */ uint32_t cur_addr; /* current decode pointer (byte addr in SPU RAM) */ uint32_t repeat_addr; /* effective loop point (byte addr in SPU RAM) */ uint8_t last_flags; /* flag byte from most recently decoded block */ uint8_t sample_idx; /* position inside the current 28-sample block */ uint16_t phase; /* sub-sample phase counter (0..0x1000) */ uint16_t env_level; /* live ADSR envelope level (0..0x7FFF) */ uint8_t adsr_phase; /* 0=attack 1=decay 2=sustain 3=release */ int16_t vol_cur_l; /* live effective L volume (sweep-aware, full int16) */ int16_t vol_cur_r; /* live effective R volume (sweep-aware, full int16) */ } SpuVoiceState; typedef struct SpuGlobalState { uint16_t ctrl; /* SPUCONT (0x1F801DAA) */ uint16_t main_vol_l; /* raw 0x1F801D80 */ uint16_t main_vol_r; /* raw 0x1F801D82 */ uint32_t kon_latch; /* last 24-bit KEYON value */ uint32_t koff_latch; /* last 24-bit KEYOFF value */ uint32_t pmon; /* pitch mod (0x1F801D90/92) */ uint32_t non; /* noise mode (0x1F801D94/96) */ uint32_t eon; /* reverb mode (0x1F801D98/9A) */ uint32_t endx; /* end-block reached latch (0x1F801D9C/9E) */ uint32_t active_mask; /* recomp-local "still voicing" mask */ /* ---- SPU DSP fidelity state (issue #103: IRQ/reverb/noise/sweeps) ---- */ uint16_t irq_flag; /* SPU IRQ latch (mirrors SPUSTAT bit 6) */ uint16_t reverb_on; /* SPUCNT bit 7 (reverb work-area write enable) */ uint32_t irq_addr; /* IRQ address as a BYTE address (reg 0x1F801DA4 << 3) */ uint32_t reverb_mbase; /* reverb work area start, byte address (mBASE << 3) */ uint32_t reverb_cur; /* current reverb buffer address (byte, advances @22050Hz) */ uint32_t capture_pos; /* capture-buffer write offset (0..0x3FE, wraps at 0x400) */ uint16_t noise_lfsr; /* live noise shift register value */ uint16_t noise_pad; /* reserved / alignment */ uint32_t sweep_l_mask; /* voices whose LEFT volume register is in sweep mode */ uint32_t sweep_r_mask; /* voices whose RIGHT volume register is in sweep mode */ uint32_t sweep_main; /* bit0 = main L in sweep mode, bit1 = main R */ } SpuGlobalState; void spu_get_voice_state(int voice, SpuVoiceState* out); void spu_get_global_state(SpuGlobalState* out); /* SPUCNT (0x1F801DAA) alone. The sample-event scheduler gates on one ctrl bit * per device-service call; building the full SpuGlobalState there (register * sweep + three 24-voice loops) was ~40% of emu-thread time during the Capcom * FMV's MMIO-polling loop (gdb-sampled 2026-09-01). */ uint16_t spu_ctrl_read(void); /* Debug peek into SPU sample RAM (spu_ram TCP command). */ uint32_t spu_ram_peek(uint32_t addr, uint8_t *out, uint32_t len); /* ---- Always-on per-voice event ring. Records KEYON, KEYOFF, voice * end-flag-stop and end-flag-loop events with a frame timestamp. Allocated * at spu_init; never armed/disarmed. Query the window of interest. */ typedef enum { SPU_EV_KEYON = 1, SPU_EV_KEYOFF = 2, SPU_EV_END_STOP = 3, /* loop_end without repeat → voice silenced */ SPU_EV_END_LOOP = 4, /* loop_end with repeat → cur_addr=repeat_addr */ SPU_EV_IRQ = 5 /* SPU RAM IRQ-address hit → I_STAT bit 9 raised; voice=0xFF (not voice-attributable), addr=byte address that matched */ } SpuEventKind; typedef struct SpuEvent { uint64_t seq; uint32_t frame; uint8_t kind; uint8_t voice; uint16_t pitch; uint32_t addr; /* start_addr for KEYON; cur_addr for KEYOFF/END_*; repeat_addr for END_LOOP */ uint16_t adsr_lo; uint16_t adsr_hi; uint16_t vol_l; uint16_t vol_r; } SpuEvent; uint64_t spu_event_total(void); uint32_t spu_event_get(SpuEvent* out, uint32_t max_count); /* most recent up to max_count */ void spu_event_reset(void); /* MMIO read/write (0x1F801C00-0x1F801FFF) */ uint32_t spu_read(uint32_t addr); void spu_write(uint32_t addr, uint32_t value); /* DMA channel 4 interface. spu_dma_read reads one 32-bit word from SPU RAM * at the current transfer address, advances the address by 4, and runs the * SPU IRQ-address check (SPU RAM -> CPU direction, DICR direction bit 0). */ void spu_dma_write(uint32_t word); uint32_t spu_dma_read(void); int spu_dma_ready(void); /* CD-ROM XA/CDDA input path. Samples are stereo 44.1 kHz PCM entering the * SPU CD input bus; SPU control bit 0 and CD volume registers gate output. */ void spu_cd_audio_push(const int16_t* stereo, int frames); void spu_cd_audio_reset(void); /* Get pointer to SPU RAM for direct access (512KB) */ const uint8_t* spu_get_ram(void); /* Opt-in enhancement assets. Bank 0 is hardware RAM; immutable ADPCM banks * 1..31 reside outside it. A binding is consumed at the next KEYON on that * voice, preserving the native decoder, pitch, envelopes, mixer and KEYOFF. * Bank addresses are byte offsets (the voice registers still use /8 units). * Both active and pending bindings are part of the SPU snapshot wire. */ int spu_register_sample_bank(uint32_t bank, const void *adpcm, uint32_t bytes); int spu_bind_next_voice_bank(unsigned voice, uint32_t bank); /* Optional title-owned voices, independent of all 24 hardware channels. * Register order is VOL_L, VOL_R, PITCH, START, ADSR_LO, ADSR_HI, ENV, LOOP. * Mode bits: 0 noise, 1 reverb. Slots 0..23; nonzero immutable bank required. * Decoder/envelope/sweep state is serialized; ordinary snapshots retain their * existing size until this enhancement is used. */ int spu_private_voice_play(unsigned slot,uint32_t bank,const uint16_t regs[8],unsigned mode); void spu_private_voice_stop(uint32_t mask); int spu_private_voice_volume(unsigned slot,uint16_t left,uint16_t right); uint32_t spu_private_voice_active(void); /* ---- Verified-enhancement shadow tap (consumed by spu_shadow.c) --------- * * Present-time, opt-in float SPU re-render. When the shadow is enabled, * spu_render fills a per-output-frame, per-voice tap with the exact inputs the * canon used (the 4 decoded samples bracketing the sub-sample phase, the * fractional phase, envelope level, and per-voice + main volumes). The shadow * re-interpolates these in float. The layout MUST match spu.c's internal * SpuShadowVoiceTap / SpuShadowFrameTap. */ #define SPU_SHADOW_MAX_VOICES 48 typedef struct { int16_t s[4]; /* decoded samples sample_idx-1 .. +2 (block-edge clamped) */ float frac; /* fractional sub-sample phase in [0,1) */ uint16_t env; /* env_level (0..0x7FFF) */ int16_t vol_l; /* per-voice L volume, 1.14 scale (effective int16 >> 1) */ int16_t vol_r; /* per-voice R volume, same scale */ uint8_t active; } SpuShadowVoiceTapPub; typedef struct { SpuShadowVoiceTapPub voice[SPU_SHADOW_MAX_VOICES]; int16_t main_l; int16_t main_r; int enabled; } SpuShadowFrameTapPub; /* Pointer to the tap array (SpuShadowFrameTapPub[]) and the number of valid * frames filled by the most recent spu_render block. */ const void* spu_shadow_tap_buffer(void); int spu_shadow_tap_count(void); uint32_t spu_snapshot_bytes(void); int spu_snapshot_len_ok(uint32_t len); /* Optional host-owned implementation, not serialized. The caller owns any * mutable playback state and restores it with its guest snapshot. */ void spu_set_pcm_overlay(void (*callback)(int16_t* stereo, uint32_t frames)); void spu_snapshot_write(uint8_t *p); int spu_snapshot_read(const uint8_t *p, uint32_t len); uint8_t *spu_get_ram_ptr(void); uint32_t spu_get_ram_bytes(void); /* Split digests of the snap wire (regs / voices / DSP tail) for Win↔Linux * aux bisect. PSX_RB_SPU_PARTS=1 prints these on rb live dig. */ typedef struct SpuSnapPartDigests { uint32_t regs; uint32_t voices; uint32_t tail; } SpuSnapPartDigests; void spu_snapshot_part_digests(SpuSnapPartDigests *out); #ifdef __cplusplus } #endif #endif /* PSXRECOMP_SPU_H */