Simple LPF code?

Yep, understood. The cutoff and frequency values are being used in my Groovebox module, and they’re only updated at most every step of the drum machine. I’ll check the CPU usage to see if I notice any blips.

oh, we know that. My (again ancient) LFN used to be kind of popular. It is a noise generator running into a graphic equalizer with bands at like 1 hz, 2 hz, .5hz… I only run it like every 200 samples, then smooth (one pole LPF) at full rate to avoid the jaggies.

I hope these kinds of things are knows by devs who are doing DSP in VCV…

My only comment is if you change cutoff and resonance you redalculate twice

I would probably myself have a std atomic bool for dirty have the set methods force dirty true and have profess recalc and reset dirty dirty, using either the naive if set if it is one thread or maybe even an atomic compare thingy if sets can come from non audio threads often

what? are there multiple threads in here? That’s pretty unusual for a VCV module. If there’s only one thread (the audio thread) then a normal bool is just fine, right?

Correct

I definitely do not! Which is why you don’t see me launching any filter modules. Ha ha ha. I’m the sample and sequencer guy, not the DSP guru. :grinning:

But I couldn’t help but to add a lowpass filter to the groovebox module, and I appreciate all of your help in making that happen. It looks like I’m good to go!

5 Likes

This :point_up_2: seems like a smart idea! I went ahead and implemented it. Here’s my updated file:

#pragma once

// From: https://github.com/surge-synthesizer/clap-saw-demo/blob/main/src/saw-voice.cpp
// Reformatted to fit my personal coding style.

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

struct Filter
{
    int mode = LP;
    float cutoff = 1.0;
    float resonance = 0.7;
    float pival = 3.14159265358979323846;

    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;

    bool dirty = true;

    Filter()
    {
        dirty = true;
    }

    // cutoff value should range from 0 to 1
    void setCutoff(float cutoff)
    {
        this->cutoff = cutoff;
        dirty = true;
    }
    
    // resonance should range from 0 to 1
    void setResonance(float resonance)
    {
        this->resonance = resonance;
        dirty = true;
    }

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

    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 * APP->engine->getSampleTime());
        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)
    {
        if(dirty) recalculate();

        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];

            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];
    }
};


yeah great. the only other thing is: if you modulate this with like an LFO or something it will dirty on every sample. In that case there’s a few strategies but the one which we use in surge is: recalculate the coefficients every N blocks (in surge VST N=32; surge modules N=8). When we do calculate coefficient and dcoefficient = (new_coeff - current_coeff)/N. Then on each step do coefficient += dC.

This means you get better results if your coefficients are in ‘sorta linear’ form. That is, your function is F not 1/F type things.

There’s other strategies too of course, but just: watch out for CV control with the code you outlined. But it is great for knob control if you don’ use the param mechanism. (You can also check param changes etc… in the similar way)

1 Like

Good point. I suppose that I’m an edge case – in that there’s no way to modulate the filter at that rate in my module. Or, at least it would be highly unlikely. If I end up using the filter in other situations, I’ll do as you recommend and post the updated code. :man_bowing:

2 Likes

You might remember this is exactly what I said, above, a few days ago.

With modules like CV-Map, it’s always possible for someone to map an audio-rate input to these knobs. But there are lots of modules where this is a bad idea. Just something to keep in mind.

No worries.

The knobs are only applied to the parameter locks when a step is triggered. (Sort of like a sample-and-hold). Even using CV-Map to automate knob tweaking, the lowpass filter properties would only be updated every step.

I ran a test using an oscillator as the clock source to reach a ridiculously high BPM and applied random lowpass values to all 8 tracks. I didn’t see any major CPU trouble. (Although, mental note, I should do some profiling on the module to see if I can reduce the CPU usage in general.)