I’m working on a plugin that takes the incoming signal and applies a simple EQ boost (bell shaped) where the frequency of the filter moves up and down between a fixed range using an LFO (we can control the speed of the motion via a knob).
For the filter I’m using juce::dsp::IIR::Filter<float> and the frequency is controlled by a juce::dsp::Oscillator.
Here’s the problem: if I update the filter coefficients at each sample, then there are no artefacts when the filter sweeps (but this eats up a lot of CPU). If, however, I update the filter every say 100 samples or so, I start to hear some high-frequency noise/glitches, especially if I set the frequency range to move into the upper frequencies, say from 200hz to 17000hz (I tried setting the range from 200hz to 2000hz and seems fine!) .
Those glitches are not really audible when processing an a complex signal, but they become evident when processing a simple sine wave (which I’ll probably never do in real life, but just did as a test).
I have read that if you require “fast modulation” it is better to use a StateVariableFilter however from what I can see that only supports low-pass, high-pass and band-pass, while I need a “bell” filter (notch?). Also, I’m not sure what “fast modulation” is in this context; my LFO will actually be very slow, say from 0.01hz to 0.5hz maximum.
On a related note, I noticed that also quickly moving e.g. the Q or gain knobs produces the same issue; I’m using SmoothedValues already, with a ramp time of 0.0005; I can get the artefacts to disappear if I set the number to something much bigger, but then the plugin becomes unusable.
Does anybody have any thoughts about this? Am I doing something wrong or that’s just the way it works?
Sounds like this is zipper noise indeed. You’re already smoothing the knobs but still updating the filter coefficients in steps, so that still gives zipper noise. You can try doing a linear interpolation between the filter coefficients over those 100 samples.
The StateVariableFilter can be used to create a bell or notch filter by combining the different outputs. But the problem you’re having doesn’t sound like it’s due to fast modulation, since your modulation is very slow, and it works OK if you calculate the coefficients on every sample.
if you imagine non-sample accurate filter updates as a signal it would be a steppy one, a little discontinuity every 100th sample. that’s why it makes noise at that implied frequency
So are you suggesting every 100 (or whatever I’ll set this interval to be) samples instead of just getting the LFO value and calculating the new frequency, to instead calculate this new frequency, interpolating with the previous one (that I’ll have saved as a member) and then use the interpolated frequency?
Or are you referring to actually interpolating all the coefficients?
The way I obtain the coefficients is via juce::dsp::IIR::Coefficients<float>::makePeakFilter(...).
I wonder in general what’s the standard way to solve this problem? It sounds like virtually every plugin must deal with this, is there any best practice or standard solution?
You can interpolate either but the coefficients is more efficient. Interpolating between the frequencies would be the same as smoothing the parameter and calculating the coefficients on every sample.
Ah - I think I misunderstood your initial answer then. Let me check if I’m understanding correctly now…
So, what I’m doing at the moment is computing the coefficients and updating the filter once every 100 samples (which means the filter frequency is updated in steps, possibly causing zipper noise).
Are you suggesting I could still compute the coefficients once every 100 samples but update the filter at each sample instead of every 100 (using interpolated coefficients in between those 100 samples)?
If so, isn’t that still expensive? Isn’t the point of “control rate” modulation to avoid updating certain DSP blocks at each sample? Or is this gonna be ok because the actually expensive bit is calculating the coefficient but once we have them, updating the filter is basically just a few assignments?
In my dynamic EQ plugin, when I change the coeff updating scheme from per 1ms to per sample, Reaper shows that the CPU load increases from 0.06% to 0.13% (of one filter). I still think this kind of CPU consumption is acceptable. If your CPU load rises much more than that, better check you code to make sure there is no allocation (e.g., juce::dsp::IIR::Coefficients<float>::makePeakFilter) or system calls. You may also re-write the IIR filter to avoid some redundant calculations.
You can map coefficients of TDF-II to coefficients of SVF easily. It does become more stable. However, it introduces much more phase shift (even for peaking filters). See the reference below for details.
Yeah my CPU goes from 1% (which is already a lot I guess) if I update the filter every 100 samples, to about 7% if I update the filter at each sample. Definitely something wrong there
In my processBlock I actually use juce::dsp::IIR::Coefficients<float>::makePeakFilter to generate the new coefficients every time, could that be part of the problem? But if I’m not supposed to use makePeakFilter inside processBlock, what should I use instead?
Calculating the coefficients from the frequency and Q factor is relatively slow because it involves a tan or sin and cos function. However, if you only calculate them every 100 samples this is not so bad.
Doing a linear interpolation between the new and old coefficients over those 100 steps is pretty fast and simple to code:
where step goes between 0 and 99 (or 1 and 100). After 100 steps, you set old_coeff = new_coeff and calculate a new new_coeff from the current filter frequency and Q.
It’s fairly common for things like synths to do their parameter updates every 32 samples or so and interpolate everything in between.
That CPU usage is abnormal. Never use juce::dsp::IIR::Coefficients<float>::makePeakFilter in processBlock. Use ArrayCoefficients instead. See more info at:
BTW, I see about 5~10% reduction in CPU usage if I use my own IIR filter (basically copy most code from juce::dsp::IIR and remove some redundant checks).
Just a quick follow up: since the coefficients are wrapped inside a dsp::IIR::Coefficients struct, what’s the easiest way to do this interpolation in practice? Should I handle each of the 6 coefficients separately as a plain float and then wrap them into a dsp::IIR::Coefficients again when it’s time to update the filter? Or is there a more convenient or built-in way to do it?
Thanks for the tips. I’m going to take a look at the post you linked and will definitely investigate using ArrayCoefficients so that we avoid allocating in the processBlock
Just a side note: % of CPU is very coarse and machine dependant, so you cannot compare with other developers
And of course never look at CPU readings of a debug build.
The filter internally does coefficients->getRawCoefficients() to get the coefficient values. So if you modify the coefficients member of the IIR::Coefficients object, then the next time the filter runs it will pick up the new coefficients. Like so:
Hey, I’m trying to replace dsp::IIR::Coefficients<float>::makePeakFilter with dsp::IIR::ArrayCoefficients<float>::makePeakFilter based on your suggestion.
I’m not sure what I’m doing wrong but as soon as I do the swap the filter stops working; it does a “pop” when it’s loaded and then complete silence.
The first one is a juce::Array while the second one is a std::array. I am not sure whether it makes a difference in your code. You may want to check the following demo:
Thanks for your reply! I’m not sure what’s wrong but still have the filter not working when replacing IIR::Coefficients<float>::makePeakFilter with IIR::ArrayCoefficients<float>::makePeakFilter.
Here’s how my code is set up: in the processor class, I have a member
std::array<float, 6> newCoefficients;
that will be combined with other things and eventually the values will be fed into the filter coeffs.
Then, in processBlock, every few samples I update it using one of the two methods above; one works and the other doesn’t; here’s the test code I’m looking at:
// OK - filter works
auto testCoefficients_ok = juce::dsp::IIR::Coefficients<float>::makePeakFilter (
getSampleRate(),
cutoffFreqHz,
currentIntensitySmoothed,
juce::Decibels::decibelsToGain (currentAmountSmoothed))->coefficients;
// FAILS (filter does initial "pop" then silence)
auto testCoefficients_broken = juce::dsp::IIR::ArrayCoefficients<float>::makePeakFilter (
getSampleRate(),
cutoffFreqHz,
currentIntensitySmoothed,
juce::Decibels::decibelsToGain (currentAmountSmoothed));
// copy into our member variable (will be used for processing)
for (auto c = 0; c < 6; ++c)
{
newCoefficients[c] = testCoefficients_ok[c];
//newCoefficients[c] = testCoefficients_broken[c]; // not working!!
}
I’m pretty sure there’s something stupid going on here, but I really can’t understand what… The difference between std::array and juce::Array shouldn’t be a problem here I think? I’d expect those two things to be completely interchangeable but something else is going on apparently!