mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 02:56:36 -04:00
675 lines
24 KiB
C
675 lines
24 KiB
C
/*
|
||
* spu.c - PS1 Sound Processing Unit register and direct ADPCM voice model.
|
||
*
|
||
* This is intentionally still a compact hardware model: it accepts SPU
|
||
* register reads/writes, DMA4 transfers into 512KB SPU RAM, mixes the
|
||
* 24 direct ADPCM voices, and accepts decoded CD/XA audio on the SPU CD
|
||
* input bus. Reverb, noise, sweep volumes, and IRQ timing are not modeled yet.
|
||
*/
|
||
|
||
#include "spu.h"
|
||
|
||
#include <string.h>
|
||
|
||
#define SPU_RAM_SIZE (512 * 1024)
|
||
#define SPU_REG_COUNT 256
|
||
#define SPU_VOICE_COUNT 24
|
||
#define SPU_BLOCK_SAMPLES 28
|
||
|
||
static uint8_t spu_ram[SPU_RAM_SIZE];
|
||
static uint16_t spu_regs[SPU_REG_COUNT];
|
||
static uint32_t transfer_addr;
|
||
static uint32_t key_on_count;
|
||
static uint64_t render_frames;
|
||
static uint64_t nonzero_frames;
|
||
static int32_t last_peak;
|
||
static int32_t peak;
|
||
|
||
/* End-block-reached latch (SPU register 0x1F801D9C/D9E on real hw).
|
||
* Set when a voice decodes a block whose flag byte has bit 0 (loop end).
|
||
* Polled by music engines to know when a one-shot voice has finished. */
|
||
static uint32_t endx_latch;
|
||
|
||
/* KEYON/KEYOFF latches: most recent values written, sticky across reads
|
||
* so debug snapshots always see what the BIOS last requested even if the
|
||
* BIOS clears them quickly. */
|
||
static uint32_t kon_latch;
|
||
static uint32_t koff_latch;
|
||
|
||
/* External vblank counter (debug_server.c) used as event timestamp. */
|
||
extern uint64_t s_frame_count;
|
||
|
||
/* ---- Always-on event ring -------------------------------------------- */
|
||
/* 1M entries × ~32B = 32 MB. Power of 2 so wrap is a mask. Beetle peaks at
|
||
* a few hundred audible-voice events per chime second; recomp at <1k total
|
||
* per chime. 1M gives ~minutes of headroom for game-scene capture too. */
|
||
#define SPU_EVENT_CAP (1u << 20)
|
||
static SpuEvent s_events[SPU_EVENT_CAP];
|
||
static uint32_t s_event_idx = 0;
|
||
static uint64_t s_event_seq = 0;
|
||
|
||
/* CD input FIFO, fed by the CD-ROM XA decoder at 44.1 kHz stereo. */
|
||
#define SPU_CD_RING_FRAMES (44100u * 8u)
|
||
static int16_t cd_ring[SPU_CD_RING_FRAMES * 2u];
|
||
static uint32_t cd_read_pos;
|
||
static uint32_t cd_write_pos;
|
||
static uint32_t cd_frame_count;
|
||
static uint64_t cd_push_frames;
|
||
static uint64_t cd_overflow_frames;
|
||
static uint64_t cd_underflow_frames;
|
||
|
||
/* ADSR phases — match Beetle's order so cross-process diffs read straight. */
|
||
#define ADSR_ATTACK 0
|
||
#define ADSR_DECAY 1
|
||
#define ADSR_SUSTAIN 2
|
||
#define ADSR_RELEASE 3
|
||
|
||
typedef struct {
|
||
int active;
|
||
uint32_t cur_addr;
|
||
uint32_t repeat_addr;
|
||
int16_t samples[SPU_BLOCK_SAMPLES];
|
||
int sample_idx;
|
||
uint32_t phase;
|
||
int16_t hist1;
|
||
int16_t hist2;
|
||
uint8_t flags;
|
||
|
||
/* ADSR envelope state. Stepped once per output sample (44.1 kHz). */
|
||
uint16_t env_level; /* 0..0x7FFF — applied to raw decoded sample */
|
||
uint32_t adsr_divider; /* fixed-point counter; level updates on overflow */
|
||
uint8_t adsr_phase; /* ADSR_ATTACK / DECAY / SUSTAIN / RELEASE */
|
||
} SpuVoice;
|
||
|
||
static SpuVoice voices[SPU_VOICE_COUNT];
|
||
|
||
static void spu_event_record(uint8_t kind, int voice, uint32_t addr) {
|
||
SpuEvent *e = &s_events[s_event_idx & (SPU_EVENT_CAP - 1u)];
|
||
e->seq = s_event_seq++;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
e->kind = kind;
|
||
e->voice = (uint8_t)voice;
|
||
e->pitch = spu_regs[(uint32_t)voice * 8u + 2u];
|
||
e->addr = addr;
|
||
/* PSX voice block layout (16-bit register indices from voice base):
|
||
* 0=VOL_L 1=VOL_R 2=PITCH 3=START 4=ADSR_LO 5=ADSR_HI 6=CURVOL 7=LOOP */
|
||
e->adsr_lo = spu_regs[(uint32_t)voice * 8u + 4u];
|
||
e->adsr_hi = spu_regs[(uint32_t)voice * 8u + 5u];
|
||
e->vol_l = spu_regs[(uint32_t)voice * 8u + 0u];
|
||
e->vol_r = spu_regs[(uint32_t)voice * 8u + 1u];
|
||
s_event_idx++;
|
||
}
|
||
|
||
/* PS1 envelope rate decoder. Ported verbatim from Beetle's CalcVCDelta
|
||
* (beetle-psx/mednafen/psx/spu.cpp). Each call emits the per-step
|
||
* `increment` to add to env_level, and `divinco` added to a divider —
|
||
* level is updated only when divider crosses 0x8000. The combination
|
||
* encodes both linear and pseudo-exponential ramps at PSX-faithful
|
||
* rates (rates 0..127 span ~0.1 ms .. ~30+ s). */
|
||
static void calc_vc_delta(uint8_t zs, uint8_t speed, int log_mode, int dec_mode,
|
||
int inv_increment, int16_t current,
|
||
int *out_increment, int *out_divinco)
|
||
{
|
||
int increment = (7 - (speed & 0x3));
|
||
if (inv_increment) increment = ~increment;
|
||
int divinco = 32768;
|
||
|
||
if (speed < 0x2C)
|
||
increment = (unsigned)increment << ((0x2F - speed) >> 2);
|
||
if (speed >= 0x30)
|
||
divinco >>= (speed - 0x2C) >> 2;
|
||
|
||
if (log_mode) {
|
||
if (dec_mode) {
|
||
increment = (current * increment) >> 15;
|
||
} else if ((current & 0x7FFF) >= 0x6000) {
|
||
if (speed < 0x28) {
|
||
increment >>= 2;
|
||
} else if (speed >= 0x2C) {
|
||
divinco >>= 2;
|
||
} else {
|
||
increment >>= 1;
|
||
divinco >>= 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (divinco == 0 && speed < zs) divinco = 1;
|
||
|
||
*out_increment = increment;
|
||
*out_divinco = divinco;
|
||
}
|
||
|
||
/* Step ADSR envelope by one output sample for voice `idx`. Mirrors
|
||
* Beetle's PS_SPU::RunEnvelope. */
|
||
static void adsr_run(int idx, SpuVoice *v) {
|
||
uint32_t raw = (uint32_t)spu_regs[(uint32_t)idx * 8u + 4u]
|
||
| ((uint32_t)spu_regs[(uint32_t)idx * 8u + 5u] << 16);
|
||
|
||
int Sl = (int)(raw >> 0) & 0x0F;
|
||
int Dr = (int)(raw >> 4) & 0x0F;
|
||
int Ar = (int)(raw >> 8) & 0x7F;
|
||
int attack_exp = (int)((raw >> 15) & 1);
|
||
int Rr = (int)(raw >> 16) & 0x1F;
|
||
int release_exp = (int)((raw >> 21) & 1);
|
||
int Sr = (int)(raw >> 22) & 0x7F;
|
||
int sustain_dec = (int)((raw >> 30) & 1);
|
||
int sustain_exp = (int)((raw >> 31) & 1);
|
||
int sustain_lvl = (Sl + 1) << 11;
|
||
|
||
/* Attack tops out at 0x7FFF — switch to Decay (Beetle does this
|
||
* before the switch on Phase). */
|
||
if (v->adsr_phase == ADSR_ATTACK && v->env_level == 0x7FFF)
|
||
v->adsr_phase = ADSR_DECAY;
|
||
|
||
int increment = 0, divinco = 0;
|
||
int16_t uoflow_reset = 0;
|
||
|
||
switch (v->adsr_phase) {
|
||
case ADSR_ATTACK:
|
||
calc_vc_delta(0x7F, (uint8_t)Ar, attack_exp, 0, 0,
|
||
(int16_t)v->env_level, &increment, &divinco);
|
||
uoflow_reset = 0x7FFF;
|
||
break;
|
||
case ADSR_DECAY:
|
||
calc_vc_delta(0x1F << 2, (uint8_t)(Dr << 2), 1, 1, 1,
|
||
(int16_t)v->env_level, &increment, &divinco);
|
||
uoflow_reset = 0;
|
||
break;
|
||
case ADSR_SUSTAIN:
|
||
calc_vc_delta(0x7F, (uint8_t)Sr, sustain_exp, sustain_dec, sustain_dec,
|
||
(int16_t)v->env_level, &increment, &divinco);
|
||
uoflow_reset = sustain_dec ? 0 : 0x7FFF;
|
||
break;
|
||
case ADSR_RELEASE:
|
||
calc_vc_delta(0x1F << 2, (uint8_t)(Rr << 2), release_exp, 1, 1,
|
||
(int16_t)v->env_level, &increment, &divinco);
|
||
uoflow_reset = 0;
|
||
break;
|
||
default:
|
||
return;
|
||
}
|
||
|
||
v->adsr_divider += (uint32_t)divinco;
|
||
if (v->adsr_divider & 0x8000u) {
|
||
uint16_t prev = v->env_level;
|
||
v->adsr_divider = 0;
|
||
v->env_level = (uint16_t)((int)v->env_level + increment);
|
||
|
||
if (v->adsr_phase == ADSR_ATTACK) {
|
||
/* If high bit just rolled over (0→1), clamp to uoflow_reset. */
|
||
if (((prev ^ v->env_level) & v->env_level) & 0x8000u)
|
||
v->env_level = (uint16_t)uoflow_reset;
|
||
} else {
|
||
if (v->env_level & 0x8000u)
|
||
v->env_level = (uint16_t)uoflow_reset;
|
||
}
|
||
|
||
if (v->adsr_phase == ADSR_DECAY && v->env_level < (uint16_t)sustain_lvl)
|
||
v->adsr_phase = ADSR_SUSTAIN;
|
||
}
|
||
}
|
||
|
||
static inline int16_t clamp16(int32_t v) {
|
||
if (v > 32767) return 32767;
|
||
if (v < -32768) return -32768;
|
||
return (int16_t)v;
|
||
}
|
||
|
||
static inline int32_t abs32(int32_t v) {
|
||
return v < 0 ? -v : v;
|
||
}
|
||
|
||
static inline uint32_t reg_index(uint32_t addr) {
|
||
return (addr - 0x1F801C00u) >> 1;
|
||
}
|
||
|
||
static inline uint16_t voice_reg(int voice, int reg) {
|
||
return spu_regs[(uint32_t)voice * 8u + (uint32_t)reg];
|
||
}
|
||
|
||
static inline int16_t direct_volume(uint16_t raw) {
|
||
int32_t v;
|
||
if (raw & 0x8000u) {
|
||
/* Sweep mode is not modeled; use the magnitude as a direct volume. */
|
||
v = (int32_t)(raw & 0x7FFFu);
|
||
} else {
|
||
v = (int32_t)(raw & 0x7FFFu);
|
||
if (v & 0x4000) v -= 0x8000;
|
||
}
|
||
if (v > 0x3FFF) v = 0x3FFF;
|
||
if (v < -0x4000) v = -0x4000;
|
||
return (int16_t)v;
|
||
}
|
||
|
||
static inline int16_t cd_input_volume(uint16_t raw) {
|
||
/* CD input volume registers use signed 16-bit linear gain; games commonly
|
||
* program 0x7FFF for full-scale CD audio. */
|
||
return (int16_t)raw;
|
||
}
|
||
|
||
void spu_cd_audio_reset(void) {
|
||
memset(cd_ring, 0, sizeof(cd_ring));
|
||
cd_read_pos = 0;
|
||
cd_write_pos = 0;
|
||
cd_frame_count = 0;
|
||
cd_push_frames = 0;
|
||
cd_overflow_frames = 0;
|
||
cd_underflow_frames = 0;
|
||
}
|
||
|
||
void spu_cd_audio_push(const int16_t* stereo, int frames) {
|
||
if (!stereo || frames <= 0) return;
|
||
|
||
uint32_t in_frames = (uint32_t)frames;
|
||
if (in_frames > SPU_CD_RING_FRAMES) {
|
||
uint32_t skip = in_frames - SPU_CD_RING_FRAMES;
|
||
stereo += skip * 2u;
|
||
cd_overflow_frames += skip;
|
||
in_frames = SPU_CD_RING_FRAMES;
|
||
}
|
||
|
||
if (cd_frame_count + in_frames > SPU_CD_RING_FRAMES) {
|
||
uint32_t drop = (cd_frame_count + in_frames) - SPU_CD_RING_FRAMES;
|
||
cd_read_pos = (cd_read_pos + drop) % SPU_CD_RING_FRAMES;
|
||
cd_frame_count -= drop;
|
||
cd_overflow_frames += drop;
|
||
}
|
||
|
||
for (uint32_t i = 0; i < in_frames; i++) {
|
||
cd_ring[cd_write_pos * 2u + 0u] = stereo[i * 2u + 0u];
|
||
cd_ring[cd_write_pos * 2u + 1u] = stereo[i * 2u + 1u];
|
||
cd_write_pos = (cd_write_pos + 1u) % SPU_CD_RING_FRAMES;
|
||
}
|
||
cd_frame_count += in_frames;
|
||
cd_push_frames += in_frames;
|
||
}
|
||
|
||
static int cd_audio_pop(int16_t* left, int16_t* right) {
|
||
if (cd_frame_count == 0) return 0;
|
||
*left = cd_ring[cd_read_pos * 2u + 0u];
|
||
*right = cd_ring[cd_read_pos * 2u + 1u];
|
||
cd_read_pos = (cd_read_pos + 1u) % SPU_CD_RING_FRAMES;
|
||
cd_frame_count--;
|
||
return 1;
|
||
}
|
||
|
||
static void decode_block(SpuVoice *v) {
|
||
static const int f0[5] = { 0, 60, 115, 98, 122 };
|
||
static const int f1[5] = { 0, 0, -52, -55, -60 };
|
||
|
||
uint32_t addr = v->cur_addr & (SPU_RAM_SIZE - 1u);
|
||
if (addr + 16u > SPU_RAM_SIZE) addr = 0;
|
||
|
||
uint8_t header = spu_ram[addr + 0u];
|
||
uint8_t flags = spu_ram[addr + 1u];
|
||
int shift = header & 0x0F;
|
||
int filter = (header >> 4) & 0x0F;
|
||
if (filter > 4) filter = 0;
|
||
if (shift > 12) shift = 12;
|
||
|
||
int out = 0;
|
||
for (int b = 0; b < 14; b++) {
|
||
uint8_t packed = spu_ram[addr + 2u + (uint32_t)b];
|
||
for (int n = 0; n < 2; n++) {
|
||
int sample4 = (n == 0) ? (packed & 0x0F) : (packed >> 4);
|
||
if (sample4 & 0x08) sample4 -= 0x10;
|
||
|
||
int32_t s = sample4 << 12;
|
||
s >>= shift;
|
||
s += ((int32_t)v->hist1 * f0[filter] +
|
||
(int32_t)v->hist2 * f1[filter] + 32) >> 6;
|
||
s = clamp16(s);
|
||
v->hist2 = v->hist1;
|
||
v->hist1 = (int16_t)s;
|
||
v->samples[out++] = (int16_t)s;
|
||
}
|
||
}
|
||
|
||
if (flags & 0x04u) v->repeat_addr = addr;
|
||
v->flags = flags;
|
||
v->sample_idx = 0;
|
||
v->cur_addr = (addr + 16u) & (SPU_RAM_SIZE - 1u);
|
||
|
||
/* Latch end-block-reached so the BIOS music engine sees ENDX[v] = 1
|
||
* when it polls 0x1F801D9C/D9E. Without this latch one-shot music
|
||
* engines never advance, leaving subsequent voices unkeyed. */
|
||
if (flags & 0x01u) {
|
||
int v_idx = (int)(v - voices);
|
||
if (v_idx >= 0 && v_idx < SPU_VOICE_COUNT) {
|
||
endx_latch |= (1u << v_idx);
|
||
}
|
||
}
|
||
}
|
||
|
||
static int16_t voice_next_sample(int idx) {
|
||
SpuVoice *v = &voices[idx];
|
||
if (!v->active) return 0;
|
||
|
||
if (v->sample_idx >= SPU_BLOCK_SAMPLES) {
|
||
if (v->flags & 0x01u) {
|
||
if (v->flags & 0x02u) {
|
||
v->cur_addr = v->repeat_addr & (SPU_RAM_SIZE - 1u);
|
||
spu_event_record(SPU_EV_END_LOOP, idx, v->repeat_addr);
|
||
} else {
|
||
/* End-without-repeat triggers Release on real hardware
|
||
* (Beetle's RunDecoder calls ReleaseEnvelope here). The
|
||
* voice keeps decoding past the end block — whatever
|
||
* follows in SPU RAM — while env_level decays to 0.
|
||
* Garbage samples are masked by the dying envelope, so
|
||
* by the time anything would be audible it's silent. */
|
||
spu_event_record(SPU_EV_END_STOP, idx, v->cur_addr);
|
||
v->adsr_phase = ADSR_RELEASE;
|
||
v->adsr_divider = 0;
|
||
v->flags = 0; /* don't re-enter this branch */
|
||
}
|
||
}
|
||
decode_block(v);
|
||
}
|
||
|
||
int16_t raw_s = v->samples[v->sample_idx];
|
||
/* Apply envelope (0..0x7FFF as a 15-bit gain). */
|
||
int32_t shaped = ((int32_t)raw_s * (int32_t)v->env_level) >> 15;
|
||
if (shaped > 32767) shaped = 32767;
|
||
if (shaped < -32768) shaped = -32768;
|
||
|
||
/* Step envelope once per output sample (44.1 kHz). */
|
||
adsr_run(idx, v);
|
||
/* Deactivate once Release fully decays to silence. */
|
||
if (v->adsr_phase == ADSR_RELEASE && v->env_level == 0) {
|
||
v->active = 0;
|
||
}
|
||
|
||
uint32_t pitch = voice_reg(idx, 2) & 0x3FFFu;
|
||
if (pitch == 0) pitch = 0x1000u;
|
||
v->phase += pitch;
|
||
while (v->phase >= 0x1000u) {
|
||
v->phase -= 0x1000u;
|
||
v->sample_idx++;
|
||
if (v->sample_idx >= SPU_BLOCK_SAMPLES) break;
|
||
}
|
||
return (int16_t)shaped;
|
||
}
|
||
|
||
static void key_on(uint32_t mask) {
|
||
for (int i = 0; i < SPU_VOICE_COUNT; i++) {
|
||
if (!(mask & (1u << i))) continue;
|
||
SpuVoice *v = &voices[i];
|
||
memset(v, 0, sizeof(*v));
|
||
v->active = 1;
|
||
v->cur_addr = ((uint32_t)voice_reg(i, 3) << 3) & (SPU_RAM_SIZE - 1u);
|
||
v->repeat_addr = ((uint32_t)voice_reg(i, 7) << 3) & (SPU_RAM_SIZE - 1u);
|
||
v->sample_idx = SPU_BLOCK_SAMPLES;
|
||
/* Reset ADSR — KEYON starts envelope at 0 in Attack phase
|
||
* (matches Beetle's PS_SPU::ResetEnvelope). */
|
||
v->env_level = 0;
|
||
v->adsr_divider = 0;
|
||
v->adsr_phase = ADSR_ATTACK;
|
||
key_on_count++;
|
||
endx_latch &= ~(1u << i); /* KEYON clears ENDX bit on real hw */
|
||
spu_event_record(SPU_EV_KEYON, i, v->cur_addr);
|
||
}
|
||
}
|
||
|
||
/* KEYOFF triggers Release phase, NOT immediate silence. The voice
|
||
* keeps voicing while env_level decays from its current value to 0
|
||
* at the configured Release rate. This is the boot-chime fade tail —
|
||
* silencing immediately is what made channels appear to "cut out". */
|
||
static void key_off(uint32_t mask) {
|
||
for (int i = 0; i < SPU_VOICE_COUNT; i++) {
|
||
if (!(mask & (1u << i))) continue;
|
||
if (!voices[i].active) continue;
|
||
spu_event_record(SPU_EV_KEYOFF, i, voices[i].cur_addr);
|
||
voices[i].adsr_phase = ADSR_RELEASE;
|
||
voices[i].adsr_divider = 0;
|
||
/* env_level preserved — release decays from wherever we are now. */
|
||
}
|
||
}
|
||
|
||
void spu_init(void) {
|
||
memset(spu_ram, 0, sizeof(spu_ram));
|
||
memset(spu_regs, 0, sizeof(spu_regs));
|
||
memset(voices, 0, sizeof(voices));
|
||
memset(s_events, 0, sizeof(s_events));
|
||
transfer_addr = 0;
|
||
key_on_count = 0;
|
||
render_frames = 0;
|
||
nonzero_frames = 0;
|
||
last_peak = 0;
|
||
peak = 0;
|
||
endx_latch = 0;
|
||
kon_latch = 0;
|
||
koff_latch = 0;
|
||
s_event_idx = 0;
|
||
s_event_seq = 0;
|
||
spu_cd_audio_reset();
|
||
}
|
||
|
||
void spu_render(int16_t* out_stereo, int frames) {
|
||
if (!out_stereo || frames <= 0) return;
|
||
|
||
uint16_t ctrl = spu_regs[reg_index(0x1F801DAAu)];
|
||
int enabled = (ctrl & 0x8000u) != 0;
|
||
int16_t main_l = direct_volume(spu_regs[reg_index(0x1F801D80u)]);
|
||
int16_t main_r = direct_volume(spu_regs[reg_index(0x1F801D82u)]);
|
||
int16_t cd_vol_l = cd_input_volume(spu_regs[reg_index(0x1F801DB0u)]);
|
||
int16_t cd_vol_r = cd_input_volume(spu_regs[reg_index(0x1F801DB2u)]);
|
||
|
||
int32_t block_peak = 0;
|
||
for (int f = 0; f < frames; f++) {
|
||
int32_t mix_l = 0;
|
||
int32_t mix_r = 0;
|
||
|
||
if (enabled) {
|
||
for (int v = 0; v < SPU_VOICE_COUNT; v++) {
|
||
int16_t s = voice_next_sample(v);
|
||
if (!s) continue;
|
||
int16_t vl = direct_volume(voice_reg(v, 0));
|
||
int16_t vr = direct_volume(voice_reg(v, 1));
|
||
mix_l += ((int32_t)s * vl) >> 14;
|
||
mix_r += ((int32_t)s * vr) >> 14;
|
||
}
|
||
if (ctrl & 0x0001u) {
|
||
int16_t cd_l = 0;
|
||
int16_t cd_r = 0;
|
||
if (cd_audio_pop(&cd_l, &cd_r)) {
|
||
mix_l += ((int32_t)cd_l * cd_vol_l) >> 15;
|
||
mix_r += ((int32_t)cd_r * cd_vol_r) >> 15;
|
||
} else if (cd_push_frames != 0) {
|
||
cd_underflow_frames++;
|
||
}
|
||
}
|
||
mix_l = (mix_l * main_l) >> 14;
|
||
mix_r = (mix_r * main_r) >> 14;
|
||
}
|
||
|
||
out_stereo[f * 2 + 0] = clamp16(mix_l);
|
||
out_stereo[f * 2 + 1] = clamp16(mix_r);
|
||
int32_t frame_peak = abs32(out_stereo[f * 2 + 0]);
|
||
int32_t right_peak = abs32(out_stereo[f * 2 + 1]);
|
||
if (right_peak > frame_peak) frame_peak = right_peak;
|
||
if (frame_peak) nonzero_frames++;
|
||
if (frame_peak > block_peak) block_peak = frame_peak;
|
||
}
|
||
render_frames += (uint64_t)frames;
|
||
last_peak = block_peak;
|
||
if (block_peak > peak) peak = block_peak;
|
||
}
|
||
|
||
void spu_debug_info(SpuDebugInfo* out) {
|
||
if (!out) return;
|
||
memset(out, 0, sizeof(*out));
|
||
out->ctrl = spu_regs[reg_index(0x1F801DAAu)];
|
||
out->main_l = direct_volume(spu_regs[reg_index(0x1F801D80u)]);
|
||
out->main_r = direct_volume(spu_regs[reg_index(0x1F801D82u)]);
|
||
out->cd_l = cd_input_volume(spu_regs[reg_index(0x1F801DB0u)]);
|
||
out->cd_r = cd_input_volume(spu_regs[reg_index(0x1F801DB2u)]);
|
||
for (int i = 0; i < SPU_VOICE_COUNT; i++) {
|
||
if (voices[i].active) out->active_mask |= (1u << i);
|
||
}
|
||
out->key_on_count = key_on_count;
|
||
out->render_frames = render_frames;
|
||
out->nonzero_frames = nonzero_frames;
|
||
out->last_peak = last_peak;
|
||
out->peak = peak;
|
||
out->cd_frames = cd_frame_count;
|
||
out->cd_push_frames = cd_push_frames;
|
||
out->cd_overflow_frames = cd_overflow_frames;
|
||
out->cd_underflow_frames = cd_underflow_frames;
|
||
}
|
||
|
||
uint32_t spu_read(uint32_t addr) {
|
||
if (addr >= 0x1F801C00u && addr <= 0x1F801DFFu) {
|
||
uint32_t idx = reg_index(addr);
|
||
if (idx < SPU_REG_COUNT) {
|
||
if (addr == 0x1F801DAEu) {
|
||
return 0x0400; /* SPUSTAT: ready */
|
||
}
|
||
/* ENDX (end-block-reached latch). Real hw sets bit v when voice
|
||
* v decodes a block whose flag byte has bit 0; KEYON[v] clears
|
||
* it. Without this latch, music engines that poll ENDX to wait
|
||
* for a sample to finish never advance and downstream voices
|
||
* never get keyed on. */
|
||
if (addr == 0x1F801D9Cu) {
|
||
return endx_latch & 0xFFFFu;
|
||
}
|
||
if (addr == 0x1F801D9Eu) {
|
||
return (endx_latch >> 16) & 0xFFu;
|
||
}
|
||
/* Voice register 6 (byte offset 0x0C) is CURVOL / ADSR_LEVEL —
|
||
* returns the live envelope level. PSX music engines poll this
|
||
* to pick a "free" voice (env_level == 0). Without exposing the
|
||
* real envelope, the BIOS sees every voice as silent and over-
|
||
* recycles voices 0-3 instead of fanning across all 24. */
|
||
if (addr >= 0x1F801C00u && addr < 0x1F801D80u
|
||
&& (idx & 7u) == 6u) {
|
||
int v = (int)(idx >> 3);
|
||
if (v >= 0 && v < SPU_VOICE_COUNT)
|
||
return voices[v].env_level;
|
||
}
|
||
return spu_regs[idx];
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
void spu_write(uint32_t addr, uint32_t value) {
|
||
if (addr >= 0x1F801C00u && addr <= 0x1F801DFFu) {
|
||
uint32_t idx = reg_index(addr);
|
||
if (idx < SPU_REG_COUNT) {
|
||
spu_regs[idx] = (uint16_t)value;
|
||
|
||
if (addr == 0x1F801D88u) {
|
||
kon_latch = (kon_latch & 0xFFFF0000u) | (uint32_t)(uint16_t)value;
|
||
key_on((uint32_t)(uint16_t)value);
|
||
}
|
||
if (addr == 0x1F801D8Au) {
|
||
kon_latch = (kon_latch & 0x0000FFFFu) | ((uint32_t)(uint16_t)value << 16);
|
||
key_on((uint32_t)(uint16_t)value << 16);
|
||
}
|
||
if (addr == 0x1F801D8Cu) {
|
||
koff_latch = (koff_latch & 0xFFFF0000u) | (uint32_t)(uint16_t)value;
|
||
key_off((uint32_t)(uint16_t)value);
|
||
}
|
||
if (addr == 0x1F801D8Eu) {
|
||
koff_latch = (koff_latch & 0x0000FFFFu) | ((uint32_t)(uint16_t)value << 16);
|
||
key_off((uint32_t)(uint16_t)value << 16);
|
||
}
|
||
|
||
if (addr == 0x1F801DA6u) {
|
||
transfer_addr = ((uint32_t)(uint16_t)value) << 3;
|
||
if (transfer_addr >= SPU_RAM_SIZE) transfer_addr = 0;
|
||
}
|
||
|
||
if (addr == 0x1F801DA8u) {
|
||
if (transfer_addr + 1 < SPU_RAM_SIZE) {
|
||
spu_ram[transfer_addr] = (uint8_t)(value & 0xFF);
|
||
spu_ram[transfer_addr + 1] = (uint8_t)((value >> 8) & 0xFF);
|
||
}
|
||
transfer_addr = (transfer_addr + 2) % SPU_RAM_SIZE;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void spu_dma_write(uint32_t word) {
|
||
if (transfer_addr + 3 < SPU_RAM_SIZE) {
|
||
spu_ram[transfer_addr] = (uint8_t)(word & 0xFF);
|
||
spu_ram[transfer_addr + 1] = (uint8_t)((word >> 8) & 0xFF);
|
||
spu_ram[transfer_addr + 2] = (uint8_t)((word >> 16) & 0xFF);
|
||
spu_ram[transfer_addr + 3] = (uint8_t)((word >> 24) & 0xFF);
|
||
}
|
||
transfer_addr = (transfer_addr + 4) % SPU_RAM_SIZE;
|
||
}
|
||
|
||
int spu_dma_ready(void) {
|
||
return 1;
|
||
}
|
||
|
||
const uint8_t* spu_get_ram(void) {
|
||
return spu_ram;
|
||
}
|
||
|
||
void spu_get_voice_state(int idx, SpuVoiceState* out) {
|
||
if (!out) return;
|
||
memset(out, 0, sizeof(*out));
|
||
if (idx < 0 || idx >= SPU_VOICE_COUNT) return;
|
||
SpuVoice *v = &voices[idx];
|
||
out->active = v->active;
|
||
out->vol_ctrl_l = voice_reg(idx, 0);
|
||
out->vol_ctrl_r = voice_reg(idx, 1);
|
||
out->pitch = voice_reg(idx, 2);
|
||
out->start_lo = voice_reg(idx, 3);
|
||
out->adsr_lo = voice_reg(idx, 4);
|
||
out->adsr_hi = voice_reg(idx, 5);
|
||
out->loop_lo = voice_reg(idx, 7);
|
||
out->cur_addr = v->cur_addr;
|
||
out->repeat_addr = v->repeat_addr;
|
||
out->last_flags = v->flags;
|
||
out->sample_idx = (uint8_t)v->sample_idx;
|
||
out->phase = (uint16_t)v->phase;
|
||
}
|
||
|
||
void spu_get_global_state(SpuGlobalState* out) {
|
||
if (!out) return;
|
||
memset(out, 0, sizeof(*out));
|
||
out->ctrl = spu_regs[reg_index(0x1F801DAAu)];
|
||
out->main_vol_l = spu_regs[reg_index(0x1F801D80u)];
|
||
out->main_vol_r = spu_regs[reg_index(0x1F801D82u)];
|
||
out->kon_latch = kon_latch & 0xFFFFFFu;
|
||
out->koff_latch = koff_latch & 0xFFFFFFu;
|
||
out->pmon = (uint32_t)spu_regs[reg_index(0x1F801D90u)] |
|
||
((uint32_t)spu_regs[reg_index(0x1F801D92u)] << 16);
|
||
out->non = (uint32_t)spu_regs[reg_index(0x1F801D94u)] |
|
||
((uint32_t)spu_regs[reg_index(0x1F801D96u)] << 16);
|
||
out->eon = (uint32_t)spu_regs[reg_index(0x1F801D98u)] |
|
||
((uint32_t)spu_regs[reg_index(0x1F801D9Au)] << 16);
|
||
out->endx = endx_latch & 0xFFFFFFu;
|
||
uint32_t am = 0;
|
||
for (int i = 0; i < SPU_VOICE_COUNT; i++)
|
||
if (voices[i].active) am |= (1u << i);
|
||
out->active_mask = am;
|
||
}
|
||
|
||
uint64_t spu_event_total(void) { return s_event_seq; }
|
||
|
||
uint32_t spu_event_get(SpuEvent* out, uint32_t max_count) {
|
||
if (!out || max_count == 0) return 0;
|
||
uint64_t avail = s_event_seq < (uint64_t)SPU_EVENT_CAP
|
||
? s_event_seq : (uint64_t)SPU_EVENT_CAP;
|
||
if ((uint64_t)max_count > avail) max_count = (uint32_t)avail;
|
||
/* Most recent N, oldest first. */
|
||
uint32_t start = (s_event_idx + SPU_EVENT_CAP - max_count) & (SPU_EVENT_CAP - 1u);
|
||
for (uint32_t i = 0; i < max_count; i++) {
|
||
out[i] = s_events[(start + i) & (SPU_EVENT_CAP - 1u)];
|
||
}
|
||
return max_count;
|
||
}
|
||
|
||
void spu_event_reset(void) {
|
||
s_event_idx = 0;
|
||
s_event_seq = 0;
|
||
memset(s_events, 0, sizeof(s_events));
|
||
}
|