[Solved] What causes the IIRFilter class to pop when filter frequency is changed quickly, and how can i fix it?


#1

I’ve been using the basic IIRFilter class in my project and it mostly works pretty well. But when I rapidly change the filter frequency. I get pops in the audio. I’ve tried smoothing out sudden changes in frequency by applying a ramp to the filter frequency and changing coefficients between individual samples, but unless I set the ramp time to the point where it’s actually audible, it doesn’t do much. I’ve also tried resetting the filter when I lower the filter frequency by some ratio of the original value, but while this removes the pops, the filter starts to crackle instead when I change the filter frequency slowly.

I’m guessing that the problem has something to do with the fact that IIRFilters rely on previous outputs to calculate future outputs. As such, after looking at some sort-of similar threads, I’m currently considering running multiple filters on the audio buffer in parallel, each handling filter frequencies between a certain range (so, for example, one filter would have its filter frequency between 20Hz and 2,000Hz, while the other would cover 2,000Hz to 20,000Hz) so if I suddenly changed from 20k Hz to 20Hz I switch from getting output from the high register filter to the low register filter, so then the difference in filter frequencies between samples is only 1,980Hz instead of 19,980Hz. That being said, I don’t know if this method would work, and even if it does, I can see it being pretty computationally expensive.

tl;dr: how do I make the IIRFilter not cause pops/clicks?

For reference, this is the relevant code.

Processing the AudioBuffer

void MainSection::processFilter(AudioBuffer< float >& outputAudio, int startSample, int numSamples) {
	setFilterCoefficients();

	if (MSD.isFilterOn) {
		if (outputAudio.getNumChannels() > 0) {
			filtL->processSamples(outputAudio.getWritePointer(0) + startSample, numSamples);

			if (outputAudio.getNumChannels() > 1) {
				filtR->processSamples(outputAudio.getWritePointer(1) + startSample, numSamples);
			}
		}
	}
}

Setting the filter coefficients

void MainSection::setFilterCoefficients() {
	if (MSD.filterChanged) {
		//Up here I set filtFreq and filtRes using the member variables and the mod wheel
		//...
		//makeLowPass is just here as an example: I actually use a switch to make different kinds of filters as selected
		MSD.coef = IIRCoefficients::makeLowPass(MSD.sampleRate, filtFreq, utilities::resonanceConversionLowpassHighpass(filtRes));

		if (MSD.isFilterOn) {
			filtL->setCoefficients(MSD.coef);
			filtR->setCoefficients(MSD.coef);

			if (MSD.filterTypeChanged /*|| MSD.filterFrequencyLowered*/) {
				filtL->reset();
				filtR->reset();
				MSD.filterTypeChanged = MSD.filterFrequencyLowered = false;
			}

			MSD.filterChanged = false;
		}
		else {
			filtL->makeInactive();
			filtR->makeInactive();
		}
	}
}

Setting the filter frequency

void MainSection::setFilterFrequency(float frequency) {
	if (frequency < MSD.filterFrequency) {
		MSD.filterFrequencyLowered = true;
	}
	MSD.filterFrequency = frequency;
	MSD.filterChanged = MSD.filterFrequencyChanged = true;
}

#2

The pop is probably from the reset()

Rail


#3

First, avoid resetting the filters during the audio stream, this will black internal state and create a 0.0 at the output, and probably a click.

The coefficient calculation is based on Bi-linear transforms, which relies on “Linear Time-Invariant” systems theory. Based on that, you can figure out that stability isn’t guaranteed if you’re making it work as a “Time-Variant” system.
The pops you’ve been hearing very likely comes from the filter going unstable.
Luckily, Bilinear Transform based filters behave quite well under cutoff freq variations, but not if they’re very abrupt.

I would suggest smoothing the cutoff freq variations (note that this doesn’t mean updating the coeffs every sample).

You’re not the first one to tackle this issue, there’s plenty discussion around that on other forums.

Good luck!


#4

For the record, the current implementation only ever resets the filter when I change from one type of filter to another (ex. Lowpass to Highpass). Should I avoid doing this anyways?

