Porting moog ladder filters to audio objects?

Just to get everyone caught up... the latest code is on github.

https://github.com/PaulStoffregen/Audio

The simplest way to use it is to click the green "Code" button and download the zip file. Extract it in {Documents}/Arduino/libraries, where it will override the Teensyduino-installed library. (later, remember to delete it, so this copy doesn't forever override the installer's audio library)

Once you've installed the new library, restart Arduino, then click File > Examples > Audio > Synthesis > LadderFilter to open the example program which demonstrates using both of the control inputs to modulate frequency and resonance at full audio bandwidth.

Here is a copy of the design tool with the new ladder filter added.

https://www.pjrc.com/teensy/gui/index2.html?info=AudioFilterLadder

I'm really looking forward to hearing what you think of this. Is it ready for release? Are there still issues? Does it really give "Moog Sound"?
 
Last edited:
I'm really hoping el_supremo, martianredskies, houtson, RogerD and others will find time to give the latest code a try over the next several days.

I'm happy to add the passband gain, change the allowed ranges, even include a 2nd model. We're currently using only around 1% CPU on Teensy 4.x without modulation, and somewhere between 2-3% with both control inputs. If a more complex model would give real benefit, seems like we have plenty of CPU time available.

But I do need to work on other non-audio stuff too, so maybe best to leave the code on github alone for a day or two to allow time for everyone else on this thread to catch up and give it a try?

Wow, I had forgotten about this discussion. Have a newborn so I’ve been absent from teensy for the last 6 months or so, I’m happy to see it get incorporated into the library.

I will dust off my 4.0 I bought and try and give it a play in the next week.

Nice work, Paul!

Strangely I didn’t get any update notices to this forum until just today, which is what contributed to me forgetting about it.
 
Paul/Richard - looks like great progress, can't wait to give it a try at the weekend and will be sure to give some feedback, cheers Paul
 
Just to get everyone caught up... the latest code is on github.

https://github.com/PaulStoffregen/Audio

The simplest way to use it is to click the green "Code" button and download the zip file. Extract it in {Documents}/Arduino/libraries, where it will override the Teensyduino-installed library. (later, remember to delete it, so this copy doesn't forever override the installer's audio library)

Once you've installed the new library, restart Arduino, then click File > Examples > Audio > Synthesis > LadderFilter to open the example program which demonstrates using both of the control inputs to modulate frequency and resonance at full audio bandwidth.

Here is a copy of the design tool with the new ladder filter added.

https://www.pjrc.com/teensy/gui/index2.html?info=AudioFilterLadder

I'm really looking forward to hearing what you think of this. Is it ready for release? Are there still issues? Does it really give "Moog Sound"?

Thanks for sending this Paul. At this point all I can say is that I'm fairly impressed. I set up a testing Teensy for this last night and I can say you are certainly on the right track and I can now see that this could possibly be doable. More range is needed on the resonance though and that also needs a little (hard to explain) depth. I'm still testing though and more tweaking of my comparisons are needed.

