IIR filter pops when state changes (even with crossfading)

Hi,

I’ve been lucky enough to get my code working as well as it does already, but a minor detail is proving to be difficult to debug. I have a program that filters audio until a change in the UI is detected (I have a face tracker that makes the command for the filter to change based on my location on the screen). It then crossfades from filter to another using the .state variable. The pop happens after I update the filter with the state change, so I suspect this is why it happens. But I have no idea what to do as the solution.

Here are the relevant pieces of code. Let me know if you need to see something else, too.

void MainComponent::prepareToPlay (int samplesPerBlockExpected, double sampleRate)
{
transport.prepareToPlay(samplesPerBlockExpected, sampleRate);
lastSampleRate = sampleRate;

CF_FLAG = false;

dsp::ProcessSpec spec;
spec.sampleRate = sampleRate;
spec.maximumBlockSize = samplesPerBlockExpected;
spec.numChannels = otherDeviceManager.getCurrentAudioDevice()->getActiveOutputChannels().countNumberOfSetBits();

currentFilterState = 1;
lastFilterState = 1;

crossfadeLength = 2;
smoothedGain.reset(sampleRate, crossfadeLength);
smoothedGain.setTargetValue(1);

CFBuffer.setSize(otherDeviceManager.getCurrentAudioDevice()->getActiveOutputChannels().countNumberOfSetBits(),
        samplesPerBlockExpected);

filter1.reset();
filterCF1.reset();
updateFilter(1);

filter1.prepare(spec);
filterCF1.prepare(spec);

}

void MainComponent::updateFilter(int state_filter)
{
if (state_filter == 1)
{
*filter1.state = *coeffs1;
*filterCF1.state = *coeffs2;
}
else
{
*filter1.state = *coeffs2;
*filterCF1.state = *coeffs1;
}
}

void MainComponent::getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill)
{
// Check if UI wants filter to change
if (currentFilterState != lastFilterState && CF_FLAG == false)
{
CF_FLAG = true; // Crossfade
}

bufferToFill.clearActiveBufferRegion();
CFBuffer.clear();
transport.getNextAudioBlock(bufferToFill);
dsp::AudioBlock<float> block(*bufferToFill.buffer,
    (size_t)bufferToFill.startSample);

for (int j = 0; j < 2; j++)
{
    CFBuffer.copyFrom(j, 0, bufferToFill.buffer->getReadPointer(j), bufferToFill.numSamples);
}

dsp::AudioBlock<float> block_CF(CFBuffer, 0);

// Process the outgoing audio with the corresponding filter
filter1.process(dsp::ProcessContextReplacing <float>(block));

// Begin crossfading if the flag is on
if (CF_FLAG == true)
{
    float sampleCF = 0;
    filterCF1.process(dsp::ProcessContextReplacing <float>(block_CF));
    lastFilterState = currentFilterState;

    for (int i = 0; i < bufferToFill.numSamples; i++)
    {
        const auto gain = smoothedGain.getNextValue();
        sampleCF = block.getSample(0, i) *
            (1.0 - gain) + block_CF.getSample(0, i) * gain;
        block.setSample(0, i, sampleCF);

        sampleCF = block.getSample(1, i) *
            (1.0 - gain) + block_CF.getSample(1, i) * gain;
        block.setSample(1, i, sampleCF);
    }

    // Reset the crossfade if previous crossfading event has ended
    if (smoothedGain.getCurrentValue() == 1)
    {
        smoothedGain.setTargetValue(0);
        smoothedGain.skip(crossfadeLength * lastSampleRate);
        CF_FLAG = false;
        smoothedGain.setTargetValue(1);
        updateFilter(currentFilterState);   // Change filter
    }
}

}

I hope it’s nothing too obvious. I’m still new to JUCE and C++ so I appreciate any help I can get. Thank you!

Bump! I’m really a bit lost with this issue and would appreciate any help!

Not an easy solution but Vadim Zavalishin’s free book “Virtual Analog Filter Design” instructs on how to design/code filters with better modulation behaviours

Thank you for the response! So would you say that this issue is due to how (JUCE) IIR filters work in general and not a bug in how I’m using the state change / crossfade?

I haven’t looked into your code so I can’t rule out it’s that too

Fair enough! Thanks a lot for the help thus far! :slight_smile:

You maybe use a topology preserving filter (like the ones described by vladim) if you want to make fast modulations to the filter. For example when you modulate the filter with an oscillator.

For a normal use case a common IIR filter should be enough stable also when the user changes the value with a slider. Just make sure you smooth the filter cutoff value. I think JUCE already offers solutions for this. See here:

I never used the JUCE internal filter classes, so i can’t tell you how this works when you do block processing.

Thank you very much for the link and the information.

The thing is, I’m using SmoothedValue for the crossfade, and I’ve tried with a 2 second long crossfade. I still get a pop when I change the filter so I suspect I’m using things wrong somehow. But I’m stuck and I have no idea how to debug it.

I don’t understand that block processing. You have to make sure that you change the filter state only for the filter that has the gain at zero.

I would not do it this way with two filters that fade in and out. You maybe also get phasing effects if you fade the signals (every filter introduces delays at some frequencies).

I would smooth the values that set the filter state. For example if the cutoff goes from 0 to 100, then i would smooth this. With a state variable filter you can even “morph” the filter type.