How do I make filters with sharp cutoff corners and steep slopes?

Hi,

I’m trying to make a filter in JUCE with sharp cutoff corners and steep slopes and cannot figure out how.

I want the same shape as the screenshot fabfilter.png, a setting in my FabFilter Pro-Q2.

The Pro-Q2 setting is a combined high pass filter (250Hz corner frequency, 36dB/octave, Q=1) and low pass filter (2500Hz corner frequency, 36dB/octave, Q=1)

As you can see, the Pro-Q2 is flat on top and steep on the sides.

The second screenshot plugindoctor,myfilter.png is what my JUCE filter is making. I’m using the default makeLowPass and makeHighPass filter objects. As you can see, the slopes are not sharp at all. The top is not flat at all.

The final screenshot is plugindoctor,fabfilter_and_myfilter.png showing both.

I know enough to realize I need to make custom filters, but how? Where do I start?

Here is my filter code; I put three filters in series to create 36dB/octave slopes:

from the constructor:

//hpf
double hpfCutoffFrequency;
dsp::IIR::Filter<float> hpfSignalLeft1;
dsp::IIR::Filter<float> hpfSignalRight1;
dsp::IIR::Filter<float> hpfSignalLeft2;
dsp::IIR::Filter<float> hpfSignalRight2;
dsp::IIR::Filter<float> hpfSignalLeft3;
dsp::IIR::Filter<float> hpfSignalRight3;

//lpf
double lpfCutoffFrequency;
dsp::IIR::Filter<float> lpfSignalLeft1;
dsp::IIR::Filter<float> lpfSignalRight1;
dsp::IIR::Filter<float> lpfSignalLeft2;
dsp::IIR::Filter<float> lpfSignalRight2;
dsp::IIR::Filter<float> lpfSignalLeft3;
dsp::IIR::Filter<float> lpfSignalRight3;

hpfCutoffFrequency = 250.0;
lpfCutoffFrequency = 2500.0;

from prepareToPlay:

hpfSignalLeft1.reset();
hpfSignalRight1.reset();
hpfSignalLeft2.reset();
hpfSignalRight2.reset();
hpfSignalLeft3.reset();
hpfSignalRight3.reset();

//each one is 12dB/oct
hpfSignalLeft1.coefficients = dsp::IIR::Coefficients<float>::makeHighPass (sampleRate, hpfCutoffFrequency);
hpfSignalRight1.coefficients = dsp::IIR::Coefficients<float>::makeHighPass (sampleRate, hpfCutoffFrequency);
hpfSignalLeft2.coefficients = dsp::IIR::Coefficients<float>::makeHighPass (sampleRate, hpfCutoffFrequency);
hpfSignalRight2.coefficients = dsp::IIR::Coefficients<float>::makeHighPass (sampleRate, hpfCutoffFrequency);
hpfSignalLeft3.coefficients = dsp::IIR::Coefficients<float>::makeHighPass (sampleRate, hpfCutoffFrequency);
hpfSignalRight3.coefficients = dsp::IIR::Coefficients<float>::makeHighPass (sampleRate, hpfCutoffFrequency);

lpfSignalLeft1.reset();
lpfSignalRight1.reset();
lpfSignalLeft2.reset();
lpfSignalRight2.reset();
lpfSignalLeft3.reset();
lpfSignalRight3.reset();

//each one is 12dB/oct
lpfSignalLeft1.coefficients = dsp::IIR::Coefficients<float>::makeLowPass (sampleRate, lpfCutoffFrequency);
lpfSignalRight1.coefficients = dsp::IIR::Coefficients<float>::makeLowPass (sampleRate, lpfCutoffFrequency);
lpfSignalLeft2.coefficients = dsp::IIR::Coefficients<float>::makeLowPass (sampleRate, lpfCutoffFrequency);
lpfSignalRight2.coefficients = dsp::IIR::Coefficients<float>::makeLowPass (sampleRate, lpfCutoffFrequency);
lpfSignalLeft3.coefficients = dsp::IIR::Coefficients<float>::makeLowPass (sampleRate, lpfCutoffFrequency);
lpfSignalRight3.coefficients = dsp::IIR::Coefficients<float>::makeLowPass (sampleRate, lpfCutoffFrequency);

from processBlock:

//do the highpass filtering in series to get 36dB/octave slope
//do the lowpass filtering in series to get 36dB/octave slope
if (channel == 0) {
	channelData[sample] = hpfSignalLeft1.processSample(channelData[sample]);
	hpfSignalLeft1.snapToZero();
	channelData[sample] = hpfSignalLeft2.processSample(channelData[sample]);
	hpfSignalLeft2.snapToZero();
	channelData[sample] = hpfSignalLeft3.processSample(channelData[sample]);
	hpfSignalLeft3.snapToZero();

	channelData[sample] = lpfSignalLeft1.processSample(channelData[sample]);
	lpfSignalLeft1.snapToZero();
	channelData[sample] = lpfSignalLeft2.processSample(channelData[sample]);
	lpfSignalLeft2.snapToZero();
	channelData[sample] = lpfSignalLeft3.processSample(channelData[sample]);
	lpfSignalLeft3.snapToZero();
}