While I have not got a MiniMoog I have been comparing it to an old evaluation version of the "Arturia MiniMoog V" that I have for comparison to hand built modules (you are doing better than mine so far). This was given the thumbs up as a good emulation by Bob Moog about a year or two before his passing and it is an impressive effort, very close to the original. (Frustratingly I had a hunt for the installer this morning and can't find it to send a copy. I possibly could get it from the public library that I originally found it in (if they still have it) but it looks like we are going into lockdown again.) I have also had a brief fiddle with a genuine 1st gen Minimoog and I'd have to agree, the Arturia is great and is found in recording studios for good reason. (Arturia has a brilliant secret formula for managing high frequency aliasing that works very, very well). I have also been trying it against a Behringer version of the filter but then while it is a pretty nice ladder filter that is not quite Moogish either.

One thing that may help though is that Moog Music is distributing a small, free emulation for the MiniMoog for iPad and iPhone that should give a reasonable version of the effect and should be a helpful aid. https://apps.apple.com/us/app/minimoog-model-d/id1339418001#?platform=ipad
 
Thanks for sending this Paul. At this point all I can say is that I'm fairly impressed. I set up a testing Teensy for this last night and I can say you are certainly on the right track and I can now see that this could possibly be doable. More range is needed on the resonance though and that also needs a little (hard to explain) depth. I'm still testing though and more tweaking of my comparisons are needed.

The paper that I used to derive the code for this ladder filter is by Valimaki and Huovilainen (Computer Music Journal, June 2006). My impression is that the it was designed specifically to be very low on CPU requirements, so it's not going to be able to produce all the nuances of the moog ladder.

I mentioned to Paul earlier in this discussion that I wanted to limit the intensity of the self oscillation because it otherwise produced noticeable aliasing when cutoff was above about 11kHz. However, I had another look at the paper today, and noticed there may be a way partially remedy this. If so, I'll revise the code and ask Paul to update the Audio library accordingly.

Cheers,
Richard
 
For what it is worth it is not possible to exactly simulate non-linear analog filter with feedback in digital domain because of the delay introduced by digitization.
So called "zero delay" filters were developed to address that problem https://urs.silvrback.com/zero-delay-feedback

Also each non-linearity introduces harmonics, so as soon as you go into saturation range you can generate content beyond half of sampling rate and get terrible aliasing.
tahn() has infinite harmonics because its Taylor expansion is infinite https://math.stackexchange.com/questions/1052884/taylor-series-expansion-of-tanh-x. x^3-type nonlinarity gives 3rd harmonic. If you use Tahn without oversampling are always going to get aliasing.

I once programmed distortion effect using tanh() and even with 8x oversampling and 64-tap polyphase antialiasing filter it was a challenge to get rid of aliasing.
 
Last edited:
Was busy with other stuff, but over the weekend I intend to pull that code and check against analog 4pole "moog" filter simulation in hardware Virus TI synthesiser. Guys from Kemper Music are famous from good sounding digital filters. Their analog model goes into self-oscillation easily and produces relatively clean sine wave (2% harmonic distortion). I am pretty sure it is oversampled because it eats lots of synth DSP power (albeit the synth only uses two Freescale DSP 56367 so its computing power is smaller than Teensy, but on the other hand I know that they coded synth engine in DSP assembler so it is hand-optimized.)
 
Last edited:
I have this morning discovered that in adapting my code to be better matched to the rest of the library, we have accidentally introduced an error that allows resonance to go higher than intended when it's modulated. I will post the corrected filter_ladder.h file here shortly.

As I mentioned, there may also be an additional correction possible to improve high Q/Fc performance. I'm currently looking into this.
 
Here is the corrected filter_ladder.cpp file. It re-instates the limit check on the total modulated 'K' value, which had gone missing. I have also increased the max allowable resonance to 1.2 since more resonance seems a priority. But I've also limited the max Fc to about 11kHz for now to avoid excessive aliasing. You can change these values to your own preferences by changing the lines:
#define MAX_RESONANCE ((float)1.2)
#define MAX_FREQUENCY ((float)(AUDIO_SAMPLE_RATE_EXACT * 0.249f))

I think my own preference is about 1.1 for MAX_RESONANCE. Maye we should make this an exposed parameter so you don't have to carefully adjust your modulator magnitudes?

Code:
-------------------------------
/* Audio Library for Teensy, Ladder Filter
 * Copyright (c) 2021, Richard van Hoesel
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice, development funding notice, and this permission
 * notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

//-----------------------------------------------------------
// Huovilainen New Moog (HNM) model as per CMJ jun 2006
// Implemented as Teensy Audio Library compatible object
// Richard van Hoesel, Feb. 9 2021
// v.1.02 now includes both cutoff and resonance "CV" modulation inputs
// please retain this header if you use this code.
//-----------------------------------------------------------

// [URL]https://forum.pjrc.com/threads/60488?p=269755&viewfull=1#post269755[/URL]
// [URL]https://forum.pjrc.com/threads/60488?p=269609&viewfull=1#post269609[/URL]

#include <Arduino.h>
#include "filter_ladder.h"
#include <math.h>
#include <stdint.h>
#define MOOG_PI ((float)3.14159265358979323846264338327950288)

//#define MAX_RESONANCE ((float)1.07)

#define MAX_RESONANCE ((float)1.2)
#define MAX_FREQUENCY ((float)(AUDIO_SAMPLE_RATE_EXACT * 0.249f))

float AudioFilterLadder::LPF(float s, int i)
{
    float ft = s * (1.0f/1.3f) + (0.3f/1.3f) * z0[i] - z1[i];
    ft = ft * alpha + z1[i];
    z1[i] = ft;
    z0[i] = s;
    return ft;
}

void AudioFilterLadder::resonance(float res)
{
    // maps resonance = 0->1 to K = 0 -> 4
    if (res > MAX_RESONANCE) {
        res = MAX_RESONANCE;
    } else if (res < 0.0f) {
        res = 0.0f;
    }
    K = 4.0f * res;
}

void AudioFilterLadder::frequency(float c)
{
    Fbase = c;
    compute_coeffs(c);
}

void AudioFilterLadder:: octaveControl(float octaves)
{
    if (octaves > 7.0f) {
        octaves = 7.0f;
    } else if (octaves < 0.0f) {
        octaves = 0.0f;
    }
    octaveScale = octaves / 32768.0f;
}

void AudioFilterLadder::compute_coeffs(float c)
{
    if (c > MAX_FREQUENCY) {
        c = MAX_FREQUENCY;
    } else if (c < 1.0f) {
        c = 1.0f;
    }
    float wc = c * (float)(2.0f * MOOG_PI / AUDIO_SAMPLE_RATE_EXACT);
    float wc2 = wc * wc;
    alpha = 0.9892f * wc - 0.4324f * wc2 + 0.1381f * wc * wc2 - 0.0202f * wc2 * wc2;
}

bool AudioFilterLadder::resonating()
{
    for (int i=0; i < 4; i++) {
        if (fabsf(z0[i]) > 0.0001f) return true;
        if (fabsf(z1[i]) > 0.0001f) return true;
    }
    return false;
}

static inline float fast_exp2f(float x)
{
    float i;
    float f = modff(x, &i);
    f *= 0.693147f / 256.0f;
    f += 1.0f;
    f *= f;
    f *= f;
    f *= f;
    f *= f;
    f *= f;
    f *= f;
    f *= f;
    f *= f;
    f = ldexpf(f, i);
    return f;
}

static inline float fast_tanh(float x)
{
    float x2 = x * x;
    return x * (27.0f + x2) / (27.0f + 9.0f * x2);
}

void AudioFilterLadder::update(void)
{
    audio_block_t *blocka, *blockb, *blockc;
    float Ktot;
    bool FCmodActive = true;
    bool QmodActive = true;

    blocka = receiveWritable(0);
    blockb = receiveReadOnly(1);
    blockc = receiveReadOnly(2);
    if (!blocka) {
        if (resonating()) {
            // When no data arrives but the filter is still
            // resonating, we must continue computing the filter
            // with zero input to sustain the resonance
            blocka = allocate();
        }
        if (!blocka) {
            if (blockb) release(blockb);
            if (blockc) release(blockc);
            return;
        }
        for (int i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
            blocka->data[i] = 0;
        }
    }
    if (!blockb) {
        FCmodActive = false;
    }
    if (!blockc) {
        QmodActive = false;
        Ktot = K;
    }
    for (int i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
        float input = blocka->data[i] * (1.0f/32768.0f);
        if (FCmodActive) {
            float FCmod = blockb->data[i] * octaveScale;
            float ftot = Fbase * fast_exp2f(FCmod);
            if (ftot > MAX_FREQUENCY) ftot = MAX_FREQUENCY;
            if (FCmod != 0) compute_coeffs(ftot);
        }
        if (QmodActive) {
            float Qmod = blockc->data[i] * (1.0f/32768.0f);
            Ktot = K + (MAX_RESONANCE * 4.0f) * Qmod;
                        if (Ktot > MAX_RESONANCE * 4.0f) Ktot = MAX_RESONANCE * 4.0f;
            if (Ktot < 0.0f) Ktot = 0.0f;
        }
        float u = input - (z1[3] - 0.5f * input) * Ktot;
        u = fast_tanh(u);
        float stage1 = LPF(u, 0);
        float stage2 = LPF(stage1, 1);
        float stage3 = LPF(stage2, 2);
        float stage4 = LPF(stage3, 3);
        blocka->data[i] = stage4 * 32767.0f;
    }
    transmit(blocka);
    release(blocka);
    if (blockb) release(blockb);
    if (blockc) release(blockc);
}
 
Last edited by a moderator:
I tried adding the HF resonance error compensation by the amount suggested by equation (3) in the paper:
Qadjust = 1.0029f + 0.0526f * wc - 0.0926 * wc2 + 0.0218* wc * wc2;
But it seems to do odd things to the pass band gain when it kicks in at higher frequencies. So at this stage it doesn't seem to help and I won't add it to the model.
It increasingly looks like the filter would indeed need to be oversampled to allow high FC/Q combinations.

But if you are happy to live with an upper limit of ~11kHz at self-resonance, pushing resonance to 1.2 certainly increases the level of the self oscillation, and doesn't actually sound too bad to me. Note also that when we add the passband_gain parameter, dialing that down will make the relative level of the self oscillation more prominent too. I look forward to hearing what others think.
 
Last edited:
Hi Richard

I had a quick try of your code from #85 - sounds really nice. Certainly the closest I've heard to that moog sound on a teensy - thanks for the work on this I've been looking for a decent sounding low pass filter for ages.

I'll try it later today with a few different sources and back to back with a moog lpf. I'm sure it won't be an exact match but I don't think that's the point - it's always going to be an approximation but first impression is that as is it's really usable and sounds good.

I've got old ears so the 11kHz limit works for me and in practical applications that works fine for my style of music. I'll have a play with max res as you suggest.

Cheers Paul
 
Thanks Paul, my ears aren't as broaodband as they used to be either, but 11kHz is just a bit too low for me. And in any case it's annoying to know the limit is there.

I had some time today and managed to get a preliminary over-sampled version up and running that I think may be a substantial improvement. I won't post it just yet because I know (the other) Paul is busy with a few other things at the moment, and it will give me a little time to double check things too. But it looks promising, and CPU usage is still only 3%..

I hope we can get the new version out as soon as Paul is ready. So in the meantime, I suggest that for anyone trying out the current version, keep in mind that the next version should have more resonance and less aliasing.
 
It re-instates the limit check on the total modulated 'K' value, which had gone missing.

Opps, sorry about that. I had mistakenly believed we were overriding the fixed resonance setting rather than modulating it.

I've merged this fix and updated the documentation.

https://github.com/PaulStoffregen/Audio/commit/f8c6dc2653e93a1a4eb7ff3a7ff1d0eb6319a3bf



I see we also have what looks like a leftover / incorrect check for zero in the frequency modulation.

Code:
                if (FCmodActive) {
                        float FCmod = blockb->data[i] * octaveScale;
                        float ftot = Fbase * fast_exp2f(FCmod);
                        if (ftot > MAX_FREQUENCY) ftot = MAX_FREQUENCY;
                        [B]if (FCmod != 0)[/B] compute_coeffs(ftot);
                }

Just thought I'd bring it up here before deleting this. We do always want to undate the coefficients, right? Now that we're using exp2 for "volt per octave" scaling, we shouldn't be able to get all the way down to exactly DC.
 
Note also that when we add the passband_gain parameter, dialing that down will make the relative level of the self oscillation more prominent too.

Would it make sense to keep this at 0.5 for resonance below 1.0, and maybe use a polynomial or other function to gradually / smoothly scale down the passband gain as the resonance setting is cranked up ever higher?

I see a number of commercial analog ladder filter have an optional "overdrive" switch, apparently to emulate a popular design mistake in one of the Moog synthesizers. Should we support this? Could it be done by just scaling the input before the tanh function? Or an alternate non-linear function for more distortion? I see the paper says:

The embedded nonlinearities within sections are replaced by a single nonlinearity, thus greatly reducing the computational cost of the filter. We have used the ‘tanh’ function for the nonlinearity, but any smoothly saturating function may be used.

Maybe we could get more of the "depth" or "dirty" feeling some people describe (and desire) of the Moog Sound by just scaling the tanh or using a stronger non-linearity?
 
For what it is worth soft saturation (distortion) is absolutely essential to Moog sound. https://www.youtube.com/watch?v=5sAq0FjRUI4
And using tanh() in simultation is not random choice. Moog ladder filter consist of bipolar transistor pairs forming variable transconductance amplifier stages. The use of tanh() has strong math background https://ieeexplore.ieee.org/document/654932

The code that rav posted above uses a fast tanh function. If you are using the real tanh function, you probably want to use tanhf instead if the calculations are done in float. The tanh function converts its argument to 64-bit double, and does all of the calculation in 64-bit and returns a 64-bit result. The tanhf function takes 32-bit float, does the calculation in 32-bit and returns a 32-bit result. I believe the 64-bit version may have a few extra calculations in the Taylor series to get more precision.

On a Teensy 4.0/4.1, the chip handles both float and double in hardware, but I imagine that in general float will be faster.

On a Teensy 3.5/3.6, the chip only handles float in hardware, and it has to do 64-bit via software emulation.

On a Teensy 3.2/LC, the chip has no floating point hardware, and it has to use software emulation for both float and double.
 
For obvious (performance) reasons nobody uses math library tanhf for audio. It is way too slow. And if you read previous posts, you would see that I earlier wrote to use float everywhere, not double as it was used in earlier versions of the code. Rational Pade approximation used by the code given in this thread comes from MusicDSP https://www.musicdsp.org/en/latest/Other/238-rational-tanh-approximation.html The disadvantage of this approximation is that it works for abs(x)< 4.5. Beyond that you need to clip output. There are better approximations (but slower) that use higher order rational, but it makes no sense to use them for filter.
Below you will find comparison of real tahn() with Pade approximation used in the code. You can easily see it goes wild above +/-4.5

Snap2.png


https://www.wolframalpha.com/input/?i=tanh(x);x+*+(+27+++x*x+)+/+(+27+++9+*+x*x+);

The code presented in this thread unfortunately does not clip output and this may cause problems for larger inputs.
A safer version of the code is here:

Code:
inline float fast_tanh_rational(float x)
{
   if( x < -3.0f )
        return -1.0f;
    else if( x > 3.0f )
        return 1.0f;
    
    register float x2 = x * x;

    return x * ( 27.0f + x2 ) / ( 27.0f + 9.0f * x2 );
}

Due to conditional jumps it is also a bit slower.
 
Last edited:
Out of curiosity, I ran a couple quick tests on Teensy 4.1. Using tanhf() instead of fast_tanh() adds about 1.5% CPU usage. That's more than the whole filter (sans modulation), but much less than I expected.

While running the Synth > LadderFilter example, the vast majority of input is between -1.5 to +1.1. But with some code to capture the worst case min & max, over several minute I saw it use -3.87 to +2.47.

Adding this seemingly very simple range check consumes a surprising amount of CPU time, almost 0.2%. I wasn't expecting it to have a noticeable effect on over CPU usage, but it definitely does.
 
Opps, sorry about that. I had mistakenly believed we were overriding the fixed resonance setting rather than modulating it.

I've merged this fix and updated the documentation.

https://github.com/PaulStoffregen/Audio/commit/f8c6dc2653e93a1a4eb7ff3a7ff1d0eb6319a3bf



I see we also have what looks like a leftover / incorrect check for zero in the frequency modulation.

Code:
                if (FCmodActive) {
                        float FCmod = blockb->data[i] * octaveScale;
                        float ftot = Fbase * fast_exp2f(FCmod);
                        if (ftot > MAX_FREQUENCY) ftot = MAX_FREQUENCY;
                        [B]if (FCmod != 0)[/B] compute_coeffs(ftot);
                }

Just thought I'd bring it up here before deleting this. We do always want to undate the coefficients, right? Now that we're using exp2 for "volt per octave" scaling, we shouldn't be able to get all the way down to exactly DC.

Yes, we always want to update now if FCmodActive is true.
 
Would it make sense to keep this at 0.5 for resonance below 1.0, and maybe use a polynomial or other function to gradually / smoothly scale down the passband gain as the resonance setting is cranked up ever higher?

I see a number of commercial analog ladder filter have an optional "overdrive" switch, apparently to emulate a popular design mistake in one of the Moog synthesizers. Should we support this? Could it be done by just scaling the input before the tanh function? Or an alternate non-linear function for more distortion? I see the paper says:



Maybe we could get more of the "depth" or "dirty" feeling some people describe (and desire) of the Moog Sound by just scaling the tanh or using a stronger non-linearity?

The passbandgain mainly boosts passband level at high resonance, less so at lower levels. It already acts to combat passband the level reduction you get at high Q compared to low Q. To sound more like the full model, which has 12dB passband reduction at q=1, I think it should be dialed back to 0 which makes the relative level of the self oscillation more prominent. It's a nice feature of this model that it offers this control.

As tomas already mentions, the tanh is already emulating the overdrive without adding more front end gain. I initially had an additional gain option, but didn't think it added much. I'll revisit it and double check, but it should not be at the expense of increased aliasing.
 
Out of curiosity, I ran a couple quick tests on Teensy 4.1. Using tanhf() instead of fast_tanh() adds about 1.5% CPU usage. That's more than the whole filter (sans modulation), but much less than I expected.

While running the Synth > LadderFilter example, the vast majority of input is between -1.5 to +1.1. But with some code to capture the worst case min & max, over several minute I saw it use -3.87 to +2.47.

Adding this seemingly very simple range check consumes a surprising amount of CPU time, almost 0.2%. I wasn't expecting it to have a noticeable effect on over CPU usage, but it definitely does.

I actually had the same checks that tomas shows (x > 3, x < -3) inside my fast_tanh at first, but removed them because they didn't show any advantage in terms of cpu use.
 
I had a quick exploration and have come to the conclusion that exposing both passband_gain and input_drive are definitely worthwhile after all. But they do interact somewhat in terms of the useful range before 'breaking up' the filter. A good compromise seems to be to allow passband_gain to span 0-0.5 (default maybe around 0.2) and input_drive 0-4 (default 1). In addition to the very fat and aggressive sounds you can get at high drive values, allowing it below 1 also lets you attenuate the excitation signal at self oscillation (and setting it to 0 can act as a way to shut the filter off).
 
Paul (Stoffregen), let me know when you're ready, and I'll post the new version that includes the two additional parameters and also allows the filter to be oversampled.

For now I'll set the oversampling factor to 2x by default, but it is backwards compatible in that it can be set to 1x. It can be set higher too, but it's diminishing returns with increased CPU. The largest benefit comes from going to 2x, which to my ears allows max_resonance to increase to 1.2 and max_frequency to AUDIO_SAMPLE_RATE_EXACT * 0.425f without increasing aliasing compared to the 1x case with its more restricted parameters. At 2x, CPU use on the Teensy 4.0 remains just at a very respectable 3 to 4%.
 
Back
Top