Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/internal/upstream/martin-pr16-spu-gaussian.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Martin Penkava PR #16: SPU Gaussian interpolation

Last evaluated: 2026-07-14

| Item | Value |
| --- | --- |
| Head repository/branch | `shaneomac1337/psxrecomp`, `smackdown2-fixes` (submitted as `mstan/psxrecomp` PR #16) |
| Source commit | `e069c8a531b14c2ec4566d6a3dd5b1816a5600e6` |
| Source author | Martin Penkava `<mpenkava1337@gmail.com>` |
| Source co-author trailer | Claude Fable 5 `<noreply@anthropic.com>` |
| Local base | `7085721afe338a03cb114321a3576cdff420b732` (`origin/master`) |
| Local branch | `feat/pr16-spu-gaussian-interpolation-mpenkava` |

## Included scope

- The 512-entry PS1 SPU Gaussian coefficient table introduced by the source commit.
- Four-tap hardware Gaussian interpolation of decoded ADPCM voice samples.
- Preservation of the preceding ADPCM block's final three samples so interpolation remains continuous at block boundaries.
- A standalone regression test covering the boundary-history tap order and the equivalent in-block window.

## Deliberate exclusions

- The reverb engine and reverb register handling from the same source commit are isolated on a different branch.
- The source commit's high-quality shadow/reverb gating is not needed for the dry Gaussian path and is excluded.
- No game-specific addresses, generated code, configuration, or scripts are included.

## Provenance note

The source header identifies the coefficient values as the documented PS1 hardware table from No$PSX documentation and DuckStation's `core/spu.cpp`. The values are hardware data; this branch preserves Martin Penkava's implementation provenance and exact authorship trailers. No DuckStation implementation code was imported here.
104 changes: 104 additions & 0 deletions runtime/include/spu_gauss.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/* spu_gauss.h - PS1 SPU hardware Gaussian interpolation table (512 entries).
* Values from No$PSX docs / DuckStation core/spu.cpp (SPU::Voice::Interpolate).
* Usage, with i = (phase >> 4) & 0xFF and s[0]=current, s[-1..-3]=history:
* out = (g[0x0FF-i]*s[-3] + g[0x1FF-i]*s[-2] + g[0x100+i]*s[-1] + g[i]*s[0]) >> 15
*/
#ifndef PSX_SPU_GAUSS_H
#define PSX_SPU_GAUSS_H
#include <stdint.h>
static const int16_t spu_gauss_table[512] = {
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
0, 0, 0, 0, 0, 0, 0, 1,
1, 1, 1, 2, 2, 2, 3, 3,
3, 4, 4, 5, 5, 6, 7, 7,
8, 9, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 21, 22, 24,
25, 27, 28, 30, 32, 33, 35, 37,
39, 41, 44, 46, 48, 51, 53, 56,
58, 61, 64, 67, 70, 73, 77, 80,
84, 87, 91, 95, 99, 103, 107, 111,
116, 120, 125, 130, 135, 140, 145, 150,
156, 161, 167, 173, 179, 186, 192, 199,
205, 212, 219, 227, 234, 242, 250, 257,
266, 274, 283, 291, 300, 309, 319, 328,
338, 348, 358, 369, 379, 390, 401, 412,
424, 436, 448, 460, 473, 485, 498, 512,
525, 539, 553, 567, 582, 597, 612, 627,
643, 659, 675, 692, 708, 726, 743, 761,
779, 797, 816, 835, 854, 874, 894, 914,
935, 956, 977, 999, 1020, 1043, 1066, 1089,
1112, 1136, 1160, 1184, 1209, 1234, 1260, 1286,
1312, 1339, 1366, 1394, 1422, 1450, 1479, 1508,
1537, 1567, 1598, 1628, 1660, 1691, 1723, 1756,
1789, 1822, 1856, 1890, 1924, 1959, 1995, 2031,
2067, 2104, 2141, 2179, 2217, 2256, 2295, 2334,
2374, 2415, 2456, 2497, 2539, 2582, 2624, 2668,
2712, 2756, 2801, 2846, 2892, 2938, 2985, 3032,
3079, 3128, 3176, 3225, 3275, 3325, 3376, 3427,
3479, 3531, 3584, 3637, 3691, 3745, 3799, 3855,
3910, 3967, 4023, 4081, 4138, 4197, 4255, 4315,
4374, 4435, 4495, 4557, 4619, 4681, 4744, 4807,
4871, 4935, 5000, 5065, 5131, 5197, 5264, 5332,
5399, 5468, 5536, 5606, 5676, 5746, 5817, 5888,
5959, 6032, 6104, 6177, 6251, 6325, 6400, 6475,
6550, 6626, 6702, 6779, 6856, 6934, 7012, 7091,
7170, 7249, 7329, 7409, 7490, 7571, 7653, 7735,
7817, 7900, 7983, 8066, 8150, 8234, 8319, 8404,
8489, 8575, 8661, 8748, 8834, 8922, 9009, 9097,
9185, 9273, 9362, 9451, 9541, 9630, 9720, 9811,
9901, 9992, 10083, 10174, 10266, 10358, 10450, 10542,
10635, 10727, 10820, 10913, 11007, 11100, 11194, 11288,
11382, 11476, 11571, 11665, 11760, 11855, 11950, 12045,
12140, 12236, 12331, 12427, 12522, 12618, 12714, 12809,
12905, 13001, 13097, 13193, 13289, 13385, 13481, 13577,
13673, 13769, 13865, 13961, 14056, 14152, 14248, 14343,
14439, 14534, 14630, 14725, 14820, 14915, 15010, 15104,
15199, 15293, 15387, 15481, 15575, 15669, 15762, 15855,
15948, 16041, 16133, 16226, 16317, 16409, 16500, 16592,
16682, 16773, 16863, 16953, 17042, 17131, 17220, 17308,
17396, 17484, 17571, 17658, 17744, 17830, 17916, 18001,
18086, 18170, 18254, 18337, 18420, 18502, 18584, 18665,
18746, 18826, 18905, 18985, 19063, 19141, 19219, 19295,
19372, 19447, 19522, 19597, 19671, 19744, 19816, 19888,
19959, 20030, 20100, 20169, 20238, 20306, 20373, 20439,
20505, 20570, 20634, 20698, 20760, 20822, 20884, 20944,
21004, 21063, 21121, 21178, 21235, 21290, 21345, 21399,
21452, 21505, 21556, 21607, 21657, 21706, 21754, 21801,
21848, 21893, 21938, 21982, 22025, 22066, 22107, 22148,
22187, 22225, 22262, 22299, 22334, 22369, 22402, 22435,
22467, 22498, 22527, 22556, 22584, 22611, 22637, 22662,
22686, 22709, 22731, 22752, 22772, 22791, 22809, 22826,
22842, 22857, 22872, 22885, 22897, 22908, 22918, 22927,
22935, 22942, 22948, 22953, 22957, 22960, 22962, 22963,
};

/* Interpolate the current decoded sample using the SPU's four-tap Gaussian
* filter. previous[] is the final three samples from the preceding ADPCM
* block, oldest first; sample_index must address samples[]. */
static inline int16_t spu_gaussian_interpolate(const int16_t previous[3],
const int16_t samples[28],
int sample_index,
uint32_t phase)
{
const int gaussian_index = (int)((phase >> 4) & 0xFFu);
static const int tap_offset[4] = { 3, 2, 1, 0 };
int32_t accumulator = 0;

for (int tap = 0; tap < 4; ++tap) {
const int offset = sample_index - tap_offset[tap];
const int16_t sample = offset >= 0 ? samples[offset]
: previous[3 + offset];
int coefficient_index;
switch (tap) {
case 0: coefficient_index = 0x0FF - gaussian_index; break;
case 1: coefficient_index = 0x1FF - gaussian_index; break;
case 2: coefficient_index = 0x100 + gaussian_index; break;
default: coefficient_index = gaussian_index; break;
}
accumulator += (int32_t)spu_gauss_table[coefficient_index] * sample;
}

return (int16_t)(accumulator >> 15);
}
#endif /* PSX_SPU_GAUSS_H */
20 changes: 16 additions & 4 deletions runtime/src/spu.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

#include "spu.h"
#include "spu_gauss.h"
#include "spu_shadow.h"
#include "audio_trace.h"
#include "psx_cycles.h"
Expand Down Expand Up @@ -72,6 +73,7 @@ typedef struct {
uint32_t cur_addr;
uint32_t repeat_addr;
int16_t samples[SPU_BLOCK_SAMPLES];
int16_t previous_samples[3];
int sample_idx;
uint32_t phase;
int16_t hist1;
Expand Down Expand Up @@ -319,6 +321,13 @@ static void decode_block(SpuVoice *v) {
if (filter > 4) filter = 0;
if (shift > 12) shift = 12;

/* Gaussian interpolation reaches three samples into the preceding ADPCM
* block. KEYON clears samples[], so the first block naturally sees
* silence for its unavailable history. */
v->previous_samples[0] = v->samples[SPU_BLOCK_SAMPLES - 3];
v->previous_samples[1] = v->samples[SPU_BLOCK_SAMPLES - 2];
v->previous_samples[2] = v->samples[SPU_BLOCK_SAMPLES - 1];

int out = 0;
for (int b = 0; b < 14; b++) {
uint8_t packed = spu_ram[addr + 2u + (uint32_t)b];
Expand Down Expand Up @@ -445,17 +454,20 @@ static int16_t voice_next_sample(int idx) {
decode_block(v);
}

int16_t raw_s = v->samples[v->sample_idx];
int16_t raw_s = spu_gaussian_interpolate(v->previous_samples,
v->samples,
v->sample_idx,
v->phase);
/* Apply envelope (0..0x7FFF as a 15-bit gain). */
int32_t shaped = ((int32_t)raw_s * (int32_t)v->env_level) >> 15;
if (shaped > 32767) shaped = 32767;
if (shaped < -32768) shaped = -32768;

/* Shadow tap: record the four decoded samples bracketing the current
* sample position + the fractional phase + envelope, BEFORE the phase
* advances. The shadow re-interpolates these in float. Cross-block
* neighbours clamp to the block edge (the canon never interpolates across
* blocks either; documented in docs/SHADOW_ENHANCEMENTS.md). */
* 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) {
SpuShadowVoiceTap *t =
&s_shadow_tap[s_shadow_tap_frame].voice[idx];
Expand Down
32 changes: 32 additions & 0 deletions runtime/tests/test_spu_gaussian.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#include "spu_gauss.h"

#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
int16_t previous[3] = { 1000, 2000, 3000 };
int16_t samples[28];
memset(samples, 0, sizeof(samples));
samples[0] = 4000;

/* At the start of a block all three historical taps must come from the
* previous block. These coefficients are table entries 255, 511, 256, 0. */
assert(spu_gaussian_interpolate(previous, samples, 0, 0) == 1994);

/* The same four values inside a block must produce the same result. */
samples[0] = 1000;
samples[1] = 2000;
samples[2] = 3000;
samples[3] = 4000;
assert(spu_gaussian_interpolate(previous, samples, 3, 0) == 1994);

memset(previous, 0, sizeof(previous));
memset(samples, 0, sizeof(samples));
assert(spu_gaussian_interpolate(previous, samples, 0, 0x800u) == 0);

puts("SPU Gaussian interpolation tests passed");
return 0;
}