How audible do you think I can get away with making the frequency smoothing? Testing SynthMaster in Ableton I notice that it takes ~8ms to get from its maximum frequency to its minimum frequency with parameter automation using its digital 24 Db lowpass, but TAL NoiseMaker takes ~12ms so I can’t tell whether these numbers where chosen out of a hat because of developer preference, or whether they were optimization decisions or what.

Thanks for the information, by the way!


#5

I noticed you only reset the IIR structures when changing filter type, that is indeed necessary. But anyway, I recommend avoiding that pop by fading in and out some buffers around the switch.
Check this class.

A classic approach to smoothing the cutoff frequency variations, is thinking as the cutoff frequency itself being a signal, with a sampling period N times bigger than the audio signal, N being number of samples between each filter coefficient update.
Then, you can apply a smoothing filter to the “cutoff frequency signal”. This smoothing filter could be something as simple as a first order low pass.
And don’t forget to be cautious about how your updating the values from IU knob, it has to be in sync with the filter coefficient updating.

The the smoothing technique used behind “performance expressive” parameters is key to making a cool sounding effect. This isn’t a problem with a well defined solution, developers will very likely try to tune the parameter’s smoothing profile to best fit their processing algorithm and the kind experience they envisioned.

As you can see this hole goes as deep as you dig. I suggest taking the approach described above, but keep in mind that if you change the cutoff frequency too fast, it will very likely make you filter unstable.


#6

The filter will only become unstable when the poles drift outside the unit circle. There’s some nuance that iirc comes from changes of filter coefficients that happen shorter than the period related to the bandwidth of the filter related to the Q factor as defined in the basic s domain biquads (denominator is s^2 + swc/Q + wc^2) that I remember from some old AES paper I read on time variant filters, but in practice I’ve found that’s not the biggest problem.

If you plot the pole-loci of common IIR filter coefficient changes in your standard direct form filter structures, such as a cutoff change, the pole locations are unlikely to drift outside the unit circle in the Z domain except in high Q filters where the poles are very close to the unit circle to begin with and you run into issues with numeric precision. That said, the loci get more precise as you get closer to the unit circle which usually mitigates the problem. The DAFX text by Zölzer goes into more depth about it.

A very quick test to see if the filter is going unstable is if the click causes a crash in the filter (as in a short parameter change causes a clip followed by silence). This happens when the filter output shoots up to a nan or inf val which is easy to check in a debug configuration. Otherwise the clicks are due to resetting the filter state variables to 0 or a rapid change in filter parameter.

The easiest method I’ve heard and seen used to mitigate the issues of time variant filters is to use an exponential average directly on the coefficents of a biquad, which can be seen as an interpolation scheme to smooth the coefficents as you change parameters. You can do the math to figure out the cutoff frequency of the averager, but usually a coefficient > 0.9 is sufficient. Depending on your sampling rate. It doesn’t need to be done on the parameters themselves, which is less efficient. So long as the two different parameters result in a stable filter, an interpolation between the coefficients will be stable.


#7

Since the filter type is not changed I don’t think the reset is executed here.

Here are some thoughts to give you an idea of what is happening:
Any recursive filter has a state. This state is represented by the values that are stored in its feedback path. This state is causing the filter to have a certain momentum - very much like a car that drives on a highway with a certain speed. Imagine the driver of that car nearly missed an exit and now desparately tries to take the route off by steering sharp towards the exit.
A lot of things can happen now. Stuff in the car might be flying through the drivers cabin. The wheels could even loose contact with the roadway ceiling causing a car crash.
A similar thing happens with your filter if you try to change its parameters very rapidly. (Except that the outcome is not nearly as dramatic - instead of a car crash you just get a PLOP.)

But there is more to consider - An anlog filter also has a state so why wouldn’t it pop when you crank the dial? The answer is in the name of the digital filter: It is recursive that means a part of its output gets fed back into the input. While we are in the digital domain a feedback can’t be done without introducing a minimal delay into the filter code. After all you do have to calculate the output of the filter before you can add it back to the (next) input value.

Now if a very experienced race car driver had to make the same maneuvre he would be able to control the car and nothing bad would happen.
Likewise there are some advanced filter designs that allow for more snappy and instantaneous parameter changes.
Those filters are commonly referred to as “zero feedback delay” filter. However keep in mind that no race car driver can magically make the momentum of the car and his own reaction time disappear. In the same way no filter design can cancle out the filter state. The art is to control the momentum and the unit delay to get the filter response to appear more continous.

