diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 59dcc656..0d34ccbe 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -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) diff --git a/runtime/src/spu.c b/runtime/src/spu.c index 4a4cdf05..7afed116 100644 --- a/runtime/src/spu.c +++ b/runtime/src/spu.c @@ -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 (idxsample_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]; diff --git a/runtime/tests/test_spu_silent_fetch.c b/runtime/tests/test_spu_silent_fetch.c new file mode 100644 index 00000000..5cbf2c5a --- /dev/null +++ b/runtime/tests/test_spu_silent_fetch.c @@ -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 +#include +#include + +#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; +}