mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Keep silent SPU hardware voices fetching ADPCM and IRQs
This commit is contained in:
@@ -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
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user