IIRFilter class filter structures should be TDF2


#1

Hello Jules !

I can see in the processSamples class of the juce::IIRFilter class that the filter structure used is the Direct Form I. I think it is a bad idea, and that this class should use at least the Transposed Direct Form II instead. The resulting structure would be canonic (using less delay lines and less state variables), with a better numerical robustness, and its output would be a little smoothed when one of the filter parameters changes (and we will have of course exactly the same transfer function). I propose to replace “float x1, x2, y1, y2;” in the header with “float v1, v2;”. And then, I propose the following new code :

[code]//==============================================================================
void IIRFilter::reset() noexcept
{
const ScopedLock sl (processLock);

v1 = 0;
v2 = 0;

}

float IIRFilter::processSingleSampleRaw (const float in) noexcept
{
float out = coefficients[0] * in + v1;

JUCE_SNAP_TO_ZERO (out);

v1 = coefficients[1] * in + v2 - coefficients[4] * out;
v2 = coefficients[2] * in - coefficients[5] * out;

return out;

}

void IIRFilter::processSamples (float* const samples,
const int numSamples) noexcept
{
const ScopedLock sl (processLock);

if (active)
{
    for (int i = 0; i < numSamples; ++i)
    {
        const float in = samples[i];

        float out = coefficients[0] * in + v1;

        JUCE_SNAP_TO_ZERO (out);

        v1 = coefficients[1] * in + v2 - coefficients[4] * out;
        v2 = coefficients[2] * in - coefficients[5] * out;

        samples[i] = out;
    }
}

}[/code]

Some people on KVRaudio are suggesting even better approaches (the topology-preserving structures for example), but this one is working a lot better than the DFI yet :wink: What do you think of this change ?


#2

We should definitely apply the change you suggested, but let’s not pretend that modulation of filter parameters in the general case will be robust even with Transposed Direct Form II.

Here’s a stable filter that tolerates parameter changes:


#3

Hello TheVinn !

Thanks for the link, I didn’t know that Andrew Simper has published papers on his website too. Anyway, I have just said that the modulation of parameters will be “a little” less abrupt with the TDF2, that’s why I suggested that topology preserving structures (like the one in the paper) are even better. The important thing here is that the change I suggest is relevant and easy to implement :wink:


#4

Ah, that’s a nice optimisation, thanks! I’ll take a look asap!


#5

Thanks a lot for the change !

I would like also to suggest the addition of four other functions in the JUCE_IIRFilter class :

[code]void IIRFilter::makeLowPass (const double sampleRate,
const double Q,
const double frequency) noexcept
{
jassert (sampleRate > 0);

const double n = 1.0 / tan (double_Pi * frequency / sampleRate);
const double nSquared = n * n;
const double Qinv = 1.0 / Q;
const double c1 = 1.0 / (1.0 + n * Qinv + nSquared);

setCoefficients (c1,
                 c1 * 2.0,
                 c1,
                 1.0,
                 c1 * 2.0 * (1.0 - nSquared),
                 c1 * (1.0 - n * Qinv + nSquared));

}

void IIRFilter::makeHighPass (const double sampleRate,
const double Q,
const double frequency) noexcept
{
const double n = tan (double_Pi * frequency / sampleRate);
const double nSquared = n * n;
const double Qinv = 1.0 / Q;
const double c1 = 1.0 / (1.0 + n * Qinv + nSquared);

setCoefficients (c1,
                 c1 * -2.0,
                 c1,
                 1.0,
                 c1 * 2.0 * (nSquared - 1.0),
                 c1 * (1.0 - n * Qinv + nSquared));

}

void IIRFilter::makeBandPass (const double sampleRate,
const double Q,
const double frequency) noexcept
{
const double n = tan (double_Pi * frequency / sampleRate);
const double nSquared = n * n;
const double Qinv = 1.0 / Q;
const double c1 = 1.0 / (1.0 + n * Qinv + nSquared);

setCoefficients (c1*n*Qinv,
                 0.0,
                 -c1*n*Qinv,
                 1.0,
                 c1 * 2.0 * (nSquared - 1.0),
                 c1 * (1.0 - n * Qinv + nSquared));

}

void IIRFilter::makeBandPass (const double sampleRate,
const double frequency) noexcept
{
const double n = tan (double_Pi * frequency / sampleRate);
const double nSquared = n * n;
const double c1 = 1.0 / (1.0 + std::sqrt (2.0) * n + nSquared);

setCoefficients (c1*n*std::sqrt (2.0),
                 0.0,
                 -c1*n * std::sqrt (2.0),
                 1.0,
                 c1 * 2.0 * (nSquared - 1.0),
                 c1 * (1.0 - std::sqrt (2.0) * n + nSquared));

}[/code]

This way, the IIRFilter class will cover all the classic SVF filters, the Lowpass/Highpass with the factor Q choice (and not only the “Butterworth” with Q = 1/sqrt(2)), and also the bandpass which wasn’t available before (your Bandpass class is in fact a parametric filter, also called “bell”, but this is not at all what is generally called a bandpass…)


#6

I second this. I’ve already done something similar with my BiquadFilter subclass (although I need to update it to use double-precision calculations now IIRFilter is double coefficients).

I’ve called what the existing band-pass form peak-notch, and implemented actual band-pass/band-stop forms along with an all-pass using the equations form the Audio EQ Cookbook.


#7

[quote=“TheVinn”]We should definitely apply the change you suggested, but let’s not pretend that modulation of filter parameters in the general case will be robust even with Transposed Direct Form II.

Here’s a stable filter that tolerates parameter changes:

http://www.cytomic.com/files/dsp/SvfLinearTrapOptimised.pdf[/quote]That sound interesting. Do you think you can convert any IIR to this? Or if starting from poles & zeroes?


#8

One way to get SVFs with a topology preserving structure is suggested here :

http://www.kvraudio.com/forum/viewtopic.php?t=350246
http://www.discodsp.net/VAFilterDesign.pdf

For any IIR filter, I’m experimenting some stuff about that right now, this should always be possible.