if (channel == 1) {
	channelData[sample] = hpfSignalRight1.processSample(channelData[sample]);
	hpfSignalRight1.snapToZero();
	channelData[sample] = hpfSignalRight2.processSample(channelData[sample]);
	hpfSignalRight2.snapToZero();
	channelData[sample] = hpfSignalRight3.processSample(channelData[sample]);
	hpfSignalRight3.snapToZero();

	channelData[sample] = lpfSignalRight1.processSample(channelData[sample]);
	lpfSignalRight1.snapToZero();
	channelData[sample] = lpfSignalRight2.processSample(channelData[sample]);
	lpfSignalRight2.snapToZero();
	channelData[sample] = lpfSignalRight3.processSample(channelData[sample]);
	lpfSignalRight3.snapToZero();
}

Thanks,

Fred

There might be people on this forum with some more experience when it comes to filters, but isn’t the cutoff frequency the point where the response crosses -3db? So if you are chaining three such filters, the drop at that frequency should already be -9db where you would expect it to be -3db? Does this match your measurements?

1 Like

As PluginPenguin said, if you chain three Butterworths with the same cutoff you’ll get -9 db at the cutoff. You need a 6th order Butterworth -three biquads / SVFs with 1/Q of 0.5176, 1.4142 and 1.9319. See Butterworth polynomials.

If you need something even sharper, a sinc filter is an ideal (brick-wall) low-pass filter. However it’s an infinite sum and is non-causal (i.e. the sum includes future samples), so realtime implementations must truncate the sum to a finite number of past samples, and would likely apply a window function as well to suppress the sideband.

Thanks everyone for the Master’s level explanations. But I’m just trying to write some code.

I have dug and dug, and eventually found some obscure mention of designIIRLowpassHighOrderButterworthMethod. Apparently this returns an array.

I tried this:

private:

ReferenceCountedArray<IIRCoefficients> lpfSignalLeftCoefficients;
ReferenceCountedArray<IIRCoefficients> lpfSignalRightCoefficients;

prepareToPlay:

lpfSignalLeftCoefficients = dsp::FilterDesign<float>::designIIRLowpassHighOrderButterworthMethod (lpfCutoffFrequency, sampleRate, 6);
lpfSignalRightCoefficients = dsp::FilterDesign<float>::designIIRLowpassHighOrderButterworthMethod (lpfCutoffFrequency, sampleRate, 6);

But it is wrong, and I know it is wrong, and I don’t know how to make it right.

I have looked everywhere, and there is absolutely no sample code anywhere to be found for how to set up designIIRLowpassHighOrderButterworthMethod.

Can you give me an example of how to set up a filter using designIIRLowpassHighOrderButterworthMethod, if I am in fact on the right path?

Thanks,
Fred

maybe fabfilter just decided that for them the freq where it’s -3db is not exactly the cutoff frequency but with some offset to improve the intuition. i’ve read that some people even once experimented with not giving all the filters that are in series the exact same cutoff frequency but i haven’t tried that yet so can’t confirm anything. just ideas

It’s not a master’s level explanation, it’s just how you make a 36 db/oct Butterworth. I don’t use the dsp classes, that’s why I didn’t point you to the exact method of the exact class. You just need three 2nd order low / highpasses in chain with Q of 1/0.5176, 1/1.4142 and 1/1.9319. I guess this will make the coefficients for each one of them.

1 Like

I know little to nothing about what I am doing, but I seem to be onto something, no thanks to any examples on the internet… I hope someone scouring Google or the forums for examples of how to use designIIRHighpassHighOrderButterworthMethod will find this useful.

While this seems to work so far, I have no idea if it correct or efficient, or if it will explode with memory leaks. For that reason maybe it is not a great example to follow.

Attached is a new screenshot comparing to the Pro-Q2; now they match. In the second screenshot, I moved the Pro-Q2 to the left a bit to show them separately.

Phase performance is another conversation; my filter suffers greatly in comparison.

I would greatly appreciate a critique of my code.

private:

//hpf
//each L/R pair is 12dB/oct
double hpfCutoffFrequency;
dsp::IIR::Filter<float> hpfSignalLeft1;
dsp::IIR::Filter<float> hpfSignalLeft2;
dsp::IIR::Filter<float> hpfSignalLeft3;
dsp::IIR::Filter<float> hpfSignalRight1;
dsp::IIR::Filter<float> hpfSignalRight2;
dsp::IIR::Filter<float> hpfSignalRight3;

//lpf
//each L/R pair is 12dB/oct
double lpfCutoffFrequency;
dsp::IIR::Filter<float> lpfSignalLeft1;
dsp::IIR::Filter<float> lpfSignalLeft2;
dsp::IIR::Filter<float> lpfSignalLeft3;
dsp::IIR::Filter<float> lpfSignalRight1;
dsp::IIR::Filter<float> lpfSignalRight2;
dsp::IIR::Filter<float> lpfSignalRight3;

