Simple LPF code?

You’re not assigning c++ or ++c to anything in the for statement, so in this context it makes no difference whether you prefix or postfix ++, c still starts at zero and gets incremented on each iteration.

1 Like

Oh, wow. Why did I have it stuck in my brain that ++c would increment before the conditional was evaluated?! My bad!

1 Like

We’ve all been there :slight_smile:

But the prefix means the value isn’t needed so a copy gets skipped

1 Like

Tho compilers will do that now for ints in a loop, so it’s really just an old habit

1 Like

That has never occurred to me, thank you.

@baconpaul I almost have the lowpass filter working perfectly. There’s just a bit of tweaking that I need to do.

Would you have a moment to answer a question?

I’m trying to figure out what “co” means in the following code.

void SawDemoVoice::StereoSimperSVF::setCoeff(float key, float res, float srInv)
{
    auto co = 440.0 * pow(2.0, (key - 69.0) / 12);
    co = std::clamp(co, 10.0, 15000.0); // just to be safe/lazy
    res = std::clamp(res, 0.01f, 0.99f);

Midi key #69 is A4, and A4 is 440.0 hertz, which offers a clue. I’m not understanding, conceptually, why the filter cutoff is based on a “key” value though. No big surprise since my understanding of filters is pretty limited!

If my cutoff knobs ranges from 0 to 1.0, what would be an equation for co which ranges from fully muted to fully open? Much appreciated!

I have absolutely no idea about the code… but

I’m going to take a totally wild guess that ‘co’ might stand for ‘Cut Off’.

1 Like

Sorry, I knew that part! Ha ha ha. I suppose I should have said, “Why is co computed the way it is?”

Haha - well I could make a completely wild guess about that too but…

probably best if I don’t. :slight_smile:

I see a similar computation here:

https://www.musicdsp.org/en/latest/Filters/92-state-variable-filter-double-sampled-stable.html

Maybe I can hack something together.

Oh - possibly an idea as to why filter cutoff is based on a “key” value…

Filters tend to track 1V/oct so that you can use them as a voice when self-oscillating, by sending 1V/oct sequences into Cutoff CV.

In that sense filter cutoff frequency is similar to VCO frequency. And VCOs tend to use 440Hz/A4 as the 0V baseline such that positive CV increases the freq from there and negative CV reduces it.

I’ll get back in my box now :slight_smile:

Thanks @steve !

Experimentally, this seems to be close, if not correct:

// Where float cutoff ranges from 0 to 1.0
float key = 69 + (cutoff * 12);
float tuned_cutoff = 440.0 * (pow(2.0, key - 69.0) / 12);
tuned_cutoff = clamp(tuned_cutoff, 10.0, 15000.0);

Actually, it looks like:

float key = 69 + (cutoff * 8.68);

results in a value where tuned_cutoff = 15,039 at cutoff == 1.0. That’s close to the maximum of 15000, where it’s cropped down. I have no idea where these numbers are coming from.

Yeah exactly it is volt per octave for a midi device Doing it in log space makes modulations what you expect Nothing special. Just cutoff frequency is in midi key.

1 Like

Is it really efficient enough to do 2**x on every sample?

I would recommend if you recalibrate the filter on every sample you either smooth coefficients or use an approximation

The tanh is more expensive though

I think in the clap saw demo I calibrate every 8 blocks only if cutoff has changed but I didn’t spend a lot of time on that. In surge we have approximations and smoothers including of coefficients sure

I think that is you usual extended way of saying “recalculating the filter coefficients on every sample is going to waste a lot of CPU”?

Here’s how my version of the code is shaping up. The expensive 2**x is computed only when the cutoff or resonance values are changed. I’m totally open minded to changes.

#pragma once

// See https://github.com/surge-synthesizer/clap-saw-demo/blob/main/src/saw-voice.cpp

enum Mode
{
    LP,
    HP,
    BP,
    NOTCH,
    PEAK,
    ALL
};

struct Filter
{
    // Filter characteristics
    int mode = LP;
    float cutoff = 69.0;
    float resonance = 0.7;
    float pival = 3.14159265358979323846;
    float sample_time = 1.0 / 44100;

    float ic1eq[2] = {0.0, 0.0};
    float ic2eq[2] = {0.0, 0.0};
    float g = 0.0;
    float k = 0.0;
    float gk = 0.0;
    float a1 = 0.0;
    float a2 = 0.0;
    float a3 = 0.0;
    float ak = 0.0;

    Filter()
    {
        sample_time = APP->engine->getSampleTime();
        this->recalculate();
    }

    void setCutoffAndResonance(float cutoff, float resonance)
    {
        this->cutoff = cutoff;
        this->resonance = resonance;
        this->recalculate();
    }

    void setCutoff(float cutoff)
    {
        this->cutoff = cutoff;
        this->recalculate();
    }

    void setMode(int mode)
    {
        init();
        this->mode = mode;
        this->recalculate();
    }

    void recalculate()
    {
        float key = 69 + (cutoff * 8.68);
        float tuned_cutoff = 440.0 * (pow(2.0, key - 69.0) / 12);
        tuned_cutoff = clamp(tuned_cutoff, 10.0, 15000.0); // just to be safe/lazy

        resonance = clamp(resonance, 0.01f, 0.99f);
        g = std::tan(pival * tuned_cutoff * this->sample_time);
        k = 2.0 - 2.0 * resonance;
        gk = g + k;
        a1 = 1.0 / (1.0 + g * gk);
        a2 = g * a1;
        a3 = g * a2;
        ak = gk * a1;
    }

    void init()
    {
        for (int channel = 0; channel < 2; ++channel)
        {
            ic1eq[channel] = 0.f;
            ic2eq[channel] = 0.f;
        }
    }

    void process(float *left, float *right)
    {
        float vin[2] = {*left, *right};
        float result[2] = {0, 0};

        for (int channel = 0; channel < 2; channel++)
        {
            auto v3 = vin[channel] - ic2eq[channel];
            auto v0 = a1 * v3 - ak * ic1eq[channel];
            auto v1 = a2 * v3 + a1 * ic1eq[channel];
            auto v2 = a3 * v3 + a2 * ic1eq[channel] + ic2eq[channel];

            ic1eq[channel] = 2 * v1 - ic1eq[channel];
            ic2eq[channel] = 2 * v2 - ic2eq[channel];

            // I know that a branch in this loop is inefficient and so on
            // remember this is mostly showing you how it hangs together.
            switch (mode)
            {
            case LP:
                result[channel] = v2;
                break;
            case BP:
                result[channel] = v1;
                break;
            case HP:
                result[channel] = v0;
                break;
            case NOTCH:
                result[channel] = v2 + v0; // low + high
                break;
            case PEAK:
                result[channel] = v2 - v0; // low - high;
                break;
            case ALL:
                result[channel] = v2 + v0 - k * v1; // low + high - k * band
                break;
            }
        }

        *left = result[0];
        *right = result[1];
    }
};

like @baconpaul says (and my ancient paper on efficient plugins does, too) these kinds of things should be done a) only when the change, and b) even if the “change” every sample, don’t recalc every sample unless you really want to. Even an LFO will “change” every sample.

Yeah and with the filters you can cast the coefficients as having a linear change over a sample period and it will be stable if you do a per block thing

Like I said in the vst host and plugin thread, even module developers need to think about block size smoothing!