The all-pass filter in juce::Reverb is a comb filter!

Hello everybody!

I was doing some research work about reverberation algorithms lately, and I looked into the venerable good ol’ juce::Reverb algorithm to see how it was designed. I remember the LinearSmoothedValue class was originally there as a private class, before being publicly available in the modules, so I thought I could still learn one thing or two by looking at it again in its very last version.

It appears it is inspired from the Freeverb “iteration 1” algorithm that is open source since forever, and that Jules added it in JUCE around 2014 with more or less the same implementation and “tuning” values for the delay line sizes than in the original. It is a simple “Schroeder’s” structure reverb with parallel recursive comb filters and serial delay based all-pass filters, without anything fancy, which explains why the algorithm sounds a bit metallic and raw. So, the class uses internally those basic blocks thanks to internal private classes, called CombFilter and AllPassFilter, doing supposedly what their names suggest. Have a look at the CombFilter process function:

float process (const float input, const float damp, const float feedbackLevel) noexcept
{
    const float output = buffer[bufferIndex];
    last = (output * (1.0f - damp)) + (last * damp);
    JUCE_UNDENORMALISE (last);

    float temp = input + (last * feedbackLevel);
    JUCE_UNDENORMALISE (temp);
    buffer[bufferIndex] = temp;
    bufferIndex = (bufferIndex + 1) % bufferSize;
    
    return output;
}

As you can see, it is not the vanilla comb filter, since it has also an additional parameter called “damping” which can be used to low-pass a bit the output using a simple additional state variable, which is an idea introduced by Moorer if I’m not wrong. But then, here is the process function for the AllPassFilter class:

float process (const float input) noexcept
{
    const float bufferedValue = buffer [bufferIndex];
    float temp = input + (bufferedValue * 0.5f);
    JUCE_UNDENORMALISE (temp);
    
    buffer [bufferIndex] = temp;
    bufferIndex = (bufferIndex + 1) % bufferSize;

    return bufferedValue - input;
}

This is not an all-pass filter at all!! In fact, it’s a comb filter again with additional gain!

When I discover this, I was wondering why, and then I found out that the original Freeverb1 algorithm was doing this too, and that this issue was already quite well documented:

https://www.dsprelated.com/freebooks/pasp/Artificial_Reverberation.html

Funny isn’t it? So I was wondering if it could be possible to documentate that a bit more in the juce::Reverb class too, since it can be misleading for JUCE SDK users to come upon a class called “All-Pass Filter” which is actually not an all-pass filter at all :slight_smile:

@reuk @t0m @attila ?

And if you are wondering what happens when you replace the “Freeverb” APF comb filter with a true APF, here is what you get:

Original

True APF

Interesting right?

9 Likes

Interesting indeed! However i fail to see any difference in the two plots.

1 Like

Well, it’s not obvious, have a look at the beginning of the spectrogram in the high frequencies. But the thing is the code in the APF section is “almost” an all-pass filter, the coefficient is 0.5 and should be 0.618 instead to be a “true” all-pass. However, if you change this coefficient in the code and use a lowest value or something like 0.9, it makes a bigger difference in the spectrogram, and it adds some extra gain.

2 Likes