mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
test(spu): pin END-only voice mute semantics
Document the overlap with kem0x PR #14 and explicitly record the game-specific and unrelated changes excluded from this branch. Co-authored-by: Kareem Olim <kareemolim@gmail.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
# kem0x PR #14: SPU END-without-REPEAT behavior
|
||||
|
||||
Evaluated on 2026-07-13 against psxrecomp master `dde268dc0fb9daf8fe6529f4aebfe80995350334`.
|
||||
|
||||
## Source and credit
|
||||
|
||||
- Pull request: [mstan/psxrecomp#14](https://github.com/mstan/psxrecomp/pull/14)
|
||||
- Evaluated commit: [`b2597df6d5905a0e183b5309a9b84720cd417e19`](https://github.com/mstan/psxrecomp/commit/b2597df6d5905a0e183b5309a9b84720cd417e19)
|
||||
- Original author: Kareem Olim (`Kareem Olim <kareemolim@gmail.com>`)
|
||||
|
||||
The generic behavior identified in that commit is that an ADPCM block carrying
|
||||
END without REPEAT must redirect decoding to the voice's repeat address, enter
|
||||
Release, and immediately clear the envelope. This prevents a one-shot voice
|
||||
from leaking or looping unrelated sample RAM after its terminator.
|
||||
|
||||
Current master already implements the same behavior independently in commit
|
||||
`462e2bf5fdb4d46b935322bf7f782fd424aea584`. Therefore this branch does not
|
||||
replace that runtime code. It adds a focused regression test that pins the
|
||||
shared behavior: END-only becomes silent and inactive at the boundary, resumes
|
||||
decoding at the repeat address, and emits one stop event; END+REPEAT remains
|
||||
audible and active.
|
||||
|
||||
## Explicit exclusions
|
||||
|
||||
The following material from the source commit was reviewed and intentionally
|
||||
not imported:
|
||||
|
||||
- `PEPSIMAN_SFX_RETRIGGER_GUARD` and its address-based burst/cooldown policy:
|
||||
game-specific compensation for Pepsiman timing, not generic SPU behavior.
|
||||
- The `PSX_WEB` reduction of `SPU_EVENT_CAP`: WebAssembly memory sizing is
|
||||
unrelated to ADPCM END semantics and belongs in a separate change.
|
||||
- `SpuVoice.start_addr`: introduced for the excluded retrigger policy and not
|
||||
needed for the generic END transition.
|
||||
- All other PR #14 changes: CD-ROM interrupts, CD-DA, raster timing, build
|
||||
changes, and game-specific hooks require independent review and branches.
|
||||
|
||||
No Pepsiman compile-time hook or game-specific playback heuristic is present in
|
||||
this branch.
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Regression test for an ADPCM END block without REPEAT.
|
||||
*
|
||||
* The test includes spu.c so it can stage a voice exactly at the block
|
||||
* boundary, without exposing emulator internals in the runtime API.
|
||||
*
|
||||
* Build from runtime/tests:
|
||||
* gcc -std=c99 -I../include -o test_spu_end_without_repeat.exe \
|
||||
* test_spu_end_without_repeat.c
|
||||
*/
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static int failures;
|
||||
|
||||
#define CHECK(condition, message) \
|
||||
do { \
|
||||
if (!(condition)) { \
|
||||
fprintf(stderr, "FAIL: %s\n", message); \
|
||||
failures++; \
|
||||
} else { \
|
||||
printf("ok: %s\n", message); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static void fill_block(uint32_t addr, uint8_t flags, uint8_t packed) {
|
||||
spu_ram[addr] = 0x00; /* filter 0, shift 0 */
|
||||
spu_ram[addr + 1u] = flags;
|
||||
memset(spu_ram + addr + 2u, packed, 14u);
|
||||
}
|
||||
|
||||
static SpuVoice *stage_end_boundary(uint8_t flags, uint32_t repeat_addr) {
|
||||
SpuVoice *v = &voices[0];
|
||||
memset(v, 0, sizeof(*v));
|
||||
v->active = 1;
|
||||
v->cur_addr = 0x310u; /* the block after the terminator */
|
||||
v->repeat_addr = repeat_addr;
|
||||
v->sample_idx = SPU_BLOCK_SAMPLES;
|
||||
v->env_level = 0x6000u;
|
||||
v->adsr_phase = ADSR_SUSTAIN;
|
||||
v->flags = flags;
|
||||
spu_regs[2] = 0x1000u; /* one decoded sample per output sample */
|
||||
return v;
|
||||
}
|
||||
|
||||
static void test_end_without_repeat_is_silent(void) {
|
||||
const uint32_t repeat_addr = 0x120u;
|
||||
spu_init();
|
||||
|
||||
/* Distinguish the required repeat-address decode from an incorrect
|
||||
* forward decode. Both blocks are deliberately non-silent: the envelope,
|
||||
* rather than convenient zero sample data, must make the result silent. */
|
||||
fill_block(repeat_addr, 0x04u, 0x11u);
|
||||
fill_block(0x310u, 0x00u, 0x77u);
|
||||
SpuVoice *v = stage_end_boundary(0x01u, repeat_addr);
|
||||
|
||||
int16_t sample = voice_next_sample(0);
|
||||
|
||||
CHECK(sample == 0, "END without REPEAT emits silence at the boundary");
|
||||
CHECK(v->env_level == 0, "END without REPEAT forces the envelope to zero");
|
||||
CHECK(v->adsr_phase == ADSR_RELEASE,
|
||||
"END without REPEAT enters Release");
|
||||
CHECK(!v->active, "zero-level Release deactivates the voice");
|
||||
CHECK(v->samples[0] == 4096,
|
||||
"decoder resumes at the latched repeat address, not forward RAM");
|
||||
CHECK(spu_event_total() == 1, "END stop records exactly one event");
|
||||
|
||||
SpuEvent event;
|
||||
CHECK(spu_event_get(&event, 1) == 1 && event.kind == SPU_EV_END_STOP,
|
||||
"END stop event has the stop kind");
|
||||
CHECK(event.addr == repeat_addr,
|
||||
"END stop event reports the redirected repeat address");
|
||||
|
||||
(void)voice_next_sample(0);
|
||||
CHECK(spu_event_total() == 1,
|
||||
"an inactive one-shot does not retrigger its END transition");
|
||||
}
|
||||
|
||||
static void test_end_with_repeat_keeps_playing(void) {
|
||||
const uint32_t repeat_addr = 0x180u;
|
||||
spu_init();
|
||||
fill_block(repeat_addr, 0x00u, 0x11u);
|
||||
SpuVoice *v = stage_end_boundary(0x03u, repeat_addr);
|
||||
|
||||
int16_t sample = voice_next_sample(0);
|
||||
|
||||
CHECK(sample != 0, "END with REPEAT remains audible");
|
||||
CHECK(v->env_level != 0, "END with REPEAT preserves the envelope");
|
||||
CHECK(v->adsr_phase == ADSR_SUSTAIN,
|
||||
"END with REPEAT preserves the ADSR phase");
|
||||
CHECK(v->active, "END with REPEAT keeps the voice active");
|
||||
|
||||
SpuEvent event;
|
||||
CHECK(spu_event_get(&event, 1) == 1 && event.kind == SPU_EV_END_LOOP,
|
||||
"END with REPEAT records a loop event");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_end_without_repeat_is_silent();
|
||||
test_end_with_repeat_keeps_playing();
|
||||
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user