If you want to know more then read on here:
http://www.earlevel.com/main/2016/02/21/filters-for-synths-starting-out/
http://www.earlevel.com/main/2016/02/22/filters-for-synths-the-4-pole/
(just for clarification: I did not write any of the linked stuff nor would I be able to)


#8

The new class dsp::StateVariableFilter is exactly what you need if you experience artefacts when modulating the cutoff frequency.


#9

When will we get a documentation for the dsp module? Or are the few doxygen comments as good as it gets? @IvanC @jules


#10

I have experienced this myself, and managed to get rid of audible pops with changing the resonant frequency of a filter.

I used the reset method in the LinearSmoothValue class in prepareToPlay().
e.g. cutoffFrequency.reset(sampleRate, 0.1);

The second value is not arbitrary, but 0.1 should work. I have not worked out the math myself, but I imagine that there is a maximum step that can be taken (band-limited step) that will prevent pops.


#11

Thanks for all the help guys! I’ve decided to apply two of the fixes suggested: For my per-oscillator filters I decided to switch to the state variable filters found in the new dsp classes. For any other new users, the code to start one up goes like this:

dsp::StateVariableFilter::Filter<double> filter = new dsp::StateVariableFilter::Filter<double>();
filter->parameters->type = dsp::StateVariableFilter::Parameters<double>::Type::lowPass; //or whatever other type of filter you want
filter->parameters->setCutOffFrequency(getSampleRate(), FILTER_FREQUENCY);

For my main section filter, which I don’t expect to be automated as often, and instead am more in need of the added functionality the IIR filter class provides (like the low shelf and high shelf filters) I’ve made a class that divides a range of values into a number of steps (as defined by a function, like a linear function or logarithmic one) and steps from the maximum value to the minimum one. It’s more or less eliminated pops when I don’t have any resonance applied, and I’ve decided that the pop that happens with a high resonance filter going from high filter frequencies to low filter frequencies is a feature rather than a bug, because I happens predictably and can probably be used to make kicks or subs or something. (Lazy, I know.)

If anyone is interested in these classes, feel free to use them under CC-BY-SA 2.0. which basically means you can do whatever with them. Note that these classes are a little dangerous, in that I don’t do much checking for unexpected values.

RangeStepper.h
/*
==============================================================================

    RangeStepper.h
    Created: 21 Jul 2017 2:11:43pm
    Author:  Gabriel

  ==============================================================================
*/

#pragma once

#include "../JuceLibraryCode/JuceHeader.h"

class RangeStepper {
public:
	friend class SubRange;

	enum RangeType {
		Linear,
		Exponential,
		Logarithmic,
	};

	class SubRange {
	public:
		virtual double stepVal() = 0;
		bool isFinished() { return finished; };
	protected:
		SubRange(RangeStepper* parent) : p(parent) {};
		~SubRange() {};
		virtual void startRange(int start, int end, float endVal) = 0;

		bool finished;

		RangeStepper* p;
	};

	RangeStepper(double min, double max, int steps, RangeType t, int LogarithmOrExponentBase = 2);
	~RangeStepper();
	SubRange* returnSubRange(double curr, double target);
private:
	void generateRange(RangeType t);
	int mapInputToRange(double in);

	class SubRangeStepper : public SubRange {
	public:
		SubRangeStepper(RangeStepper* parent);
		~SubRangeStepper();
		void startRange(int start, int end, float endVal) override;
		double stepVal() override;
	private:
		int startIndex, endIndex, currIndex, direction;
		double endValue;
	};

	double minimum, maximum;
	int stepsInRange;
	int base;
	ScopedPointer<SubRangeStepper> sR;
	double* range;
	RangeType thisType;
};

RangeStepper.cpp

/*
  ==============================================================================

    RangeStepper.cpp
    Created: 21 Jul 2017 2:11:43pm
    Author:  Gabriel

  ==============================================================================
*/

#include "RangeStepper.h"

