sceSas: Add a hidden ReverbFilter setting

On by default. Off gives the hardware's unfiltered rate conversions
around the reverb: the send decimated by keeping the second frame of
each pair, and the output zero-stuffed at double level. With it off,
pspautotests' audio/reverb/upsample matches its .expected.

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 36fbf29364
commit e3b9e54c23
3 files changed
+36 -8

No files matched your search

+1
View File
@@ -1147,6 +1147,7 @@ static constexpr ConfigSetting soundSettings[] = {
.Label(I18NCat::AUDIO, "Reverb volume").Range(VOLUME_OFF, 2 * VOLUMEHI_FULL).Units("%").ZeroLabel("Disabled"),
ConfigSetting("AltSpeedRelativeVolume", SETTING(g_Config, iAltSpeedVolume), VOLUMEHI_FULL, CfgFlag::PER_GAME)
.Label(I18NCat::AUDIO, "Alternate speed volume").Range(VOLUME_OFF, VOLUMEHI_FULL).Units("%").ZeroLabel("Mute"),
ConfigSetting("ReverbFilter", SETTING(g_Config, bReverbFilter), true, CfgFlag::PER_GAME),
ConfigSetting("AchievementVolume", SETTING(g_Config, iAchievementVolume), &DefaultAchievementVolume, CfgFlag::PER_GAME)
.Label(I18NCat::AUDIO, "Achievement sound volume").Range(VOLUME_OFF, VOLUMEHI_FULL).Units("%").ZeroLabel("Mute"),
ConfigSetting("UIVolume", SETTING(g_Config, iUIVolume), 75, CfgFlag::DEFAULT)
+4
View File
@@ -451,6 +451,10 @@ public:
int iAchievementVolume;
int iAltSpeedVolume;
// Filter the SAS reverb's rate conversions to and from 22khz. Off gives the hardware's
// unfiltered decimation and zero-stuffing. No UI.
bool bReverbFilter;
bool bExtraAudioBuffering; // For bluetooth
// Android only.
int iAndroidAudioApi; // AndroidAudioApi
+31 -8
View File
@@ -282,7 +282,8 @@ private:
// (tests/audio/reverb/upsample: with dry off, every odd frame is exactly zero). That
// leaves a mirror image of the whole wet signal just below 22khz, at full amplitude: it is barely
// audible itself, but it eats the headroom of the final mix, so loud scenes clip much more (Outrun
// 2006 while racing). We deliberately replace that with a proper interpolation filter.
// 2006 while racing). We deliberately replace that with a proper interpolation filter, unless the
// hidden ReverbFilter setting is off.
//
// A half-band filter: even output frames are the input samples, odd frames are interpolated from
// UPSAMPLE_HALF_TAPS samples on each side. Kaiser-windowed sinc (beta 7.25), Q15, summing to
@@ -347,6 +348,15 @@ void SasReverb::UpsampleWet(int16_t *out, int count) {
// 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);
if (!g_Config.bReverbFilter) {
// What the hardware does: the second frame of each pair, unfiltered.
for (int i = 0; i < outputFrames; i++) {
output[i * 2 + 0] = clamp_s16(input[i * 4 + 2]);
output[i * 2 + 1] = clamp_s16(input[i * 4 + 3]);
}
return;
}
const int halfTaps = UPSAMPLE_HALF_TAPS;
float *even = sendEven_ + UPSAMPLE_HISTORY * 2;
float *odd = sendOdd_ + UPSAMPLE_HISTORY * 2;
@@ -404,11 +414,14 @@ void SasReverb::ProcessReverb(int16_t *output, const int16_t *input, size_t inpu
// >> 12, because it only puts the wet signal on every other 44khz frame and leaves the
// rest at zero. We interpolate instead (see below), which gives every frame the signal,
// so the same average level comes from the plain >> 12 that our caller's leftVol << 3
// pairs with the >> 15 below.
// pairs with the >> 15 below. Unfiltered, the output uses >> 14 to match the ME.
volLeft *= reverbVolumeMultiplier;
volRight *= reverbVolumeMultiplier;
}
// Read once per grain, so a change can't split one between the two output paths.
const bool filter = g_Config.bReverbFilter;
_dbg_assert_(inputSize <= MAX_INPUT_FRAMES);
const SasReverbData &d = *data_;
@@ -452,11 +465,19 @@ void SasReverb::ProcessReverb(int16_t *output, const int16_t *input, size_t inpu
b[d.mRAPF2] = clamp_s16(Rout - (d.vAPF2*b[(d.mRAPF2 - d.dAPF2)] >> 15));
Rout = b[(d.mRAPF2 - d.dAPF2)] + (b[d.mRAPF2] * d.vAPF2 >> 15);
// ___Output to Mixer(Output volume multiplied with input from APF2)___________
// Lout reaches about 17 bits and the volume 18, so this needs 64 bits. The result is kept
// unclamped until after interpolation.
float *wet = wet_ + (UPSAMPLE_HISTORY + i) * 2;
wet[0] = (float)(((int64_t)Lout * volLeft) >> 15);
wet[1] = (float)(((int64_t)Rout * volRight) >> 15);
// Lout reaches about 17 bits and the volume 18, so this needs 64 bits.
if (filter) {
// Kept unclamped until after interpolation.
float *wet = wet_ + (UPSAMPLE_HISTORY + i) * 2;
wet[0] = (float)(((int64_t)Lout * volLeft) >> 15);
wet[1] = (float)(((int64_t)Rout * volRight) >> 15);
} else {
// What the hardware does: every other frame at twice the level (its >> 11), the rest zero.
output[i * 4 + 0] = (int16_t)std::clamp(((int64_t)Lout * volLeft) >> 14, (int64_t)-32768, (int64_t)32767);
output[i * 4 + 1] = (int16_t)std::clamp(((int64_t)Rout * volRight) >> 14, (int64_t)-32768, (int64_t)32767);
output[i * 4 + 2] = 0;
output[i * 4 + 3] = 0;
}
b.Next();
}
@@ -464,6 +485,8 @@ void SasReverb::ProcessReverb(int16_t *output, const int16_t *input, size_t inpu
// Save the state in the object.
pos_ = b.GetPosition();
UpsampleWet(output, (int)inputSize);
if (filter) {
UpsampleWet(output, (int)inputSize);
}
}