diff --git a/src/mips/tests/spu/Makefile b/src/mips/tests/spu/Makefile new file mode 100644 index 000000000..540cae8bf --- /dev/null +++ b/src/mips/tests/spu/Makefile @@ -0,0 +1,61 @@ +TARGET = spu +USE_FUNCTION_SECTIONS = false +TYPE = ps-exe + +SRCS = \ +../uC-sdk-glue/BoardConsole.c \ +../uC-sdk-glue/BoardInit.c \ +../uC-sdk-glue/init.c \ +\ +../../../../third_party/uC-sdk/libc/src/cxx-glue.c \ +../../../../third_party/uC-sdk/libc/src/errno.c \ +../../../../third_party/uC-sdk/libc/src/initfini.c \ +../../../../third_party/uC-sdk/libc/src/malloc.c \ +../../../../third_party/uC-sdk/libc/src/qsort.c \ +../../../../third_party/uC-sdk/libc/src/rand.c \ +../../../../third_party/uC-sdk/libc/src/reent.c \ +../../../../third_party/uC-sdk/libc/src/stdio.c \ +../../../../third_party/uC-sdk/libc/src/string.c \ +../../../../third_party/uC-sdk/libc/src/strto.c \ +../../../../third_party/uC-sdk/libc/src/unistd.c \ +../../../../third_party/uC-sdk/libc/src/xprintf.c \ +../../../../third_party/uC-sdk/libc/src/xscanf.c \ +../../../../third_party/uC-sdk/libc/src/yscanf.c \ +../../../../third_party/uC-sdk/os/src/devfs.c \ +../../../../third_party/uC-sdk/os/src/filesystem.c \ +../../../../third_party/uC-sdk/os/src/fio.c \ +../../../../third_party/uC-sdk/os/src/hash-djb2.c \ +../../../../third_party/uC-sdk/os/src/init.c \ +../../../../third_party/uC-sdk/os/src/osdebug.c \ +../../../../third_party/uC-sdk/os/src/romfs.c \ +../../../../third_party/uC-sdk/os/src/sbrk.c \ + + +CPPFLAGS = -DNOFLOATINGPOINT +CPPFLAGS += -I. +CPPFLAGS += -I../../../../third_party/uC-sdk/libc/include +CPPFLAGS += -I../../../../third_party/uC-sdk/os/include +CPPFLAGS += -I../../../../third_party/libcester/include +CPPFLAGS += -I../../openbios/uC-sdk-glue + +ifeq ($(PCSX_TESTS),true) +CPPFLAGS += -DPCSX_TESTS=1 +endif + +ifeq ($(SPU_DUMP),true) +CPPFLAGS += -DSPU_DUMP=1 +endif + +SRCS += \ +../../common/syscalls/printf.s \ +../../common/crt0/uC-sdk-crt0.s \ +spu.c \ + +include ../../common.mk + +# generate .test.pcm samples instead of running assertion +.PHONY: spu_dump +spu_dump: + $(MAKE) clean + $(MAKE) SPU_DUMP=true + diff --git a/src/mips/tests/spu/loop_t0.test.pcm b/src/mips/tests/spu/loop_t0.test.pcm new file mode 100644 index 000000000..959173213 Binary files /dev/null and b/src/mips/tests/spu/loop_t0.test.pcm differ diff --git a/src/mips/tests/spu/loop_t1.test.pcm b/src/mips/tests/spu/loop_t1.test.pcm new file mode 100644 index 000000000..20894ef17 Binary files /dev/null and b/src/mips/tests/spu/loop_t1.test.pcm differ diff --git a/src/mips/tests/spu/silent.test.pcm b/src/mips/tests/spu/silent.test.pcm new file mode 100644 index 000000000..08ddc4d9d Binary files /dev/null and b/src/mips/tests/spu/silent.test.pcm differ diff --git a/src/mips/tests/spu/sine.test.pcm b/src/mips/tests/spu/sine.test.pcm new file mode 100644 index 000000000..268ba4462 Binary files /dev/null and b/src/mips/tests/spu/sine.test.pcm differ diff --git a/src/mips/tests/spu/sine_high.test.pcm b/src/mips/tests/spu/sine_high.test.pcm new file mode 100644 index 000000000..c1d73b210 Binary files /dev/null and b/src/mips/tests/spu/sine_high.test.pcm differ diff --git a/src/mips/tests/spu/sine_low.test.pcm b/src/mips/tests/spu/sine_low.test.pcm new file mode 100644 index 000000000..de9cbbf62 Binary files /dev/null and b/src/mips/tests/spu/sine_low.test.pcm differ diff --git a/src/mips/tests/spu/sine_pitch_0800.test.pcm b/src/mips/tests/spu/sine_pitch_0800.test.pcm new file mode 100644 index 000000000..6e826d160 Binary files /dev/null and b/src/mips/tests/spu/sine_pitch_0800.test.pcm differ diff --git a/src/mips/tests/spu/sine_pitch_2000.test.pcm b/src/mips/tests/spu/sine_pitch_2000.test.pcm new file mode 100644 index 000000000..e38c1fa09 Binary files /dev/null and b/src/mips/tests/spu/sine_pitch_2000.test.pcm differ diff --git a/src/mips/tests/spu/sine_pitch_3000.test.pcm b/src/mips/tests/spu/sine_pitch_3000.test.pcm new file mode 100644 index 000000000..4fabeda8a Binary files /dev/null and b/src/mips/tests/spu/sine_pitch_3000.test.pcm differ diff --git a/src/mips/tests/spu/spu-adpcm.c b/src/mips/tests/spu/spu-adpcm.c new file mode 100644 index 000000000..2d9db6f32 --- /dev/null +++ b/src/mips/tests/spu/spu-adpcm.c @@ -0,0 +1,53 @@ +// ========================================================================== +// Validate ADPCM decoding, filtering, loop and carry-over +// ========================================================================== + +CESTER_TEST(adpcm_decode_silent, spu_tests, + run_voice1_with_sample(kAdpcmSilent, 0x1000); + SPU_ASSERT_GOLDEN(silent); +) + +CESTER_TEST(adpcm_decode_sinewave, spu_tests, + run_voice1_with_sample(kAdpcmSine, 0x1000); + SPU_ASSERT_GOLDEN(sine); +) + +CESTER_TEST(adpcm_decode_sinewave_lowpitch, spu_tests, + run_voice1_with_sample(kAdpcmSine394Hz, 0x1000); + SPU_ASSERT_GOLDEN(sine_low); +) + +CESTER_TEST(adpcm_decode_sinewave_highpitch, spu_tests, + run_voice1_with_sample(kAdpcmSine5512Hz, 0x1000); + SPU_ASSERT_GOLDEN(sine_high); +) + +CESTER_TEST(adpcm_decode_tranglewave, spu_tests, + run_voice1_with_sample(kAdpcmTriangle, 0x1000); + SPU_ASSERT_GOLDEN(triangle); +) + +CESTER_TEST(adpcm_decode_squarewave, spu_tests, + run_voice1_with_sample(kAdpcmSquare, 0x1000); + SPU_ASSERT_GOLDEN(square); +) + +CESTER_TEST(adpcm_decode_with_loop, spu_tests, + run_voice1_with_sample(kAdpcmTriangle, 0x1000); + SPU_ASSERT_GOLDEN(loop_t0); + + SPU_CTRL = 0x8000 | 0x4000; + SPU_VOL_MAIN_LEFT = 0x3fff; SPU_VOL_MAIN_RIGHT = 0x3fff; + SPU_KEY_OFF_LOW = 0xffff; SPU_KEY_OFF_HIGH = 0xffff; + spu_busy_wait(800000); + spu_wait_status_bit11_flip(); + + spu_voice1_keyon(SPU_UPLOAD_ADDR, 0x1000); + spu_busy_wait(15000000); // ~440 ms, the 112-sample buffer lasts ~5 ms + spu_wait_status_bit11_flip(); + spu_read_sync(0x0800, s_capture, 1024); + SPU_ASSERT_GOLDEN(loop_t1); + + SPU_KEY_OFF_LOW = 0xffff; SPU_KEY_OFF_HIGH = 0xffff; + muteSpu(); +) diff --git a/src/mips/tests/spu/spu-adsr.c b/src/mips/tests/spu/spu-adsr.c new file mode 100644 index 000000000..aae349c0b --- /dev/null +++ b/src/mips/tests/spu/spu-adsr.c @@ -0,0 +1,655 @@ +// ========================================================================== +// Validate ADSR envelope via currentVolume changes +// ========================================================================== + +#define ATTACK(step, shift, exp) (\ + (((step) & 3) << 8) | \ + (((shift) & 31) << 10) | \ + (!!(exp) << 15)) +#define DECAY(shift) (\ + (((shift) & 15) << 4)) +#define SUSTAIN(step, shift, level, direction, exp) (\ + (((step) & 3) << 22) | \ + (((shift) & 31) << 24) | \ + (((level) & 15) << 0) | \ + (!!(direction) << 30) | \ + (!!(exp) << 31)) +#define RELEASE(shift, exp) (\ + (((shift) & 31) << 16) | \ + (!!(exp) << 21)) + +CESTER_BODY( +// Given the envelope settings, spit out a currentVolume trace. +// To avoid jitter and interference, the captured output starts only when +// the volume changes from 0. The sample rate of the ENVX is a sample every +// spu_wait_status_bit11_flip call to try separating CPU clock from the +// SPU clock and increase chances of reproducibility. +static void spu_adsr_capture( + uint32_t adsr, // packed ADSR envelope (see ATTACK/DECAY/SUSTAIN/RELEASE macros) + uint16_t* envx_out, // output buffer; must be of length n_samples + unsigned n_samples // sample count to capture per SPU status bit11 flip +) { + // drain volume from previous envelope key ON + spu_reset_quiet(); + SPU_CTRL = 0x8000 | 0x4000; + SPU_VOL_MAIN_LEFT = 0; SPU_VOL_MAIN_RIGHT = 0; + SPU_KEY_OFF_LOW = 0xffff; SPU_KEY_OFF_HIGH = 0xffff; + spu_wait_status_bit11_flip(); + while (SPU_VOICES[1].currentVolume != 0) ; + + // prepare envelope, wait for bit11 flip, then fire voice! + SPU_VOICES[1].sampleRate = 0x1000; + SPU_VOICES[1].sampleStartAddr = SPU_UPLOAD_ADDR >> 3; + SPU_VOICES[1].sampleRepeatAddr = SPU_UPLOAD_ADDR >> 3; + SPU_VOICES[1].volumeLeft = 0; + SPU_VOICES[1].volumeRight = 0; + spu_wait_status_bit11_flip(); + SPU_VOICES[1].adsrLo = (uint16_t)(adsr & 0xFFFF); + SPU_VOICES[1].adsrHi = (uint16_t)(adsr >> 16); + SPU_KEY_OFF_LOW = 0; SPU_KEY_OFF_HIGH = 0; + SPU_KEY_ON_LOW = 1u << 1; + + // synchronize SPU by waiting currentVolume to change + while (SPU_VOICES[1].currentVolume == 0) ; + + // now capture one sample every bit11 flip + envx_out[0] = SPU_VOICES[1].currentVolume; + for (unsigned i = 1; i < n_samples; i++) { + spu_wait_status_bit11_flip(); + envx_out[i] = SPU_VOICES[1].currentVolume; + } + + // key off, we finished here + SPU_KEY_OFF_LOW = 0xffff; SPU_KEY_OFF_HIGH = 0xffff; + muteSpu(); +} + +static void spu_adsr_capture_with_keyoff( + uint32_t adsr, + uint16_t* envx_out, + unsigned n_samples, + unsigned keyoff_at +) { + spu_adsr_capture(adsr, envx_out, keyoff_at + 1); + for (unsigned i = keyoff_at + 1; i < n_samples; i++) { + spu_wait_status_bit11_flip(); + envx_out[i] = SPU_VOICES[1].currentVolume; + } +} +) + +// testing an envelope with the current capturing technique in spu_adsr_capture +// does not lead to exact reproducible samples due to some minor timing +// differences between the CPU polling and the produced SPU results. +// The value step is a delta used as margin of error. +#define ASSERT_ENVX_NEAR(nominal, step, got) \ + cester_assert_true((got) >= (uint16_t)((nominal) - (step)) && \ + (got) <= (uint16_t)((nominal) + (step))) + +CESTER_TEST(adsr_attack_linear_step, spu_tests, + int i; + uint16_t envx[0x40]; + + const uint32_t base = DECAY(0) | SUSTAIN(3, 0x1f, 15, 0, 0) | RELEASE(0, 0); + + spu_adsr_capture(ATTACK(2, 12, 0) | base, envx, 4); + cester_assert_uint_eq(0x0005, envx[0]); + ASSERT_ENVX_NEAR(0x04f1, 5, envx[1]); + ASSERT_ENVX_NEAR(0x09f1, 5, envx[2]); + ASSERT_ENVX_NEAR(0x0ef1, 5, envx[3]); + + spu_adsr_capture(ATTACK(3, 12, 0) | base, envx, 4); + cester_assert_uint_eq(0x0004, envx[0]); + ASSERT_ENVX_NEAR(0x03f4, 4, envx[1]); + ASSERT_ENVX_NEAR(0x07f4, 4, envx[2]); + ASSERT_ENVX_NEAR(0x0bf4, 4, envx[3]); + + spu_adsr_capture(ATTACK(2, 24, 0) | base, envx, 48); + for (i = 0; i < 17; i++) + cester_assert_uint_eq(0x0005, envx[i]); + for (i = 17; i < 33; i++) + cester_assert_uint_eq(0x000a, envx[i]); + for (i = 33; i < 48; i++) + cester_assert_uint_eq(0x000f, envx[i]); + + spu_adsr_capture(ATTACK(3, 24, 0) | base, envx, 48); + for (i = 0; i < 17; i++) + cester_assert_uint_eq(0x0004, envx[i]); + for (i = 17; i < 33; i++) + cester_assert_uint_eq(0x0008, envx[i]); + for (i = 33; i < 48; i++) + cester_assert_uint_eq(0x000c, envx[i]); +) + +CESTER_TEST(adsr_attack_linear_shift, spu_tests, + int i; + uint16_t envx[0x40]; + + const uint32_t base = DECAY(0) | SUSTAIN(3, 0x1f, 15, 0, 0) | RELEASE(0, 0); + + spu_adsr_capture(ATTACK(0, 0, 0) | base, envx, 1); + cester_assert_uint_eq(0x3800, envx[0]); + + spu_adsr_capture(ATTACK(1, 0, 0) | base, envx, 1); + cester_assert_uint_eq(0x3000, envx[0]); + + spu_adsr_capture(ATTACK(0, 11, 0) | base, envx, 4); + cester_assert_uint_eq(0x0007, envx[0]); + ASSERT_ENVX_NEAR(0x0dcf, 7, envx[1]); + ASSERT_ENVX_NEAR(0x1bcf, 7, envx[2]); + ASSERT_ENVX_NEAR(0x29cf, 7, envx[3]); + + spu_adsr_capture(ATTACK(1, 11, 0) | base, envx, 4); + cester_assert_uint_eq(0x0006, envx[0]); + ASSERT_ENVX_NEAR(0x0bdc, 7, envx[1]); + ASSERT_ENVX_NEAR(0x17d6, 7, envx[2]); + ASSERT_ENVX_NEAR(0x23d6, 7, envx[3]); + + spu_adsr_capture(ATTACK(0, 12, 0) | base, envx, 32); + cester_assert_uint_eq(0x0007, envx[0]); + ASSERT_ENVX_NEAR(0x06e4, 7, envx[1]); + ASSERT_ENVX_NEAR(0x0de4, 7, envx[2]); + ASSERT_ENVX_NEAR(0x14e4, 7, envx[3]); + ASSERT_ENVX_NEAR(0x1be4, 7, envx[4]); + ASSERT_ENVX_NEAR(0x22e4, 7, envx[5]); + ASSERT_ENVX_NEAR(0x29e4, 7, envx[6]); + ASSERT_ENVX_NEAR(0x30e4, 7, envx[7]); + ASSERT_ENVX_NEAR(0x37e4, 7, envx[8]); + ASSERT_ENVX_NEAR(0x3ee4, 7, envx[9]); + ASSERT_ENVX_NEAR(0x45e4, 7, envx[10]); + ASSERT_ENVX_NEAR(0x4ce4, 7, envx[11]); + ASSERT_ENVX_NEAR(0x53e4, 7, envx[12]); + ASSERT_ENVX_NEAR(0x5ae4, 7, envx[13]); + ASSERT_ENVX_NEAR(0x61e4, 7, envx[14]); + ASSERT_ENVX_NEAR(0x68e4, 7, envx[15]); + ASSERT_ENVX_NEAR(0x6fe4, 7, envx[16]); + ASSERT_ENVX_NEAR(0x76e4, 7, envx[17]); + ASSERT_ENVX_NEAR(0x7de4, 7, envx[18]); + + spu_adsr_capture(ATTACK(1, 12, 0) | base, envx, 4); + cester_assert_uint_eq(0x0006, envx[0]); + ASSERT_ENVX_NEAR(0x05ee, 6, envx[1]); + ASSERT_ENVX_NEAR(0x0bee, 6, envx[2]); + ASSERT_ENVX_NEAR(0x11ee, 6, envx[3]); + + spu_adsr_capture(ATTACK(0, 23, 0) | base, envx, 48); + for (i = 0; i < 9; i++) + cester_assert_uint_eq(0x0007, envx[i]); + for (i = 9; i < 17; i++) + cester_assert_uint_eq(0x000e, envx[i]); + for (i = 17; i < 25; i++) + cester_assert_uint_eq(0x0015, envx[i]); + for (i = 25; i < 33; i++) + cester_assert_uint_eq(0x001c, envx[i]); + for (i = 33; i < 41; i++) + cester_assert_uint_eq(0x0023, envx[i]); + for (i = 41; i < 48; i++) + cester_assert_uint_eq(0x002a, envx[i]); + + spu_adsr_capture(ATTACK(1, 23, 0) | base, envx, 48); + for (i = 0; i < 9; i++) + cester_assert_uint_eq(0x0006, envx[i]); + for (i = 9; i < 17; i++) + cester_assert_uint_eq(0x000c, envx[i]); + for (i = 17; i < 25; i++) + cester_assert_uint_eq(0x0012, envx[i]); + for (i = 25; i < 33; i++) + cester_assert_uint_eq(0x0018, envx[i]); + for (i = 33; i < 41; i++) + cester_assert_uint_eq(0x001e, envx[i]); + for (i = 41; i < 48; i++) + cester_assert_uint_eq(0x0024, envx[i]); + + spu_adsr_capture(ATTACK(0, 24, 0) | base, envx, 48); + for (i = 0; i < 17; i++) + cester_assert_uint_eq(0x0007, envx[i]); + for (i = 17; i < 33; i++) + cester_assert_uint_eq(0x000e, envx[i]); + for (i = 33; i < 48; i++) + cester_assert_uint_eq(0x0015, envx[i]); + + spu_adsr_capture(ATTACK(1, 24, 0) | base, envx, 48); + for (i = 0; i < 17; i++) + cester_assert_uint_eq(0x0006, envx[i]); + for (i = 17; i < 33; i++) + cester_assert_uint_eq(0x000c, envx[i]); + for (i = 33; i < 48; i++) + cester_assert_uint_eq(0x0012, envx[i]); +) + +// Above ENVX 0x6000, exponential attack right-shifts AddStep by 2 (delta /= 4), +// stretching the tail. Below 0x6000 it is bit-identical to linear. +CESTER_TEST(adsr_attack_exponential, spu_tests, + uint16_t envx[0x40]; + + const uint32_t base = DECAY(0) | SUSTAIN(3, 0x1f, 15, 0, 0) | RELEASE(0, 0); + + spu_adsr_capture(ATTACK(0, 12, 1) | base, envx, 32); + cester_assert_uint_eq(0x0007, envx[0]); + ASSERT_ENVX_NEAR(0x06e4, 7, envx[1]); + ASSERT_ENVX_NEAR(0x0de4, 7, envx[2]); + ASSERT_ENVX_NEAR(0x14e4, 7, envx[3]); + ASSERT_ENVX_NEAR(0x1be4, 7, envx[4]); + ASSERT_ENVX_NEAR(0x22e4, 7, envx[5]); + ASSERT_ENVX_NEAR(0x29e4, 7, envx[6]); + ASSERT_ENVX_NEAR(0x30e4, 7, envx[7]); + ASSERT_ENVX_NEAR(0x37e4, 7, envx[8]); + ASSERT_ENVX_NEAR(0x3ee4, 7, envx[9]); + ASSERT_ENVX_NEAR(0x45e4, 7, envx[10]); + ASSERT_ENVX_NEAR(0x4ce4, 7, envx[11]); + ASSERT_ENVX_NEAR(0x53e4, 7, envx[12]); + ASSERT_ENVX_NEAR(0x5ae4, 7, envx[13]); + ASSERT_ENVX_NEAR(0x607f, 7, envx[14]); + ASSERT_ENVX_NEAR(0x623f, 7, envx[15]); + ASSERT_ENVX_NEAR(0x63ff, 7, envx[16]); + ASSERT_ENVX_NEAR(0x65bf, 7, envx[17]); + ASSERT_ENVX_NEAR(0x677f, 7, envx[18]); + ASSERT_ENVX_NEAR(0x693f, 7, envx[19]); + ASSERT_ENVX_NEAR(0x6aff, 7, envx[20]); + ASSERT_ENVX_NEAR(0x6cbf, 7, envx[21]); + ASSERT_ENVX_NEAR(0x6e7f, 7, envx[22]); + ASSERT_ENVX_NEAR(0x703f, 7, envx[23]); + ASSERT_ENVX_NEAR(0x71ff, 7, envx[24]); + ASSERT_ENVX_NEAR(0x73bf, 7, envx[25]); + ASSERT_ENVX_NEAR(0x757f, 7, envx[26]); + ASSERT_ENVX_NEAR(0x773f, 7, envx[27]); + ASSERT_ENVX_NEAR(0x78ff, 7, envx[28]); + ASSERT_ENVX_NEAR(0x7abf, 7, envx[29]); + ASSERT_ENVX_NEAR(0x7c7f, 7, envx[30]); + ASSERT_ENVX_NEAR(0x7e3f, 7, envx[31]); +) + +// test decay rate after attack peaks; higher value means slower decay +CESTER_TEST(adsr_decay_shift, spu_tests, + uint16_t envx[0x40]; + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(10) | SUSTAIN(0, 0, 0, 1, 0) | RELEASE(0, 1), + envx, 16); + cester_assert_uint_eq(0x1c00, envx[0]); + ASSERT_ENVX_NEAR(0x6370, 0x07, envx[1]); + ASSERT_ENVX_NEAR(0x4c95, 0x05, envx[2]); + ASSERT_ENVX_NEAR(0x3ac6, 0x04, envx[3]); + ASSERT_ENVX_NEAR(0x2cef, 0x03, envx[4]); + ASSERT_ENVX_NEAR(0x221c, 0x03, envx[5]); + ASSERT_ENVX_NEAR(0x19b0, 0x02, envx[6]); + ASSERT_ENVX_NEAR(0x1343, 0x02, envx[7]); + ASSERT_ENVX_NEAR(0x0e2d, 0x01, envx[8]); + ASSERT_ENVX_NEAR(0x0a2d, 0x01, envx[9]); + for (unsigned i = 10; i < 16; i++) + cester_assert_uint_eq(0x0000, envx[i]); + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(12) | SUSTAIN(0, 0, 0, 1, 0) | RELEASE(0, 1), + envx, 32); + cester_assert_uint_eq(0x1c00, envx[0]); + ASSERT_ENVX_NEAR(0x782b, 0x04, envx[1]); + ASSERT_ENVX_NEAR(0x702b, 0x04, envx[2]); + ASSERT_ENVX_NEAR(0x6925, 0x04, envx[3]); + ASSERT_ENVX_NEAR(0x6225, 0x04, envx[4]); + ASSERT_ENVX_NEAR(0x5bd7, 0x03, envx[5]); + ASSERT_ENVX_NEAR(0x55d7, 0x03, envx[6]); + ASSERT_ENVX_NEAR(0x4fdd, 0x03, envx[7]); + ASSERT_ENVX_NEAR(0x4add, 0x03, envx[8]); + ASSERT_ENVX_NEAR(0x45dd, 0x03, envx[9]); + ASSERT_ENVX_NEAR(0x40dd, 0x03, envx[10]); + ASSERT_ENVX_NEAR(0x3cb1, 0x02, envx[11]); + ASSERT_ENVX_NEAR(0x38b1, 0x02, envx[12]); + ASSERT_ENVX_NEAR(0x34b1, 0x02, envx[13]); + ASSERT_ENVX_NEAR(0x30b1, 0x02, envx[14]); + ASSERT_ENVX_NEAR(0x2d84, 0x02, envx[15]); + ASSERT_ENVX_NEAR(0x2a84, 0x02, envx[16]); + ASSERT_ENVX_NEAR(0x2784, 0x02, envx[17]); + ASSERT_ENVX_NEAR(0x2484, 0x02, envx[18]); + ASSERT_ENVX_NEAR(0x2184, 0x02, envx[19]); + ASSERT_ENVX_NEAR(0x1f03, 0x01, envx[20]); + ASSERT_ENVX_NEAR(0x1d03, 0x01, envx[21]); + ASSERT_ENVX_NEAR(0x1b03, 0x01, envx[22]); + ASSERT_ENVX_NEAR(0x1903, 0x02, envx[23]); + ASSERT_ENVX_NEAR(0x1703, 0x01, envx[24]); + ASSERT_ENVX_NEAR(0x1503, 0x01, envx[25]); + ASSERT_ENVX_NEAR(0x1303, 0x01, envx[26]); + ASSERT_ENVX_NEAR(0x1103, 0x01, envx[27]); + ASSERT_ENVX_NEAR(0x0f81, 0x01, envx[28]); + ASSERT_ENVX_NEAR(0x0e81, 0x01, envx[29]); + ASSERT_ENVX_NEAR(0x0d81, 0x01, envx[30]); + ASSERT_ENVX_NEAR(0x0c81, 0x01, envx[31]); + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(14) | SUSTAIN(0, 0, 0, 1, 0) | RELEASE(0, 1), + envx, 32); + cester_assert_uint_eq(0x1c00, envx[0]); + ASSERT_ENVX_NEAR(0x7e05, 0x02, envx[1]); + ASSERT_ENVX_NEAR(0x7c05, 0x02, envx[2]); + ASSERT_ENVX_NEAR(0x7805, 0x02, envx[4]); + ASSERT_ENVX_NEAR(0x7405, 0x02, envx[6]); + ASSERT_ENVX_NEAR(0x7005, 0x02, envx[8]); + ASSERT_ENVX_NEAR(0x6906, 0x02, envx[12]); + ASSERT_ENVX_NEAR(0x6206, 0x02, envx[16]); + ASSERT_ENVX_NEAR(0x5bba, 0x02, envx[20]); + ASSERT_ENVX_NEAR(0x55ba, 0x02, envx[24]); + ASSERT_ENVX_NEAR(0x4fc7, 0x02, envx[28]); + ASSERT_ENVX_NEAR(0x4c05, 0x02, envx[31]); +) + +// test to which volume level the voice sits on key on sustain +CESTER_TEST(adsr_sustain_level, spu_tests, + uint16_t envx[16]; + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(10) | SUSTAIN(3, 0x1f, 0, 1, 0) | RELEASE(0, 1), + envx, 16); + for (unsigned i = 10; i < 16; i++) + cester_assert_uint_eq(0x07ff, envx[i]); + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(10) | SUSTAIN(3, 0x1f, 7, 1, 0) | RELEASE(0, 1), + envx, 16); + for (unsigned i = 3; i < 16; i++) + cester_assert_uint_eq(0x3ffa, envx[i]); + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(10) | SUSTAIN(3, 0x1f, 11, 1, 0) | RELEASE(0, 1), + envx, 16); + for (unsigned i = 2; i < 16; i++) + cester_assert_uint_eq(0x5ff6, envx[i]); + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(10) | SUSTAIN(3, 0x1f, 14, 1, 0) | RELEASE(0, 1), + envx, 16); + for (unsigned i = 1; i < 16; i++) + cester_assert_uint_eq(0x77ff, envx[i]); + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(10) | SUSTAIN(3, 0x1f, 15, 1, 0) | RELEASE(0, 1), + envx, 16); + for (unsigned i = 1; i < 16; i++) + cester_assert_uint_eq(0x7fef, envx[i]); +) + +// sustain at various levels while direction scale positive linearly +CESTER_TEST(adsr_sustain_up_linear, spu_tests, + uint16_t envx[0x20]; + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(0) | SUSTAIN(0, 10, 15, 0, 0) | RELEASE(0, 1), + envx, 8); + cester_assert_uint_eq(0x1c00, envx[0]); + ASSERT_ENVX_NEAR(0x5b50, 0x07, envx[1]); + ASSERT_ENVX_NEAR(0x7750, 0x07, envx[2]); + for (unsigned i = 3; i < 8; i++) + cester_assert_uint_eq(0x7fff, envx[i]); + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(0) | SUSTAIN(3, 10, 15, 0, 0) | RELEASE(0, 1), + envx, 8); + cester_assert_uint_eq(0x1c00, envx[0]); + ASSERT_ENVX_NEAR(0x4f9b, 0x04, envx[1]); + ASSERT_ENVX_NEAR(0x5f9b, 0x04, envx[2]); + ASSERT_ENVX_NEAR(0x6f9b, 0x04, envx[3]); + ASSERT_ENVX_NEAR(0x7f9b, 0x04, envx[4]); + for (unsigned i = 5; i < 8; i++) + cester_assert_uint_eq(0x7fff, envx[i]); + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(0) | SUSTAIN(0, 12, 15, 0, 0) | RELEASE(0, 1), + envx, 16); + cester_assert_uint_eq(0x1c00, envx[0]); + ASSERT_ENVX_NEAR(0x46d1, 0x04, envx[1]); + ASSERT_ENVX_NEAR(0x4dd1, 0x04, envx[2]); + ASSERT_ENVX_NEAR(0x54d1, 0x04, envx[3]); + ASSERT_ENVX_NEAR(0x5bd1, 0x04, envx[4]); + ASSERT_ENVX_NEAR(0x62d1, 0x04, envx[5]); + ASSERT_ENVX_NEAR(0x69d1, 0x04, envx[6]); + ASSERT_ENVX_NEAR(0x70d1, 0x04, envx[7]); + ASSERT_ENVX_NEAR(0x77d1, 0x04, envx[8]); + ASSERT_ENVX_NEAR(0x7ed1, 0x04, envx[9]); + for (unsigned i = 10; i < 16; i++) + cester_assert_uint_eq(0x7fff, envx[i]); + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(0) | SUSTAIN(0, 14, 15, 0, 0) | RELEASE(0, 1), + envx, 32); + cester_assert_uint_eq(0x1c00, envx[0]); + ASSERT_ENVX_NEAR(0x41b8, 0x02, envx[1]); + ASSERT_ENVX_NEAR(0x4378, 0x02, envx[2]); + ASSERT_ENVX_NEAR(0x46f8, 0x02, envx[4]); + ASSERT_ENVX_NEAR(0x4df8, 0x02, envx[8]); + ASSERT_ENVX_NEAR(0x54f8, 0x02, envx[12]); + ASSERT_ENVX_NEAR(0x5bf8, 0x02, envx[16]); + ASSERT_ENVX_NEAR(0x62f8, 0x02, envx[20]); + ASSERT_ENVX_NEAR(0x69f8, 0x02, envx[24]); + ASSERT_ENVX_NEAR(0x70f8, 0x02, envx[28]); + ASSERT_ENVX_NEAR(0x7638, 0x02, envx[31]); + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(0) | SUSTAIN(0, 16, 15, 0, 0) | RELEASE(0, 1), + envx, 32); + cester_assert_uint_eq(0x1c00, envx[0]); + ASSERT_ENVX_NEAR(0x4068, 0x02, envx[1]); + ASSERT_ENVX_NEAR(0x4228, 0x02, envx[5]); + ASSERT_ENVX_NEAR(0x4458, 0x02, envx[10]); + ASSERT_ENVX_NEAR(0x4688, 0x02, envx[15]); + ASSERT_ENVX_NEAR(0x48b8, 0x02, envx[20]); + ASSERT_ENVX_NEAR(0x4ae8, 0x02, envx[25]); + ASSERT_ENVX_NEAR(0x4d88, 0x02, envx[31]); +) + +// sustain at various levels while direction scale negative linearly +CESTER_TEST(adsr_sustain_down_linear, spu_tests, + uint16_t envx[0x20]; + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(0) | SUSTAIN(3, 10, 15, 1, 0) | RELEASE(0, 1), + envx, 8); + cester_assert_uint_eq(0x1c00, envx[0]); + ASSERT_ENVX_NEAR(0x2c7c, 0x05, envx[1]); + ASSERT_ENVX_NEAR(0x187c, 0x05, envx[2]); + ASSERT_ENVX_NEAR(0x047c, 0x05, envx[3]); + for (unsigned i = 4; i < 8; i++) + cester_assert_uint_eq(0x0000, envx[i]); + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(0) | SUSTAIN(0, 12, 15, 1, 0) | RELEASE(0, 1), + envx, 16); + cester_assert_uint_eq(0x1c00, envx[0]); + ASSERT_ENVX_NEAR(0x3833, 0x04, envx[1]); + ASSERT_ENVX_NEAR(0x3033, 0x04, envx[2]); + ASSERT_ENVX_NEAR(0x2833, 0x04, envx[3]); + ASSERT_ENVX_NEAR(0x2033, 0x04, envx[4]); + ASSERT_ENVX_NEAR(0x1833, 0x04, envx[5]); + ASSERT_ENVX_NEAR(0x1033, 0x04, envx[6]); + ASSERT_ENVX_NEAR(0x0833, 0x04, envx[7]); + ASSERT_ENVX_NEAR(0x0033, 0x04, envx[8]); + for (unsigned i = 9; i < 16; i++) + cester_assert_uint_eq(0x0000, envx[i]); + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(0) | SUSTAIN(0, 14, 15, 1, 0) | RELEASE(0, 1), + envx, 32); + cester_assert_uint_eq(0x1c00, envx[0]); + ASSERT_ENVX_NEAR(0x3e05, 0x02, envx[1]); + ASSERT_ENVX_NEAR(0x3c05, 0x02, envx[2]); + ASSERT_ENVX_NEAR(0x3805, 0x02, envx[4]); + ASSERT_ENVX_NEAR(0x3005, 0x02, envx[8]); + ASSERT_ENVX_NEAR(0x2805, 0x02, envx[12]); + ASSERT_ENVX_NEAR(0x2005, 0x02, envx[16]); + ASSERT_ENVX_NEAR(0x1805, 0x02, envx[20]); + ASSERT_ENVX_NEAR(0x1005, 0x02, envx[24]); + ASSERT_ENVX_NEAR(0x0805, 0x02, envx[28]); + ASSERT_ENVX_NEAR(0x0205, 0x02, envx[31]); + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(0) | SUSTAIN(0, 16, 15, 1, 0) | RELEASE(0, 1), + envx, 32); + cester_assert_uint_eq(0x1c00, envx[0]); + ASSERT_ENVX_NEAR(0x3f85, 0x02, envx[1]); + ASSERT_ENVX_NEAR(0x3e85, 0x02, envx[3]); + ASSERT_ENVX_NEAR(0x3d05, 0x02, envx[6]); + ASSERT_ENVX_NEAR(0x3b85, 0x02, envx[9]); + ASSERT_ENVX_NEAR(0x3a05, 0x02, envx[12]); + ASSERT_ENVX_NEAR(0x3885, 0x02, envx[15]); + ASSERT_ENVX_NEAR(0x3705, 0x02, envx[18]); + ASSERT_ENVX_NEAR(0x3585, 0x02, envx[21]); + ASSERT_ENVX_NEAR(0x3405, 0x02, envx[24]); + ASSERT_ENVX_NEAR(0x3285, 0x02, envx[27]); + ASSERT_ENVX_NEAR(0x3105, 0x02, envx[30]); +) + +// sustain at various levels while direction scale positive exponentially +CESTER_TEST(adsr_sustain_up_exponential, spu_tests, + uint16_t envx[0x20]; + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(0) | SUSTAIN(0, 12, 15, 0, 1) | RELEASE(0, 1), + envx, 24); + cester_assert_uint_eq(0x1c00, envx[0]); + ASSERT_ENVX_NEAR(0x46d1, 0x04, envx[1]); + ASSERT_ENVX_NEAR(0x4dd1, 0x04, envx[2]); + ASSERT_ENVX_NEAR(0x54d1, 0x04, envx[3]); + ASSERT_ENVX_NEAR(0x5bd1, 0x04, envx[4]); + ASSERT_ENVX_NEAR(0x60ba, 0x02, envx[5]); + ASSERT_ENVX_NEAR(0x627a, 0x02, envx[6]); + ASSERT_ENVX_NEAR(0x643a, 0x02, envx[7]); + ASSERT_ENVX_NEAR(0x65fa, 0x02, envx[8]); + ASSERT_ENVX_NEAR(0x67ba, 0x02, envx[9]); + ASSERT_ENVX_NEAR(0x6cfa, 0x02, envx[12]); + ASSERT_ENVX_NEAR(0x73fa, 0x02, envx[16]); + ASSERT_ENVX_NEAR(0x7afa, 0x02, envx[20]); + cester_assert_uint_eq(0x7fff, envx[23]); + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(0) | SUSTAIN(0, 14, 15, 0, 1) | RELEASE(0, 1), + envx, 32); + cester_assert_uint_eq(0x1c00, envx[0]); + ASSERT_ENVX_NEAR(0x41b8, 0x02, envx[1]); + ASSERT_ENVX_NEAR(0x4df8, 0x02, envx[8]); + ASSERT_ENVX_NEAR(0x5bf8, 0x02, envx[16]); + ASSERT_ENVX_NEAR(0x6051, 0x02, envx[19]); + ASSERT_ENVX_NEAR(0x60c1, 0x02, envx[20]); + ASSERT_ENVX_NEAR(0x6211, 0x02, envx[23]); + ASSERT_ENVX_NEAR(0x6361, 0x02, envx[26]); + ASSERT_ENVX_NEAR(0x6591, 0x02, envx[31]); +) + +// sustain at various levels while direction scale negative exponentially +CESTER_TEST(adsr_sustain_down_exponential, spu_tests, + uint16_t envx[0x20]; + + spu_adsr_capture( + ATTACK(0, 1, 0) | DECAY(0) | SUSTAIN(0, 12, 15, 1, 1) | RELEASE(0, 1), + envx, 32); + cester_assert_uint_eq(0x1c00, envx[0]); + // Multiplicative decrease — deltas shrink as EnvVol drops. + ASSERT_ENVX_NEAR(0x3c19, 0x02, envx[1]); + ASSERT_ENVX_NEAR(0x3819, 0x02, envx[2]); + ASSERT_ENVX_NEAR(0x3419, 0x02, envx[3]); + ASSERT_ENVX_NEAR(0x3019, 0x02, envx[4]); + ASSERT_ENVX_NEAR(0x2d12, 0x02, envx[5]); + ASSERT_ENVX_NEAR(0x2a12, 0x02, envx[6]); + ASSERT_ENVX_NEAR(0x2712, 0x02, envx[7]); + ASSERT_ENVX_NEAR(0x2412, 0x02, envx[8]); + ASSERT_ENVX_NEAR(0x2112, 0x02, envx[9]); + ASSERT_ENVX_NEAR(0x1eb7, 0x01, envx[10]); + ASSERT_ENVX_NEAR(0x1ab7, 0x01, envx[12]); + ASSERT_ENVX_NEAR(0x16b7, 0x01, envx[14]); + ASSERT_ENVX_NEAR(0x12b7, 0x01, envx[16]); + ASSERT_ENVX_NEAR(0x0f5b, 0x01, envx[18]); + ASSERT_ENVX_NEAR(0x0c5b, 0x01, envx[21]); + ASSERT_ENVX_NEAR(0x095b, 0x01, envx[24]); + ASSERT_ENVX_NEAR(0x065b, 0x01, envx[27]); + ASSERT_ENVX_NEAR(0x035b, 0x01, envx[30]); +) + +// release linear, capture samples as soon as key goes off +CESTER_TEST(adsr_release_linear, spu_tests, + uint16_t envx[0x20]; + + spu_adsr_capture_with_keyoff( + ATTACK(0, 1, 0) | DECAY(15) | + SUSTAIN(3, 0x1f, 15, 0, 0) | RELEASE(12, 0), + envx, 24, 3); + cester_assert_uint_eq(0x1c00, envx[0]); + cester_assert_uint_eq(0x7ff7, envx[1]); + cester_assert_uint_eq(0x7ff7, envx[2]); + cester_assert_uint_eq(0x7ff7, envx[3]); + ASSERT_ENVX_NEAR(0x77fb, 0x04, envx[4]); + ASSERT_ENVX_NEAR(0x6ffb, 0x04, envx[5]); + ASSERT_ENVX_NEAR(0x67fb, 0x04, envx[6]); + ASSERT_ENVX_NEAR(0x5ffb, 0x04, envx[7]); + ASSERT_ENVX_NEAR(0x57fb, 0x04, envx[8]); + ASSERT_ENVX_NEAR(0x4ffb, 0x04, envx[9]); + ASSERT_ENVX_NEAR(0x47fb, 0x04, envx[10]); + ASSERT_ENVX_NEAR(0x3ffb, 0x04, envx[11]); + ASSERT_ENVX_NEAR(0x37fb, 0x04, envx[12]); + ASSERT_ENVX_NEAR(0x2ffb, 0x04, envx[13]); + ASSERT_ENVX_NEAR(0x27fb, 0x04, envx[14]); + ASSERT_ENVX_NEAR(0x1ffb, 0x04, envx[15]); + ASSERT_ENVX_NEAR(0x17fb, 0x04, envx[16]); + ASSERT_ENVX_NEAR(0x0ffb, 0x04, envx[17]); + ASSERT_ENVX_NEAR(0x07fb, 0x04, envx[18]); + for (unsigned i = 19; i < 24; i++) + cester_assert_uint_eq(0x0000, envx[i]); + + spu_adsr_capture_with_keyoff( + ATTACK(0, 1, 0) | DECAY(15) | + SUSTAIN(3, 0x1f, 15, 0, 0) | RELEASE(14, 0), + envx, 32, 3); + cester_assert_uint_eq(0x1c00, envx[0]); + ASSERT_ENVX_NEAR(0x7ffb, 0x04, envx[3]); + ASSERT_ENVX_NEAR(0x7dfb, 0x04, envx[4]); + ASSERT_ENVX_NEAR(0x7bfb, 0x04, envx[5]); + ASSERT_ENVX_NEAR(0x77fb, 0x04, envx[7]); + ASSERT_ENVX_NEAR(0x6ffb, 0x04, envx[11]); + ASSERT_ENVX_NEAR(0x67fb, 0x04, envx[15]); + ASSERT_ENVX_NEAR(0x5ffb, 0x04, envx[19]); + ASSERT_ENVX_NEAR(0x57fb, 0x04, envx[23]); + ASSERT_ENVX_NEAR(0x4ffb, 0x04, envx[27]); + ASSERT_ENVX_NEAR(0x47fb, 0x04, envx[31]); + + spu_adsr_capture_with_keyoff( + ATTACK(0, 1, 0) | DECAY(15) | + SUSTAIN(3, 0x1f, 15, 0, 0) | RELEASE(16, 0), + envx, 16, 3); + cester_assert_uint_eq(0x1c00, envx[0]); + ASSERT_ENVX_NEAR(0x7ffb, 0x04, envx[3]); + ASSERT_ENVX_NEAR(0x7f7b, 0x04, envx[4]); + ASSERT_ENVX_NEAR(0x7efb, 0x04, envx[5]); + ASSERT_ENVX_NEAR(0x7dfb, 0x04, envx[7]); + ASSERT_ENVX_NEAR(0x7bfb, 0x04, envx[11]); + ASSERT_ENVX_NEAR(0x79fb, 0x04, envx[15]); +) + +// release exponential, capture samples as soon as key goes off +CESTER_TEST(adsr_release_exponential, spu_tests, + uint16_t envx[0x20]; + + spu_adsr_capture_with_keyoff( + ATTACK(0, 1, 0) | DECAY(15) | + SUSTAIN(3, 0x1f, 15, 0, 0) | RELEASE(12, 1), + envx, 24, 2); + cester_assert_uint_eq(0x1c00, envx[0]); + cester_assert_uint_eq(0x7ff7, envx[1]); + cester_assert_uint_eq(0x7ff7, envx[2]); + ASSERT_ENVX_NEAR(0x77fb, 0x04, envx[3]); + ASSERT_ENVX_NEAR(0x6ffb, 0x04, envx[4]); + ASSERT_ENVX_NEAR(0x68fb, 0x04, envx[5]); + ASSERT_ENVX_NEAR(0x61fb, 0x04, envx[6]); + ASSERT_ENVX_NEAR(0x5bb3, 0x03, envx[7]); + ASSERT_ENVX_NEAR(0x55b3, 0x03, envx[8]); + ASSERT_ENVX_NEAR(0x4fbf, 0x03, envx[9]); + ASSERT_ENVX_NEAR(0x4abf, 0x03, envx[10]); + ASSERT_ENVX_NEAR(0x45bf, 0x03, envx[11]); + ASSERT_ENVX_NEAR(0x40bf, 0x03, envx[12]); + ASSERT_ENVX_NEAR(0x3c99, 0x02, envx[13]); + ASSERT_ENVX_NEAR(0x3899, 0x02, envx[14]); + ASSERT_ENVX_NEAR(0x3499, 0x02, envx[15]); + ASSERT_ENVX_NEAR(0x3099, 0x02, envx[16]); + ASSERT_ENVX_NEAR(0x2d72, 0x02, envx[17]); + ASSERT_ENVX_NEAR(0x2a72, 0x02, envx[18]); + ASSERT_ENVX_NEAR(0x2772, 0x02, envx[19]); + ASSERT_ENVX_NEAR(0x2472, 0x02, envx[20]); + ASSERT_ENVX_NEAR(0x2172, 0x02, envx[21]); + ASSERT_ENVX_NEAR(0x1ef7, 0x01, envx[22]); + ASSERT_ENVX_NEAR(0x1cf7, 0x01, envx[23]); +) diff --git a/src/mips/tests/spu/spu-capture.c b/src/mips/tests/spu/spu-capture.c new file mode 100644 index 000000000..3b407d236 --- /dev/null +++ b/src/mips/tests/spu/spu-capture.c @@ -0,0 +1,23 @@ +// ========================================================================== +// Validate voice resampling +// ========================================================================== + +CESTER_TEST(voice_sample_rates, spu_tests, + run_voice1_with_sample(kAdpcmSine, 0x0800); + SPU_ASSERT_GOLDEN(sine_pitch_0800); + run_voice1_with_sample(kAdpcmSine, 0x2000); + SPU_ASSERT_GOLDEN(sine_pitch_2000); + run_voice1_with_sample(kAdpcmSine, 0x3000); + SPU_ASSERT_GOLDEN(sine_pitch_3000); +) + +CESTER_TEST(voice_volume_does_not_affect_capture, spu_tests, + SPU_VOICES[1].volumeLeft = 0x3fff; + SPU_VOICES[1].volumeRight = 0x3fff; + run_voice1_with_sample(kAdpcmTriangle, 0x1000); + SPU_ASSERT_GOLDEN(triangle); + SPU_VOICES[1].volumeLeft = 0; + SPU_VOICES[1].volumeRight = 0; + run_voice1_with_sample(kAdpcmTriangle, 0x1000); + SPU_ASSERT_GOLDEN(triangle); +) diff --git a/src/mips/tests/spu/spu-transfer.c b/src/mips/tests/spu/spu-transfer.c new file mode 100644 index 000000000..dc1504784 --- /dev/null +++ b/src/mips/tests/spu/spu-transfer.c @@ -0,0 +1,67 @@ +// ========================================================================== +// Validate DMA transfer between Main Memory and SPU +// ========================================================================== + +CESTER_TEST(transfer_via_dma, spu_tests, + int fail_count = 0; + const uint32_t addrs[3] = { 0x01040, 0x40000, 0x7fec0 }; + const uint8_t seeds[3] = { 0x11, 0x22, 0x33 }; + + for (int t = 0; t < 3; t++) { + spu_reset_quiet(); + + for (int i = 0; i < 256; i++) s_readback[i] = (uint8_t)(seeds[t] ^ (i * 7)); + for (int i = 256; i < 320; i++) s_readback[i] = 0xaa; + spu_write_sync(addrs[t], s_readback, 320); + + for (int i = 0; i < 256; i++) s_readback[i] = 0xCC; + spu_read_sync(addrs[t], s_readback, 256); + + int local_fail = 0; + for (int i = 0; i < 256; i++) { + uint8_t expected = (uint8_t)(seeds[t] ^ (i * 7)); + if (s_readback[i] != expected) local_fail++; + } + fail_count += local_fail; + } + cester_assert_int_eq(fail_count, 0); +) + +CESTER_TEST(transfer_to_reserved_sdk_region, spu_tests, + // PSY-Q SDK claims 0x1000-0x100F is reserved + spu_reset_quiet(); + for (int i = 0; i < 128; i++) s_upload[i] = (uint8_t)(0xa5 ^ i); + spu_write_sync(0x1000, s_upload, 128); + spu_busy_wait(500000); + spu_read_sync(0x1000, s_readback, 0x80); + + int matches_reserved = 0; + for (int i = 0; i < 0x10; i++) + if (s_readback[i] == s_upload[i]) matches_reserved++; + int matches_after = 0; + for (int i = 0x10; i < 0x40; i++) + if (s_readback[i] == s_upload[i]) matches_after++; + cester_assert_int_eq(matches_reserved, 16); + cester_assert_int_eq(matches_after, 48); +) + +CESTER_TEST(transfer_to_capture_region, spu_tests, + spu_reset_quiet(); + spu_busy_wait(2000000); + + spu_read_sync(0x0000, s_readback, 0x1000); + unsigned nonzero_silenced = 0; + for (int i = 0; i < 0x1000; i++) if (s_readback[i] != 0) nonzero_silenced++; + cester_assert_uint_eq(0, nonzero_silenced); + + for (int i = 0; i < 0x1000; i++) s_readback[i] = (uint8_t)(0x55 ^ (i & 0xff)); + spu_write_sync(0x0000, s_readback, 0x1000); + spu_busy_wait(2000000); + + static uint8_t s_phase3[0x1000] __attribute__((aligned(4))); + spu_read_sync(0x0000, s_phase3, 0x1000); + unsigned matches = 0; + for (int i = 0; i < 0x1000; i++) + if (s_phase3[i] == (uint8_t)(0x55 ^ (i & 0xff))) matches++; + cester_assert_uint_le(16, matches); +) diff --git a/src/mips/tests/spu/spu.c b/src/mips/tests/spu/spu.c new file mode 100644 index 000000000..8b8a35327 --- /dev/null +++ b/src/mips/tests/spu/spu.c @@ -0,0 +1,365 @@ +/* + +MIT License + +Copyright (c) 2026 PCSX-Redux authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#include "common/hardware/dma.h" +#include "common/hardware/hwregs.h" +#include "common/hardware/spu.h" +#include "common/kernel/pcdrv.h" +#include "common/syscalls/syscalls.h" +#include + +#undef unix +#define CESTER_NO_SIGNAL +#define CESTER_NO_TIME +#define EXIT_SUCCESS 0 +#define EXIT_FAILURE 1 +#include "exotic/cester.h" + +#ifndef SPU_TEST_HELPERS_DEFINED +#define SPU_TEST_HELPERS_DEFINED + +#define INCLUDE_PCM(NAME) \ + asm( \ + ".pushsection .rodata\n" \ + ".global " #NAME "\n" \ + ".align 2\n" \ + #NAME ":\n" \ + ".incbin \"" #NAME ".test.pcm\"\n"\ + ".popsection"); \ + extern const uint8_t NAME[] + +INCLUDE_PCM(silent); +INCLUDE_PCM(sine_low); +INCLUDE_PCM(sine); +INCLUDE_PCM(sine_high); +INCLUDE_PCM(sine_pitch_0800); +INCLUDE_PCM(sine_pitch_2000); +INCLUDE_PCM(sine_pitch_3000); +INCLUDE_PCM(triangle); +INCLUDE_PCM(square); +INCLUDE_PCM(loop_t0); +INCLUDE_PCM(loop_t1); + +// ADPCM samples, each looping back to start with filter=0, shift=0, +// all sampled at 1575Hz unless otherwise specified. +static const uint8_t kAdpcmSilent[64] __attribute__((aligned(4))) = { + 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; +static const uint8_t kAdpcmSine[64] __attribute__((aligned(4))) = { + 0x00, 0x06, 0x10, 0x43, 0x76, 0x77, 0x77, 0x46, 0x13, 0xe0, 0xbc, 0x89, 0x88, 0x88, 0xb9, 0xec, + 0x00, 0x00, 0x10, 0x43, 0x76, 0x77, 0x77, 0x46, 0x13, 0xe0, 0xbc, 0x89, 0x88, 0x88, 0xb9, 0xec, + 0x00, 0x00, 0x10, 0x43, 0x76, 0x77, 0x77, 0x46, 0x13, 0xe0, 0xbc, 0x89, 0x88, 0x88, 0xb9, 0xec, + 0x00, 0x03, 0x10, 0x43, 0x76, 0x77, 0x77, 0x46, 0x13, 0xe0, 0xbc, 0x89, 0x88, 0x88, 0xb9, 0xec, +}; +static const uint8_t kAdpcmTriangle[64] __attribute__((aligned(4))) = { + 0x00, 0x06, 0x10, 0x32, 0x54, 0x76, 0x56, 0x34, 0x12, 0xe0, 0xcd, 0xab, 0x89, 0xa9, 0xcb, 0xed, + 0x00, 0x00, 0x10, 0x32, 0x54, 0x76, 0x56, 0x34, 0x12, 0xe0, 0xcd, 0xab, 0x89, 0xa9, 0xcb, 0xed, + 0x00, 0x00, 0x10, 0x32, 0x54, 0x76, 0x56, 0x34, 0x12, 0xe0, 0xcd, 0xab, 0x89, 0xa9, 0xcb, 0xed, + 0x00, 0x03, 0x10, 0x32, 0x54, 0x76, 0x56, 0x34, 0x12, 0xe0, 0xcd, 0xab, 0x89, 0xa9, 0xcb, 0xed, +}; +static const uint8_t kAdpcmSquare[64] __attribute__((aligned(4))) = { + 0x00, 0x06, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x00, 0x00, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x00, 0x00, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x00, 0x03, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, +}; +static const uint8_t kAdpcmSine394Hz[64] __attribute__((aligned(4))) = { // low-pitch sine wave + 0x00, 0x06, 0x00, 0x10, 0x21, 0x32, 0x33, 0x44, 0x54, 0x55, 0x66, 0x76, 0x77, 0x77, 0x77, 0x77, + 0x00, 0x00, 0x77, 0x77, 0x77, 0x77, 0x77, 0x66, 0x56, 0x55, 0x44, 0x34, 0x33, 0x22, 0x11, 0x00, + 0x00, 0x00, 0xf0, 0xef, 0xde, 0xcd, 0xcc, 0xbb, 0xab, 0xaa, 0x99, 0x89, 0x88, 0x88, 0x88, 0x88, + 0x00, 0x03, 0x88, 0x88, 0x88, 0x88, 0x88, 0x99, 0xa9, 0xaa, 0xbb, 0xcb, 0xcc, 0xdd, 0xee, 0xff, +}; +static const uint8_t kAdpcmSine5512Hz[64] __attribute__((aligned(4))) = { // high-pitch sine wave + 0x00, 0x06, 0x50, 0x57, 0xa0, 0xa8, 0x50, 0x57, 0xa0, 0xa8, 0x50, 0x57, 0xa0, 0xa8, 0x50, 0x57, + 0x00, 0x00, 0xa0, 0xa8, 0x50, 0x57, 0xa0, 0xa8, 0x50, 0x57, 0xa0, 0xa8, 0x50, 0x57, 0xa0, 0xa8, + 0x00, 0x00, 0x50, 0x57, 0xa0, 0xa8, 0x50, 0x57, 0xa0, 0xa8, 0x50, 0x57, 0xa0, 0xa8, 0x50, 0x57, + 0x00, 0x03, 0xa0, 0xa8, 0x50, 0x57, 0xa0, 0xa8, 0x50, 0x57, 0xa0, 0xa8, 0x50, 0x57, 0xa0, 0xa8, +}; + +typedef struct { + uint32_t magic; // 'PCMT' header magic code + uint32_t length; // size of this header, useful for versioning+expandability + uint32_t warmup; // ADPCM-decoded bytes with artifacts from a previous run + uint32_t period; // repeatable ADPCM-decoded bytes after warm-up +} PcmTestHeader; + +// when defined, pcm test data is generated instead of testing it. +// useful for either creating new tests or for diff-ing failures. +#ifdef SPU_DUMP +static int is_pcdrv_init = 0; +static void spu_dump(const char *name, const void *buf, int len) { + if (!is_pcdrv_init) { + PCinit(); + is_pcdrv_init = 1; + } + int fd = PCcreat(name, 0); + if (fd < 0) return; + PCwrite(fd, buf, len); + PCclose(fd); +} + +static void spu_pcm_analyze(const uint8_t *data, uint32_t L, + uint32_t *out_warmup, uint32_t *out_period) { + uint32_t best_kept = L, best_w = 0, best_p = L; + for (uint32_t period = 2; period <= L / 2; period += 2) { + uint32_t i = L - period; + for (;;) { + if (i < period) break; + // Compare data[i-period .. i) against data[L-period .. L). + int eq = 1; + for (uint32_t k = 0; k < period; k++) { + if (data[i - period + k] != data[L - period + k]) { eq = 0; break; } + } + if (!eq) break; + i -= period; + } + uint32_t warmup = i; + uint32_t kept = warmup + period; + if (kept < best_kept) { best_kept = kept; best_w = warmup; best_p = period; } + } + *out_warmup = best_w; + *out_period = best_p; +} +static void spu_dump_pcm(const char *name, const void *capture, uint32_t len) { + static PcmTestHeader hdr = {0x544d4350u, sizeof(PcmTestHeader), 0, 0}; + spu_pcm_analyze((const uint8_t *)capture, len, &hdr.warmup, &hdr.period); + if (!is_pcdrv_init) { + PCinit(); + is_pcdrv_init = 1; + } + int fd = PCcreat(name, 0); + if (fd < 0) return; + PCwrite(fd, &hdr, sizeof(hdr)); + PCwrite(fd, capture, hdr.warmup + hdr.period); + PCclose(fd); +} +#endif + +// COM_DELAY at 0x1F801014. SDK programs upper nibble before every SPU DMA. +#define SPU_DELAY (*(volatile uint32_t *)0xbf801014) +#define SPU_UPLOAD_ADDR 0x1080 + +static void spu_dma_sync(uint32_t spuByteAddr, uint32_t cpuAddr, uint32_t bytes, + int isRead) { + const uint16_t tsa = (uint16_t)(spuByteAddr >> 3); + const uint16_t modeBits = isRead ? 0x0030 : 0x0020; + const uint32_t delay = isRead ? 0x22000000 : 0x20000000; + const uint32_t chcr = isRead ? 0x01000200 : 0x01000201; + + SPU_RAM_DTA = tsa; + for (int i = 0; i < 0xF01 && (SPU_RAM_DTA & 0xffff) != tsa; i++) ; + SPU_CTRL = (SPU_CTRL & ~0x0030) | modeBits; + for (volatile int i = 0; i < 60; i++) ; + for (int i = 0; i < 0xF01 && (SPU_STATUS & 0x30) != modeBits; i++) ; + SPU_DELAY = (SPU_DELAY & 0xf0ffffff) | delay; + + uint32_t blocks = (bytes >> 6) + ((bytes & 0x3f) ? 1 : 0); + DMA_CTRL[DMA_SPU].MADR = cpuAddr & 0x1fffffff; + DMA_CTRL[DMA_SPU].BCR = (blocks << 16) | 0x10; + DMA_CTRL[DMA_SPU].CHCR = chcr; + while ((DMA_CTRL[DMA_SPU].CHCR & 0x01000000) != 0) __asm__ volatile(""); + + SPU_CTRL = (SPU_CTRL & ~0x0030); + for (volatile int i = 0; i < 60; i++) ; +} + +static inline void spu_write_sync(uint32_t spuAddr, const void *src, uint32_t bytes) { + spu_dma_sync(spuAddr, (uint32_t)src, bytes, 0); +} +static inline void spu_read_sync(uint32_t spuAddr, void *dst, uint32_t bytes) { + spu_dma_sync(spuAddr, (uint32_t)dst, bytes, 1); +} + +__attribute__((noinline)) +static void spu_busy_wait(unsigned cycles) { + // waste time on CPU, 3 cycles per loop + unsigned n = cycles / 3; + if (n == 0) return; + __asm__ volatile ( + "1: addiu %0, %0, -1 \n" + " bnez %0, 1b \n" + " nop \n" + : "+r"(n) : : "memory" + ); +} + +static inline void spu_wait_status_bit11_flip(void) { + while (!(SPU_STATUS & 0x0800)); + while ((SPU_STATUS & 0x0800)); +} + +static void spu_reset_quiet(void) { + DPCR |= 0x000b0000; + SPU_CTRL = 0; + SPU_VOL_MAIN_LEFT = 0; SPU_VOL_MAIN_RIGHT = 0; + SPU_REVERB_LEFT = 0; SPU_REVERB_RIGHT = 0; + SPU_KEY_OFF_LOW = 0xffff; SPU_KEY_OFF_HIGH = 0xffff; + SPU_PITCH_MOD_LOW = 0; SPU_PITCH_MOD_HIGH = 0; + SPU_NOISE_EN_LOW = 0; SPU_NOISE_EN_HIGH = 0; + SPU_REVERB_EN_LOW = 0; SPU_REVERB_EN_HIGH = 0; + // Push reverb work area to the very top of SPU RAM so its writes don't + // overlap any test address. + SPU_REVERB_ADDR = 0xffff; + SPU_RAM_DTC = 4; + SPU_CTRL = 0x8000; +} + +// Configure voice 1 with envelope at peak from the very first sample so the +// capture mirror is the raw decoded waveform. +static void spu_voice1_keyon(uint32_t spuAddr, uint16_t pitch) { + SPU_VOICES[1].sampleRate = pitch; + SPU_VOICES[1].sampleStartAddr = spuAddr >> 3; + SPU_VOICES[1].sampleRepeatAddr = spuAddr >> 3; + SPU_VOICES[1].adsrLo = 0x000f; // instant attack, sustain level=0xF + SPU_VOICES[1].adsrHi = 0x1fc0; // sustain rate=0x7F, increase, linear + SPU_KEY_OFF_LOW = 0; SPU_KEY_OFF_HIGH = 0; + SPU_KEY_ON_LOW = 1u << 1; +} + +// Compare a captured waveform against a golden, tolerating the SPU's capture- +// start jitter. The bit-11 sync pins the start to within ~1 sample, landing on +// one of two adjacent boundaries (deterministic per power-on, and the boundary +// differs across PS1 revisions). The captured signal is otherwise bit-identical +// to the golden, so find the best-aligning integer-sample shift in a tiny window +// and then require an exact match at that shift. Samples are 16-bit. +#define SPU_GOLDEN_MAXSHIFT 2 // samples; observed jitter only ever lands in {-1,0,+1} +static int spu_compare_golden(const char *name, const void *cap, + const uint8_t *golden_file) { + const PcmTestHeader *h = (const PcmTestHeader *)golden_file; + const int16_t *golden = (const int16_t *)(golden_file + h->length); + const int16_t *a = (const int16_t *)cap; + const int warmupS = (int)(h->warmup / 2); + const int periodS = (int)(h->period / 2); + const int keptS = warmupS + periodS; + + int bestShift = 0, bestBad = 0x7fffffff; + for (int s = -SPU_GOLDEN_MAXSHIFT; s <= SPU_GOLDEN_MAXSHIFT; s++) { + int bad = 0; + for (int i = 0; i < keptS; i++) { + int j = i + s; + if (j < 0 || j >= 512) continue; + if (a[j] != golden[i]) bad++; + } + if (bad < bestBad) { + bestBad = bad; + bestShift = s; + } + } + + for (int i = 0; i < keptS; i++) { + int j = i + bestShift; + if (j < 0 || j >= 512) continue; + if (a[j] != golden[i]) { + ramsyscall_printf("%s mismatch at sample %d (shift %d): got 0x%04x, want 0x%04x\n", + name, i, bestShift, (uint16_t)a[j], (uint16_t)golden[i]); + return i + 1; + } + } + for (int i = keptS + SPU_GOLDEN_MAXSHIFT; i < 512; i++) { + if (a[i] != a[i - periodS]) { + ramsyscall_printf("%s periodicity broken at sample %d: got 0x%04x, want 0x%04x\n", + name, i, (uint16_t)a[i], (uint16_t)a[i - periodS]); + return i + 1; + } + } + return 0; +} + +#ifdef SPU_DUMP +#define SPU_ASSERT_GOLDEN(name) spu_dump_pcm(#name ".test.pcm", s_capture, 1024) +#else +#define SPU_ASSERT_GOLDEN(name) \ + cester_assert_int_eq(0, spu_compare_golden(#name, s_capture, name)) +#endif + +#endif + +// clang-format off + +CESTER_BODY( +// All buffers in main RAM - the DMAC cannot target the CPU scratchpad. +static uint8_t s_upload[64 + 64] __attribute__((aligned(4))); +static uint16_t s_capture[512] __attribute__((aligned(4))); +static uint8_t s_readback[4096] __attribute__((aligned(4))); + +// Deterministic voice1 capture being sampled at 44.1kHz in the SPU RAM. +// Reading at an arbitrary moment gives a phase-rotated snapshot that differs +// at every run. To get deterministic captures, this function will: +// 1. key off + drain so voice 1's capture region becomes all-zero +// 2. wait for SPUSTAT.bit11 to transition 1→0 for the capture to start +// 3. key on voice 1 immediately after the edge +// 4. wait for the next 1→0 edge for the full capture to complete for voice1 +// 5. read the captured voice1 buffer ring via DMA +// The result is deterministic modulo at most 1 sample of jitter from the +// CPU spin vs SPU sample-clock granularity. +static void run_voice1_with_sample(const uint8_t *sample64, uint16_t pitch) { + spu_reset_quiet(); + for (int i = 0; i < 64; i++) s_upload[i] = sample64[i]; + for (int i = 64; i < 128; i++) s_upload[i] = 0xaa; + spu_write_sync(SPU_UPLOAD_ADDR, s_upload, 128); + SPU_CTRL = 0x8000 | 0x4000; + SPU_VOL_MAIN_LEFT = 0x3fff; SPU_VOL_MAIN_RIGHT = 0x3fff; + + // Drain voice 1 capture ring to all-zero (~12 ms covers >1 full lap). + SPU_KEY_OFF_LOW = 0xffff; SPU_KEY_OFF_HIGH = 0xffff; + spu_busy_wait(800000); + + spu_wait_status_bit11_flip(); + spu_voice1_keyon(SPU_UPLOAD_ADDR, pitch); + spu_wait_status_bit11_flip(); + + spu_read_sync(0x0800, s_capture, 1024); + SPU_KEY_OFF_LOW = 0xffff; SPU_KEY_OFF_HIGH = 0xffff; + muteSpu(); +} +) + +CESTER_BODY( +static int s_interruptsWereEnabled; +) + +// The bit-11 capture sync and the busy-wait-timed envelope reads are both +// jitter-sensitive: an interrupt (Unirom's SIO/timer/vblank handlers) firing +// mid-measurement perturbs the timing, mis-syncing a capture or pushing an +// envx read past its tolerance. Disable interrupts for the suite, as the +// cop0/cpu/timer tests do. +CESTER_BEFORE_ALL(spu_tests, + s_interruptsWereEnabled = enterCriticalSection(); + IMASK = 0; + IREG = 0; +) + +CESTER_AFTER_ALL(spu_tests, + if (s_interruptsWereEnabled) leaveCriticalSection(); +) + +#include "spu-transfer.c" +#include "spu-adpcm.c" +#include "spu-capture.c" +#include "spu-adsr.c" diff --git a/src/mips/tests/spu/square.test.pcm b/src/mips/tests/spu/square.test.pcm new file mode 100644 index 000000000..c237e7383 Binary files /dev/null and b/src/mips/tests/spu/square.test.pcm differ diff --git a/src/mips/tests/spu/triangle.test.pcm b/src/mips/tests/spu/triangle.test.pcm new file mode 100644 index 000000000..959173213 Binary files /dev/null and b/src/mips/tests/spu/triangle.test.pcm differ