/**
* Creates a RangeStepper, which will contain an array of doubles from "min" to "max" in "steps" steps, using the RangeType "t"
* For this RangeStepper to work in logarithmic mode, min and max must be greater than zero
* LogarithmOrExponentBase is the base used for generating exponential and logarithmic functions. It must be greater than 0.
*/
RangeStepper::RangeStepper(double min, double max, int steps, RangeType t, int LogarithmOrExponentBase) {
	jassert(min != max);
	jassert(steps > 0);
	jassert(LogarithmOrExponentBase > 0);

	if (min != max && min > 0 && max > 0 && steps > 0 && LogarithmOrExponentBase > 0) {

		range = new double[steps + 1];

		minimum = min;
		maximum = max;
		stepsInRange = steps;
		base = LogarithmOrExponentBase;

		generateRange(t);
		thisType = t;

		sR = new SubRangeStepper(this);
	}
}

RangeStepper::~RangeStepper(){
	if (range) {
		delete[] range;
	}
}

/**
* Generates the range of value for this RangeStepper
*/
void RangeStepper::generateRange(RangeType t) {
	double holdval;

	switch (t) {
		//generate a linear range
	case(Linear):
		for (int i = 0; i <= stepsInRange; i++) {
			range[i] = i * (maximum - minimum) / stepsInRange + minimum;
		}
		break;
		//generate an exponential range
	case(Exponential):
		for (int i = 0; i < stepsInRange; i++) {
			range[i] = pow((double)i / (double)stepsInRange, base) * (maximum - minimum) + minimum;
		}
		break;
	case(Logarithmic):
		jassert(maximum > 0 && minimum > 0);
		if (maximum <= 0 || minimum <= 0) {
			return;
		}
		holdval = log(maximum / minimum) / log(base);
		for (int i = 0; i < stepsInRange; i++) {
			range[i] = minimum * pow(base, i * holdval / stepsInRange);
		}
		break;
	default:
		break;
	}
}

/**
* Quantises an input to a member of this RangeStepper's range
*/
int RangeStepper::mapInputToRange(double in) {
	double temp = in; //In my personal code I use a utility function to return a "in" within a range from min to max. That would require code I have in another class so I removed it, but I'd suggest doing the same to avoid unexpected behavior/
	double tempVal;

	switch (thisType) {
		//maps in to the closest member of this RangeStepper's linear range
	case(Linear):
		return (int) ((temp - minimum) * stepsInRange / (maximum - minimum));
		break;
	case(Exponential):
		return (int) (pow((temp - minimum) / (maximum - minimum), 1 / base) * stepsInRange);
		break;
	case(Logarithmic):
		jassert(maximum > 0 && minimum > 0);
		tempVal = log(maximum / minimum) / log(base);
		return (int)(log(temp / minimum) / log(base) * stepsInRange / tempVal);
		break;
	default:
		return 0;
		break;
	}
}

/**
* Returns a SubRange that will step from curr to target across the intervals found in this RangeStepper
*/
RangeStepper::SubRange* RangeStepper::returnSubRange(double curr, double target) {
	sR->startRange(mapInputToRange(curr), mapInputToRange(target), target);

	return (SubRange*) sR.get();
}

//==============================================================================
/**
* A subset of a RangeStepper with a direction to iterate through the parent range
*/
RangeStepper::SubRangeStepper::SubRangeStepper(RangeStepper* parent) : SubRange(parent){
	finished = true;
}
/**
* Destructor
*/
RangeStepper::SubRangeStepper::~SubRangeStepper() {

}

/**
* Starts a SubRange
*/
void RangeStepper::SubRangeStepper::startRange(int start, int end, float endVal) {
	if (start == end) {
		finished = true;
		return;
	}

	finished = false;

	currIndex = startIndex = start;
	endIndex = end;
	direction = start < end ? 1 : -1;
	endValue = endVal;
}

/**
* Steps forward across the subrange
*/
double RangeStepper::SubRangeStepper::stepVal() {
	if (finished) {
		return endValue;
	}

	currIndex += direction;

	if ((currIndex - endIndex) * direction  < 0) {
		return p->range[currIndex];
	}
	else {
		finished = true;
		return endValue;
	}
}