Question about Filters and freq response plots

Hey all,

Hopefully you can help me understand what i’m trying to figure out. I have a synth using classes built around the IIRFilter in -12dB, -24dB, -48dB, and notch configurations. Right now in my synth the filter is being controlled by cutoffFreq and resonance parameter Sliders, as well as ADSR and LFO modulation, and it’s being processed every sample in mono. (I break out the signal to stereo at the end of my process block.)

I want to add a visualization of the different filters’ rolloff and resonance. After several days of trying to pick apart and decipher different bits of code or math i’ve found online, on the forum, or in the different Juce filter & coefficient classes, I’m stuck.

What do you suggest is the best way to tackle this? Or is there some resource that would help a non-math-expert explain exactly what i need to calculate to draw a Path component along the frequency response of the different filters?

I’ve thought about changing to the dsp::IIR::Filter to try to utilize dsp::IIR::Coefficients getMagnitude method, but when i implemented a dsp::IIR::Filter, it was nothing but pops and clicks, as expected because of the ADSR/LFO.

I’m also willing to fake-it, and draw curved paths that emulate what the different filters look like, if it’ll save me more grief.

If you need to see any specific bits of my code, let me know. Didn’t want to just plaster my post with needless code-clutter.

Thanks

It’s a bit unclear – are you or are you not utilizing the juce IIR class?

It’s quite easy, you use the getMagnitudeForFrequency function to get the magnitude response as a hz value

        Path p;
        
        for (int i = 0; i < getWidth(); i += 1)
        {
            // convert from pixel space to frequency space
            float frequency = position_to_frequency((float)i / (float)getWidth());

            // get the magnitude for that frequency
            float mag = mFilterCoeffs->getMagnitudeForFrequency(frequency, 44100);

            // do a bit of filtering how you like so it fits nicely into the display
            float db_scale = 12.f;
            float decibels = Decibels::gainToDecibels(mag);
            decibels = jlimit(-db_scale, db_scale, decibels);

            // create the y pos
            float yPos = jmap(decibels, -db_scale, db_scale, 1.f, 0.f);


            // create the path
            if (i == 0) {
                p.startNewSubPath(i, yPos * getHeight());
            } else {
                p.lineTo(i, yPos * getHeight());
            }
        }
        
        // stroke the path 
        g.strokePath(p, PathStrokeType(2));

Praise JUCE for the convenient helpers on something which is otherwise quite complicated

And you can absolutely use the IIRs with modulation, just need to add smoothing

I tried doing this a while ago and I ran into issues. There are two IIR filters in juce, only the one in the dsp module offers getMagnitudeForFrequency. However, you can’t change the parameters of this filter without calling new, so trying to use smoothing it makes CPU usage go crazy.

The other juce IIR filter you can change the parameters on the fly, but it doesn’t offer getMagnitudeForFrequency.

1 Like

Thanks! I’ll take a look at your code when i have some time tonight or tomorrow morning, and see how it works.

To answer your question, like @g-mon said, there are two different IIR Filters: one in the dsp Module, dsp::IIR::Filter, and one in the standard modules called IIRFilter. I’m using the latter, IIRFilter.

Ahh yeah true I forgot about that…

What I’ve done is just copied the IIR code and changed it to take a coefficient struct by reference and modify it’s coefficients instead of calling new…

It’s a bit of a pain but it makes the class usable

So, I had a go using your code and the problem i run into is right about here:

Because i used IIRFilter, not dsp::IIR::Filter, i tried writing my own version of getMagnitudeForFrequency() using Juce’s as a guide. And i think that’s where my math skills failed me again. I was getting values for magnitudes, but they weren’t quite right, and it’s been so long since i did math using complex numbers that i didn’t know where my version of the method went wrong. Otherwise, i could follow the rest of your code and see it looked like it was working as properly as it could with my incorrect magnitudes.

    void FilterVisual::drawLowPass2P()
    {
        setLowpassCoefficients(sampleRate, cutoffFreq, resonance);
        
        // for each increment of the width of the visual space
        for (int i=0; i<getWidth(); i++)
        {
            // Convert the X position to a frequency, normalized 0-1
            float xFreq = positionToFrequency( (float)i / (float)getWidth() );
            int order = 4;//(5 - 1) / 2;  // juce: (coefficients.size()) - 1) / 2;
            
            // Get magnitude for that frequency.
            float mag = getMagAtFreq(xFreq, sampleRate, order); // = filtCoeff.getMagnitudeForFrequency( (double)xFreq, (double)sampleRate );  // *** MAGNITUDE FUNCTION GOES HERE SOMEHOW ***
            DBG(mag);
            
            // Filtering to fit nicely in the display
            float dbScale = 12.0f;
            float dBs     = Decibels::gainToDecibels( mag );
            DBG(dBs);
            dBs = jlimit( -dbScale, dbScale, dBs );
            
            // Create Y position
            float yPos = jmap( dBs, -dbScale, dbScale, 1.0f, 0.0f );
            
            // Create path
            if (i==0)
            {
                filterShape.startNewSubPath( i, yPos * getHeight() );
                //DBG(yPos * getHeight());
            }
            else
            {
                filterShape.lineTo( i, yPos * getHeight() );
                //DBG(yPos * getHeight());
            }
        }
        DBG("");
    }


    void FilterVisual::setLowpassCoefficients(float SR, float CO, float Q)
    {
        float n = 1.0f / std::tan(MathConstants<float>::pi * CO / SR );
        float nSquared = n * n;
        float invQ = 1.0f / Q;
        float c1 = 1.0f / (1.0f + invQ * n + nSquared);
        
        a0 = c1;
        a1 = c1 * 2.0f;
        a2 = c1;
        b0 = 1.0f;
        b1 = c1 * 2.0f * (1 - nSquared);
        b2 = c1 * (1.0f - invQ * n + nSquared);
    }

    float FilterVisual::getMagAtFreq(float freq, float SR, int order_)
    {
        constexpr std::complex<float> j (0.0f, 1.0f);
        int order = order_;
        float coefs[] = { a0, a1, a2, b0, b1, b2 };
        
        std::complex<float> numerator = 0.0f, denominator = 0.0f, factor = 1.0f;
        std::complex<float> jw = std::exp( -MathConstants<float>::twoPi * freq * j / sampleRate );
        
        for (int n = 0; n <= order; ++n)
        {
            numerator += static_cast<float>(coefs[n]) * factor;
            factor *= jw;
        }
        
        denominator = 1.0f;
        factor = jw;
        
        for (int n = order + 1; n <= 2 * order; ++n)
        {
            denominator += static_cast<float>(coefs[n]) * factor;
            factor *= jw;
        }
        
        return  std::abs(numerator / denominator);
    }

So, i resigned myself to just drawing the filter curves as “ideal forms” using the cutoff and resonance without the actual filter magnitudes. Honestly, that’s all that’s necessary.

But thanks for the input, nonetheless. :+1:

You can simplify the math a bit if your order is fixed to 2.

auto w = 2.0f * pi * frequency / sample_rate; 
std::complex<float> z (cos(w), sin(w)); // e^jw = cos(w) + jsin(w)
auto num = a0 * z * z + a1 * z + a2;
auto den = b0 * z * z + b1 * z + b2;
return std::abs(num / den);

You might want to double check the sign of your denominator coefficients as well, and sanity check your coefficient computation algorithm in matlab/octave/python.

1 Like

Awesome. Thanks! I’ve switched to an “emulated response curve” path. But I’ll definitely be coming back to this idea. Will see if i can get an accurate visualization from magnitude going with this bit of clean-math.