ReferenceCountedArray<dsp::IIR::Coefficients<float>> hpfSignalLeftCoefficientsArray;
ReferenceCountedArray<dsp::IIR::Coefficients<float>> hpfSignalRightCoefficientsArray;
ReferenceCountedArray<dsp::IIR::Coefficients<float>> lpfSignalLeftCoefficientsArray;
ReferenceCountedArray<dsp::IIR::Coefficients<float>> lpfSignalRightCoefficientsArray;

in the constructor:

hpfCutoffFrequency = 250.0; //Hz
lpfCutoffFrequency = 2500.0; //Hz

prepareToPlay:

//each L/R pair is 12dB/oct
hpfSignalLeft1.reset();
hpfSignalLeft2.reset();
hpfSignalLeft3.reset();
hpfSignalRight1.reset();
hpfSignalRight2.reset();
hpfSignalRight3.reset();

hpfSignalLeftCoefficientsArray = dsp::FilterDesign<float>::designIIRHighpassHighOrderButterworthMethod (hpfCutoffFrequency, sampleRate, 6);
hpfSignalRightCoefficientsArray = dsp::FilterDesign<float>::designIIRHighpassHighOrderButterworthMethod (hpfCutoffFrequency, sampleRate, 6);

//each L/R pair is 12dB/oct
hpfSignalLeft1.coefficients = hpfSignalLeftCoefficientsArray.getObjectPointer(0);
hpfSignalLeft2.coefficients = hpfSignalLeftCoefficientsArray.getObjectPointer(1);
hpfSignalLeft3.coefficients = hpfSignalLeftCoefficientsArray.getObjectPointer(2);
hpfSignalRight1.coefficients = hpfSignalRightCoefficientsArray.getObjectPointer(0);
hpfSignalRight2.coefficients = hpfSignalRightCoefficientsArray.getObjectPointer(1);
hpfSignalRight3.coefficients = hpfSignalRightCoefficientsArray.getObjectPointer(2);




//each L/R pair is 12dB/oct
lpfSignalLeft1.reset();
lpfSignalLeft2.reset();
lpfSignalLeft3.reset();
lpfSignalRight1.reset();
lpfSignalRight2.reset();
lpfSignalRight3.reset();

lpfSignalLeftCoefficientsArray = dsp::FilterDesign<float>::designIIRLowpassHighOrderButterworthMethod (lpfCutoffFrequency, sampleRate, 6);
lpfSignalRightCoefficientsArray = dsp::FilterDesign<float>::designIIRLowpassHighOrderButterworthMethod (lpfCutoffFrequency, sampleRate, 6);

//each L/R pair is 12dB/oct
lpfSignalLeft1.coefficients = lpfSignalLeftCoefficientsArray.getObjectPointer(0);
lpfSignalLeft2.coefficients = lpfSignalLeftCoefficientsArray.getObjectPointer(1);
lpfSignalLeft3.coefficients = lpfSignalLeftCoefficientsArray.getObjectPointer(2);
lpfSignalRight1.coefficients = lpfSignalRightCoefficientsArray.getObjectPointer(0);
lpfSignalRight2.coefficients = lpfSignalRightCoefficientsArray.getObjectPointer(1);
lpfSignalRight3.coefficients = lpfSignalRightCoefficientsArray.getObjectPointer(2);

processBlock:

//do the highpass filtering in series to get 36dB/octave slope
//do the lowpass filtering in series to get 36dB/octave slope
if (channel == 0) {
	channelData[sample] = hpfSignalLeft1.processSample(channelData[sample]);
	hpfSignalLeft1.snapToZero();
	channelData[sample] = hpfSignalLeft2.processSample(channelData[sample]);
	hpfSignalLeft2.snapToZero();
	channelData[sample] = hpfSignalLeft3.processSample(channelData[sample]);
	hpfSignalLeft3.snapToZero();

	channelData[sample] = lpfSignalLeft1.processSample(channelData[sample]);
	lpfSignalLeft1.snapToZero();
	channelData[sample] = lpfSignalLeft2.processSample(channelData[sample]);
	lpfSignalLeft2.snapToZero();
	channelData[sample] = lpfSignalLeft3.processSample(channelData[sample]);
	lpfSignalLeft3.snapToZero();
}

if (channel == 1) {
	channelData[sample] = hpfSignalRight1.processSample(channelData[sample]);
	hpfSignalRight1.snapToZero();
	channelData[sample] = hpfSignalRight2.processSample(channelData[sample]);
	hpfSignalRight2.snapToZero();
	channelData[sample] = hpfSignalRight3.processSample(channelData[sample]);
	hpfSignalRight3.snapToZero();

	channelData[sample] = lpfSignalRight1.processSample(channelData[sample]);
	lpfSignalRight1.snapToZero();
	channelData[sample] = lpfSignalRight2.processSample(channelData[sample]);
	lpfSignalRight2.snapToZero();
	channelData[sample] = lpfSignalRight3.processSample(channelData[sample]);
	lpfSignalRight3.snapToZero();
}

Regards,

Fred

2 Likes

Hi, I’ve tested it. It works great for me!

Good news, thanks.