sceSas: Filter the reverb send before halving its rate

The ME decimates the send by keeping one frame of each pair, so
everything above 11khz aliases into the reverb (pspautotests'
audio/reverb/upsample: a 22.05khz tone reaches it as DC). Use the
output side's half-band filter at half gain instead. Splitting the send
into its even and odd frames first makes each tap one multiply-add for
two outputs and both channels.

This deliberately departs from the hardware, like the output side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-11-09 17:29:40 +01:00
1 parent cf94ea8886
commit 36fbf29364
3 files changed
+65 -8

No files matched your search

+2 -8
View File
@@ -792,14 +792,8 @@ void SasInstance::ApplyWaveformEffect() {
return;
}
// First, downsample the send buffer to 22khz. The ME keeps one frame of each pair, with no
// filter (tests/audio/reverb/upsample: a 22.05khz tone reaches the reverb as DC). It keeps the
// second frame; we keep the first.
for (int i = 0; i < grainSize / 2; i++) {
sendBufferDownsampled[i * 2] = clamp_s16(sendBuffer[i * 4]);
sendBufferDownsampled[i * 2 + 1] = clamp_s16(sendBuffer[i * 4 + 1]);
}
// First, downsample the send buffer to 22khz, with a filter the hardware doesn't have.
reverb_.DownsampleSend(sendBufferDownsampled, sendBuffer, grainSize / 2);
reverb_.ProcessReverb(sendBufferProcessed, sendBufferDownsampled, grainSize / 2, volLeft, volRight);
}
+55
View File
@@ -176,12 +176,16 @@ SasReverb::SasReverb() : preset_(-1), pos_(0) {
workspace_ = new int16_t[BUFSIZE];
data_ = new SasReverbData{};
wet_ = new float[(UPSAMPLE_HISTORY + MAX_INPUT_FRAMES) * 2]();
sendEven_ = new float[(UPSAMPLE_HISTORY + MAX_INPUT_FRAMES) * 2]();
sendOdd_ = new float[(UPSAMPLE_HISTORY + MAX_INPUT_FRAMES) * 2]();
}
SasReverb::~SasReverb() {
delete[] workspace_;
delete data_;
delete[] wet_;
delete[] sendEven_;
delete[] sendOdd_;
}
const char *SasReverb::GetPresetName(int preset) {
@@ -204,6 +208,8 @@ void SasReverb::SetPreset(int preset) {
pos_ = BUFSIZE - presets[preset_].size;
memset(workspace_, 0, sizeof(int16_t) * BUFSIZE);
memset(wet_, 0, sizeof(float) * UPSAMPLE_HISTORY * 2);
memset(sendEven_, 0, sizeof(float) * UPSAMPLE_HISTORY * 2);
memset(sendOdd_, 0, sizeof(float) * UPSAMPLE_HISTORY * 2);
} else {
pos_ = 0;
}
@@ -330,6 +336,55 @@ void SasReverb::UpsampleWet(int16_t *out, int count) {
memmove(wet_, wet_ + count * 2, sizeof(float) * UPSAMPLE_HISTORY * 2);
}
// The hardware decimates the send by keeping one frame of each pair, with no filter
// (tests/audio/reverb/upsample: a 22.05khz tone reaches the reverb as DC), so everything above
// 11khz aliases into the reverb. We filter it instead, with the same half-band filter as the
// output, at half gain: the center frame times one half, plus the odd frames on each side.
//
// With the send split into its even frames E and odd frames O, output n is
// E[n] / 2 + sum(c[j] / 2 * (O[n - 1 - j] + O[n + j])), so one load of O gives that tap for two
// consecutive outputs, both channels, with the same coefficient. Delayed by UPSAMPLE_HALF_TAPS
// frames at 22khz, like the output side.
void SasReverb::DownsampleSend(int16_t *output, const int32_t *input, int outputFrames) {
_dbg_assert_(outputFrames <= MAX_INPUT_FRAMES);
const int halfTaps = UPSAMPLE_HALF_TAPS;
float *even = sendEven_ + UPSAMPLE_HISTORY * 2;
float *odd = sendOdd_ + UPSAMPLE_HISTORY * 2;
for (int i = 0; i < outputFrames; i++) {
even[i * 2 + 0] = (float)input[i * 4 + 0];
even[i * 2 + 1] = (float)input[i * 4 + 1];
odd[i * 2 + 0] = (float)input[i * 4 + 2];
odd[i * 2 + 1] = (float)input[i * 4 + 3];
}
float coefs[UPSAMPLE_HALF_TAPS];
for (int j = 0; j < halfTaps; j++) {
coefs[j] = (float)upsampleCoefs[j] * (0.5f / 32768.0f);
}
// Output n centers on history frame n + halfTaps, so its newest tap is frame n - 1 of this call.
for (int n = 0; n < outputFrames; n += 2) {
Vec4F32 acc = Vec4F32::Load(sendEven_ + (n + halfTaps) * 2) * 0.5f;
for (int j = 0; j < halfTaps; j++) {
const Vec4F32 pair = Vec4F32::Load(sendOdd_ + (n + halfTaps - 1 - j) * 2) + Vec4F32::Load(sendOdd_ + (n + halfTaps + j) * 2);
acc = MulAdd(pair, Vec4F32::Splat(coefs[j]), acc);
}
if (n + 1 < outputFrames) {
Vec4S32FromF32Round(acc).StoreS16Sat(output + n * 2);
} else {
// An odd count: the second output belongs to the next call (its taps haven't all
// arrived yet), so only the first is stored.
int16_t last[4];
Vec4S32FromF32Round(acc).StoreS16Sat(last);
output[n * 2 + 0] = last[0];
output[n * 2 + 1] = last[1];
}
}
memmove(sendEven_, sendEven_ + outputFrames * 2, sizeof(float) * UPSAMPLE_HISTORY * 2);
memmove(sendOdd_, sendOdd_ + outputFrames * 2, sizeof(float) * UPSAMPLE_HISTORY * 2);
}
void SasReverb::ProcessReverb(int16_t *output, const int16_t *input, size_t inputSize, int volLeft, int volRight) {
// "Off" runs at the full rate and never gets here (see SasInstance::ApplyWaveformEffect).
if (preset_ == -1) {
+8
View File
@@ -32,6 +32,10 @@ public:
static const char *GetPresetName(int preset);
// Halves the rate of the 44khz send (interleaved stereo, outputFrames * 2 frames of it), into
// ProcessReverb's input.
void DownsampleSend(int16_t *output, const int32_t *input, int outputFrames);
// Input should be a mixdown of all the channels that have reverb enabled, at 22khz.
// Output is written back at 44khz.
void ProcessReverb(int16_t *output, const int16_t *input, size_t inputSize, int volLeft, int volRight);
@@ -64,4 +68,8 @@ private:
// previous call, then this call's. Not saved in savestates: the history is about a millisecond
// of audio, and the delay line itself isn't saved either.
float *wet_;
// The send at 44khz split into its even and its odd frames (interleaved stereo each), with
// UPSAMPLE_HISTORY frames of history, for DownsampleSend. Not saved either.
float *sendEven_;
float *sendOdd_;
};