Keep silent SPU hardware voices fetching ADPCM and IRQs

This commit is contained in:
Matthew Stanley
2026-10-25 08:10:17 -07:00
parent effb75e63d
commit 0ec8f82eea
3 changed files with 149 additions and 10 deletions
+4
View File
@@ -2051,6 +2051,10 @@ if(BUILD_TESTING)
add_test(NAME spu_end_without_repeat_test
COMMAND spu_end_without_repeat_test)
add_executable(spu_silent_fetch_test tests/test_spu_silent_fetch.c)
target_include_directories(spu_silent_fetch_test PRIVATE include)
add_test(NAME spu_silent_fetch_test COMMAND spu_silent_fetch_test)
add_executable(spu_pitch_zero_test
tests/test_spu_pitch_zero.c)
target_include_directories(spu_pitch_zero_test PRIVATE include)
+27 -10
View File
@@ -890,9 +890,21 @@ typedef char spu_shadow_voice_count_check[
const void* spu_shadow_tap_buffer(void) { return s_shadow_tap; }
int spu_shadow_tap_count(void) { return s_shadow_tap_frame; }
/* Hardware ADPCM fetches continue after Release reaches zero. Audible state
* only gates the envelope/output; pitch and block/IRQ/ENDX state keep running.
* PSX-SPX, SPU Interrupt / Voice Interrupt:
* https://psx-spx.consoledev.net/soundprocessingunitspu/#voice-interrupt
* Private host voices have an explicit lifetime and are not hardware slots. */
static int voice_needs_clock(int idx) {
const SpuVoice *v = &voices[idx];
return v->active || (idx < SPU_VOICE_COUNT &&
((voice_reg(idx, 2) & 0x3FFFu) || v->sample_idx >= SPU_BLOCK_SAMPLES));
}
static int16_t voice_next_sample(int idx) {
SpuVoice *v = &voices[idx];
if (!v->active) return 0;
if (!voice_needs_clock(idx)) return 0;
const int audible = v->active;
if (v->sample_idx >= SPU_BLOCK_SAMPLES) {
if (v->flags & 0x01u) {
@@ -901,7 +913,7 @@ static int16_t voice_next_sample(int idx) {
* (CurAddr = LoopAddr) does this unconditionally. */
v->cur_addr = v->repeat_addr & (SPU_RAM_SIZE - 1u);
if (v->flags & 0x02u) {
spu_event_record(SPU_EV_END_LOOP, idx, v->repeat_addr);
if (audible) spu_event_record(SPU_EV_END_LOOP, idx, v->repeat_addr);
} else {
/* END without REPEAT: hardware forces the envelope to ZERO
* and enters Release (Beetle spu.cpp:341-352 — "Force
@@ -918,7 +930,7 @@ static int16_t voice_next_sample(int idx) {
* parked there at full envelope; a handful of ice-block
* hits in the X4 attract demo railed the whole mix at
* +32767 for ~35 s ("static" / music cut-out). */
spu_event_record(SPU_EV_END_STOP, idx, v->cur_addr);
if (audible) spu_event_record(SPU_EV_END_STOP, idx, v->cur_addr);
v->adsr_phase = ADSR_RELEASE;
v->adsr_divider = 0;
v->env_level = 0;
@@ -931,10 +943,12 @@ static int16_t voice_next_sample(int idx) {
* INSTEAD of its interpolated ADPCM sample. Everything else — block
* decoding/address advance, ENDX latching, pitch stepping, the ADSR
* envelope and the volume stages — behaves exactly as for ADPCM. */
int16_t raw_s;
int16_t raw_s = 0;
uint32_t non_mask = (uint32_t)spu_regs[reg_index(0x1F801D94u)]
| ((uint32_t)spu_regs[reg_index(0x1F801D96u)] << 16);
if (idx<SPU_VOICE_COUNT?(non_mask & (1u << idx)):
if (!audible) {
/* Silent hardware slots still clock below, without interpolation. */
} else if (idx<SPU_VOICE_COUNT?(non_mask & (1u << idx)):
(private_modes[idx-SPU_VOICE_COUNT]&1u)) {
raw_s = (int16_t)noise_lfsr;
} else {
@@ -953,7 +967,7 @@ static int16_t voice_next_sample(int idx) {
* advances. The optional shadow keeps its own cubic, block-clamped window;
* the canonical hardware path above independently uses the preceding
* block's tail for Gaussian interpolation. */
if (s_shadow_tap_on && s_shadow_tap_frame < SPU_SHADOW_TAP_FRAMES) {
if (audible && s_shadow_tap_on && s_shadow_tap_frame < SPU_SHADOW_TAP_FRAMES) {
SpuShadowVoiceTap *t =
&s_shadow_tap[s_shadow_tap_frame].voice[idx];
int si = v->sample_idx;
@@ -971,8 +985,9 @@ static int16_t voice_next_sample(int idx) {
}
/* Step envelope once per output sample (44.1 kHz). */
adsr_run(idx, v);
/* Deactivate once Release fully decays to silence. */
if (audible) adsr_run(idx, v);
/* Retire audible output once Release fully decays. Hardware fetches keep
* running through voice_needs_clock; private voices stop completely. */
if (v->adsr_phase == ADSR_RELEASE && v->env_level == 0) {
v->active = 0;
}
@@ -1135,7 +1150,7 @@ static void spu_render_block(int16_t* out_stereo, int frames) {
int mix_voices=SPU_VOICE_COUNT+(private_enabled?SPU_PRIVATE_VOICES:0);
if (enabled) {
for (int v = 0; v < mix_voices; v++) {
if (voices[v].active) { any_voice = 1; break; }
if (voice_needs_clock(v)) { any_voice = 1; break; }
}
}
@@ -1152,7 +1167,9 @@ static void spu_render_block(int16_t* out_stereo, int frames) {
/* MotK intro FMV (and other XA-only scenes): active_mask stays 0 while
* CD/XA plays. Keep capture / noise / reverb / main sweeps (issue #103
* fidelity — FMV is exactly the CD-with-reverb case) but skip the
* 24-voice walk and per-voice sweep steps. Shadow tap stays on the
* 24-voice walk and per-voice sweep steps only when no hardware slot needs
* pitch-driven fetching. Silent released slots can still trigger IRQs.
* Shadow tap stays on the
* general path. */
if (enabled && !any_voice && !s_shadow_tap_on) {
static int16_t s_voice_silence[2048 * 2];
+118
View File
@@ -0,0 +1,118 @@
/* Released hardware slots still fetch ADPCM and can deliver stream IRQs.
* Includes spu.c to verify both the mixer fast path and exact decoder state. */
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "../src/spu.c"
/* Dependencies owned by the surrounding runtime are inert in this unit test. */
uint64_t s_frame_count;
uint64_t psx_get_cycle_count(void) { return 0; }
void audio_trace_pcm(int tap, const int16_t *stereo, int frames) {
(void)tap;
(void)stereo;
(void)frames;
}
void audio_trace_event(uint16_t kind, uint32_t a, uint32_t b) {
(void)kind;
(void)a;
(void)b;
}
void psx_irq_raise(uint32_t bit, uint32_t detail) { (void)bit; (void)detail; }
uint32_t crc32_update(uint32_t crc, const uint8_t *data, size_t len) {
(void)data;
(void)len;
return crc;
}
bool spu_shadow_enabled(void) { return false; }
void spu_shadow_reset(void) {}
void spu_shadow_process(int16_t *canon, int frames) {
(void)canon;
(void)frames;
}
#define CHECK(x) do { if (!(x)) { fprintf(stderr, "line %d: %s\n", __LINE__, #x); exit(1); } } while (0)
static void stage(unsigned index, uint16_t pitch, int active) {
SpuVoice *v = &voices[index];
memset(v, 0, sizeof *v);
v->active = active;
v->env_level = active ? 0x6000u : 0;
v->adsr_phase = active ? ADSR_SUSTAIN : ADSR_RELEASE;
v->cur_addr = 0x2000;
v->repeat_addr = 0x2000;
v->sample_idx = SPU_BLOCK_SAMPLES;
spu_regs[index * 8u + 2u] = pitch;
for (uint32_t a = 0x2000; a < 0x2100; a += 16) {
spu_ram[a] = 0;
spu_ram[a + 1] = 0;
memset(spu_ram + a + 2, 0x11, 14);
}
spu_regs[reg_index(0x1F801DAA)] = 0xC040;
spu_regs[reg_index(0x1F801DA4)] = 0x2040 / 8;
}
static void silent_irq(int with_playing_voice) {
spu_init();
stage(18, 0x0800, 0);
if (with_playing_voice) {
stage(0, 0x1000, 1);
spu_regs[0] = spu_regs[1] = 0x3FFF;
spu_regs[reg_index(0x1F801D80)] = 0x3FFF;
spu_regs[reg_index(0x1F801D82)] = 0x3FFF;
}
int16_t pcm[512 * 2];
spu_render_block(pcm, 512);
CHECK(irq_flag && voices[18].cur_addr > 0x2040);
CHECK(!voices[18].active && !voices[18].env_level);
unsigned nonzero = 0;
for (unsigned i = 0; i < sizeof pcm / sizeof *pcm; ++i) nonzero += pcm[i] != 0;
CHECK(with_playing_voice ? nonzero > 0 : nonzero == 0);
SpuEvent events[64];
uint32_t count = spu_event_get(events, 64), irq = 0;
for (uint32_t i = 0; i < count; ++i)
if (events[i].kind == SPU_EV_IRQ && events[i].addr == 0x2040) ++irq;
CHECK(irq == 1);
}
int main(void) {
silent_irq(0); /* exercises the previously all-silent fast path */
silent_irq(1); /* a simultaneous audible slot must not hide the issue */
spu_init(); stage(18, 0x0800, 1);
voices[18].env_level = 1; voices[18].adsr_phase = ADSR_RELEASE;
int16_t released[512 * 2]; spu_render_block(released, 512);
CHECK(irq_flag && !voices[18].active && !voices[18].env_level);
spu_init(); stage(18, 0, 0);
int16_t pcm[512 * 2]; spu_render_block(pcm, 512);
CHECK(!irq_flag && voices[18].cur_addr == 0x2010);
CHECK(voices[18].sample_idx == 0); /* zero pitch still holds */
spu_init(); stage(18, 0x1000, 0);
spu_ram[0x2041] = 1; /* silent END still updates ENDX */
spu_render_block(pcm, 512);
CHECK(endx_latch & (1u << 18));
CHECK(!voices[18].active && !voices[18].env_level);
/* A private voice is explicitly stopped after release; it is not an
* emulated hardware fetch slot and must not dereference an expired bank. */
voices[SPU_VOICE_COUNT].active = 0;
voices[SPU_VOICE_COUNT].cur_addr = 0x1234;
voices[SPU_VOICE_COUNT].sample_idx = SPU_BLOCK_SAMPLES;
CHECK(!voice_next_sample(SPU_VOICE_COUNT));
CHECK(voices[SPU_VOICE_COUNT].cur_addr == 0x1234);
spu_init(); stage(18, 0x1000, 0);
spu_regs[reg_index(0x1F801DAA)] = 0xC000; /* silent clock runs with IRQ off */
spu_render_block(pcm, 80);
CHECK(voices[18].cur_addr > 0x2000 && !irq_flag);
uint32_t next = voices[18].cur_addr;
spu_regs[reg_index(0x1F801DA4)] = (uint16_t)(next / 8);
spu_regs[reg_index(0x1F801DAA)] = 0xC040;
spu_render_block(pcm, 100);
CHECK(irq_flag); /* enabling IRQ sees the continuously advanced position */
puts("spu_silent_fetch_test: pass");
return 0;
}