Porting moog ladder filters to audio objects?

I put the code on github and make one small fix and adapted the names to be similar to the other filters.

https://github.com/PaulStoffregen/Audio

If anyone wants to give this a try, please grab the latest from github. If you're not familiar with using git, the simplest way is to just download the ZIP file and extract it in {Documents}/Arduino/libraries. Just keep in mind anything to put there overrides all the other locations, so remember to delete this test copy when you later want to use the audio library the Teensyduino installer puts into your copy of Arduino.

Since there aren't any test programs yet, I wrote this very simple program which drives the filter with bursts of a sawtooth waveform.

Code:
#include <Audio.h>

AudioSynthWaveform       waveform1;
AudioFilterLadder        filter1;
AudioOutputMQS           mqs1;
AudioConnection          patchCord1(waveform1, 0, filter1, 0);
AudioConnection          patchCord2(filter1, 0, mqs1, 0);
AudioConnection          patchCord3(filter1, 0, mqs1, 1);

void setup() {
  AudioMemory(40);
  filter1.resonance(1.0);
  filter1.frequency(440);
  waveform1.begin(WAVEFORM_SAWTOOTH);
  waveform1.frequency(100);
}

void loop() {
  waveform1.amplitude(0.9);
  delay(500);
  waveform1.amplitude(0);
  delay(1500);
}

I'm not a synth guy, so I really don't know what this filter is supposed to sound like. But here's the output I see with my oscilloscope at the end of the sawtooth burst.

View attachment 23655

I'm not 100% confident it's really correct. As the test repeats every 2 seconds, the amount of 440 Hz superimposed on the sawtooth seems to vary quite a lot. Maybe later I'll try to capture a time-lapse video of my scope screen....

Really, I'm depending on everyone here with an ear for what the Moog sound is supposed to be to say whether they're happy with this, or if more work is needed before releasing it.

Also, still need a clear confirmation on the MIT license before this can be released.

Hi Paul. Why do you think it's possibly not correct? I ran your example and did a spectral analysis on the output, and the ringing is very nicely centered on 440Hz where you set the filter. I can code a quick demo demonstrating the Fc modulation if you like - it sounds quite nice to my ears with a three-saw complex.

Regarding your request to add the MIT license header, yes that's fine with me.

p.s. I ran your state variable with resonance at 5 (I think max?), but couldn't hear any ringing. Is that as you would expect?
 
Regarding your request to add the MIT license header, yes that's fine with me.

MIT headers added.

https://github.com/PaulStoffregen/Audio/commit/1de2cbf35162e912749b06c0a34f901c21c61a4b


Why do you think it's possibly not correct?

When I watch the scope screen, the 440 Hz changes on each update. Shouldn't it be very similar if not identical for each sawtooth burst? Maybe I need to make a video of the scope screen to show it?


I can code a quick demo demonstrating the Fc modulation if you like - it sounds quite nice to my ears with a three-saw complex.

That's be awesome.

I guess what really matters is the sound, so I'm going to trust your opinion and everyone on this thread who gives it a try and compares to their knowledge of the "Moog sound". If it sounds good, then I guess the trivial scope stuff is a non-issue.
 
Here's an example (for the audio shield and USB outputs) demonstrating the Fc modulation. It cycles between playing four pulses at minimum resonance (0), then about halfway, and then max (1.1). Note the use of bandlimited saw waves because the aliasing inherent in non-bandlimited waveforms may become more evident with high Q filters.
#include <Audio.h>
#include "filter_ladder.h"

AudioSynthWaveform waveform1;
AudioSynthWaveform waveform2;
AudioSynthWaveform waveform3;
AudioMixer4 mixer1;
AudioFilterLadder filter1;
AudioSynthWaveform lfo1;
AudioOutputI2S i2s1;
AudioOutputUSB usb1;

AudioConnection patchCord1(waveform1, 0, mixer1, 0);
AudioConnection patchCord2(waveform2, 0, mixer1, 1);
AudioConnection patchCord3(waveform3, 0, mixer1, 2);
AudioConnection patchCord4(mixer1, 0, filter1, 0);
AudioConnection patchCord5(lfo1, 0, filter1, 1);
AudioConnection patchCord6(filter1, 0, i2s1, 0);
AudioConnection patchCord7(filter1, 0, i2s1, 1);
AudioConnection patchCord8(filter1, 0, usb1, 0);
AudioConnection patchCord9(filter1, 0, usb1, 1);

void setup() {
AudioMemory(40);
filter1.resonance(1.1);
filter1.frequency(4000);
waveform1.frequency(50);
waveform2.frequency(100.1);
waveform3.frequency(150.3);
waveform1.begin(WAVEFORM_BANDLIMIT_SAWTOOTH);
waveform2.begin(WAVEFORM_BANDLIMIT_SAWTOOTH);
waveform3.begin(WAVEFORM_BANDLIMIT_SAWTOOTH);
lfo1.frequency(0.125);
lfo1.amplitude(0.975);
lfo1.phase(270);
}

void pulse(int onlength, int offlength)
{
waveform1.amplitude(0.3);
waveform2.amplitude(0.3);
waveform3.amplitude(0.3);
delay(onlength);
waveform1.amplitude(0);
waveform2.amplitude(0);
waveform3.amplitude(0);
delay(offlength);
}

void loop() {
int i;

filter1.resonance(0);
for(i=0;i<4;i++)
pulse(1000,1000);

filter1.resonance(0.5);
for(i=0;i<4;i++)
pulse(1000,1000);

filter1.resonance(1.1);
for(i=0;i<4;i++)
pulse(1000,1000);
}

Before trying this, please modify the ladder.cpp resonance function to allow res to go to 1.1 (rather than 1) which is a more sustained self resonance, but possibly may cause minor artifacts at high freqeuncy Fc.

void AudioFilterLadder::resonance(float res)
{
// maps resonance = 0->1 to K = 0 -> 4
if (res > 1.1) {
res = 1.1;
} else if (res < 0) {
res = 0;
}
K = 4.0 * res;
}
 
Here's version 1.02, which by popular demand now also allows resonance modulation :)

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

// https://forum.pjrc.com/threads/60488?p=269609&viewfull=1#post269609

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

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

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

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

void AudioFilterLadder::compute_coeffs(float c)
{
if (c > 0.49f * AUDIO_SAMPLE_RATE_EXACT)
c = 0.49f * AUDIO_SAMPLE_RATE_EXACT;
else if (c < 1)
c = 1;

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

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 ftot, FCmod, Qmod;
bool FCmodActive = true;
bool QmodActive = true;

blocka = receiveWritable(0);
blockb = receiveReadOnly(1);
blockc = receiveReadOnly(2);

if (!blocka) {
blocka = allocate();
if (!blocka) {
if (blockb) release(blockb);
if (blockc) release(blockc);
return;
}
for (int i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
blocka->data = 0;
}
}
if (!blockb) FCmodActive = false;
if (!blockc) QmodActive = false;

for (int i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
float input = blocka->data * (1.0f/32768.0f);
ftot = Fbase;
if (FCmodActive) {
FCmod = blockb->data * (1.0f/32768.0f);
ftot += Fbase * FCmod;
if (FCmod != 0) compute_coeffs(ftot);
}
if (QmodActive)
Qmod = blockc->data * (1.0/32768) ;
else
Qmod = 0;

double Ktot = K + 4.4 * Qmod;
if(Ktot < 0)
Ktot = 0;
if (Ktot > 4.4) // limit resonance to 1.1 for now ??
Ktot = 4.4;
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 = stage4 * 32767.0f;
}
transmit(blocka);
release(blocka);
if (blockb) release(blockb);
if (blockc) release(blockc);
}

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

// https://forum.pjrc.com/threads/60488?p=269609&viewfull=1#post269609

#ifndef filter_ladder_h_
#define filter_ladder_h_

#include "Arduino.h"
#include "AudioStream.h"

class AudioFilterLadder: public AudioStream
{
public:
AudioFilterLadder() : AudioStream(3, inputQueueArray) {};
void frequency(float FC);
void resonance(float reson);
virtual void update(void);
private:
float LPF(float s, int i);
void compute_coeffs(float fc);
float alpha = 1.0;
float beta[4] = {0.0, 0.0, 0.0, 0.0};
float z0[4] = {0.0, 0.0, 0.0, 0.0};
float z1[4] = {0.0, 0.0, 0.0, 0.0};
float K = 1.0;
float Fbase = 1000;
float Qbase = 0.5;

audio_block_t *inputQueueArray[3];
};

#endif
 
// Here's an example with a continuous 3-saw drone into the filter with separate lfos modulating Fc and resonance
// switching off the 150 hz waveform might make it easier to hear how th filter sounds at the very low end
// note that unlike the cutoff, I've made the resonance modulator in filter_ladder additive rather than multiplicative


#include <Audio.h>
#include "filter_ladder.h"

AudioSynthWaveform waveform1;
AudioSynthWaveform waveform2;
AudioSynthWaveform waveform3;
AudioMixer4 mixer1;
AudioFilterLadder filter1;
AudioSynthWaveform lfo1;
AudioSynthWaveform lfo2;
AudioOutputI2S i2s1;
AudioOutputUSB usb1;

AudioConnection patchCord1(waveform1, 0, mixer1, 0);
AudioConnection patchCord2(waveform2, 0, mixer1, 1);
AudioConnection patchCord3(waveform3, 0, mixer1, 2);
AudioConnection patchCord4(mixer1, 0, filter1, 0);
AudioConnection patchCord5(lfo1, 0, filter1, 1);
AudioConnection patchCord6(lfo2, 0, filter1, 2);
AudioConnection patchCord7(filter1, 0, i2s1, 0);
AudioConnection patchCord8(filter1, 0, i2s1, 1);
AudioConnection patchCord9(filter1, 0, usb1, 0);
AudioConnection patchCord10(filter1, 0, usb1, 1);

void setup() {

AudioMemory(40);

filter1.resonance(0.55);
filter1.frequency(4000);
waveform1.frequency(50);
waveform2.frequency(100.1);
waveform3.frequency(150.3);
waveform1.amplitude(0.3);
waveform2.amplitude(0.3);
waveform3.amplitude(0.3);
waveform1.begin(WAVEFORM_BANDLIMIT_SAWTOOTH);
waveform2.begin(WAVEFORM_BANDLIMIT_SAWTOOTH);
waveform3.begin(WAVEFORM_BANDLIMIT_SAWTOOTH);
lfo1.frequency(0.2);
lfo1.amplitude(0.985);
lfo1.phase(270);
lfo2.frequency(0.07);
lfo2.amplitude(0.55);
lfo2.phase(270);
}

void loop() {

}
 
Last edited:
On a slightly different note, I was looking at the ImprovedModel.h code (e.g. https://github.com/houtson/Teensy-4-Test-Area/blob/master/src/ImprovedModel.h) that says it's an implementation of D'Angelo's 2012 model. It has great resonance, but I can't seem to get the resonant frequency to go past about 2.5kHz. Maybe I've overlooked something, but I was wondering if anyone else has experienced this? Is it perhaps meant to be running in an over-sampled system?
 
Last edited:
Here's version 1.02, which by popular demand now also allows resonance modulation :)

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

As I wrote in my earlier post, I advise to keep attention to those little details such as using floating point constants:
4.0 is a DOUBLE (64-bit) constant. It causes your computations to be done in 64-bit, not 32-bit. And such computations are 2x slower.

To avoid that (and get best performance) you should always use f suffix on constants when you deal with float variables, like this:

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

In similar fashion this code:
Code:
inline double fast_tanh(double x)
{
double x2 = x * x;
return x * (27.0 + x2) / (27.0 + 9.0 * x2);
}

Would be faster if written like this:
Code:
inline float fast_tanh(float x)
{
float x2 = x * x;
return x * (27.0f + x2) / (27.0f + 9.0f * x2);
}

As Paul earlier noticed, replacing all unnecessary promotions to double precision has cut execution time by half (since 64-bit floating point is approx twice as slow as 32-bit)

For more info:
https://stackoverflow.com/questions/32266864/make-c-floating-point-literals-float-rather-than-double
 
I advise to keep attention to those little details such as using floating point constants:
....
you should always use f suffix on constants when you deal with float variables

Yeah, already done.

It did indeed make about a 2X speedup, going from about 2% CPU usage down to approx 1%, for the single input case (frequency & resonance configured by Arduino code, not modulated by other audio signals).

You should really grab the latest code from github. Much has happened in the last 24 hours!
 
I've added a first attempt at documentation. Here's a temporary copy of the design tool (the main one will be updated when this is non-beta released).

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

Did I get the details right? Is the description ok?

A low pass filter with resonant feedback, meant to approximate the classic "Moog sound". Both cut-off frequency and resonance level can optionally be controlled by input audio signals.
 
Paul, I downloaded the updated example sketch, and it seemed to be missing #include "filter_ladder.h".

Also, I adjusted some parameters to allow very high frequency cutoff modulation and was hearing quite a bit of aliasing. I'll look into this (and also check your documentation) as soon as I have a little time.

Cheers, Richard
 
it seemed to be missing #include "filter_ladder.h".

Audio.h now includes filter_ladder.h.


Also, I adjusted some parameters to allow very high frequency cutoff modulation and was hearing quite a bit of aliasing. I'll look into this (and also check your documentation) as soon as I have a little time.

My hope is to package up 1.47-beta7 within the next week. I'm going to turn my attention to a number of other non-audio things. Will check back here occasionally...
 
My suggestion is that we change the max resonance back to 1.0, which is what it was originally. At 1.1 it sustains the resonance longer, but it's at the expense of much more aliasing if you set Fc too high.

There's still a little aliasing going on even at a resonance of 1.0 at very high frequencies. I had thought that just setting the max cutoff to around 16.5kHz might solve it, but I think I will need to explore it a little more. I'll get back to you on that.
 
Last edited:
When I tested with the square wave bursts, even just slightly over 1.0 would sustain oscillation after the input went silent.
 
I would think ideally yes, but not all models succeed. And this is a computationally cheap model that is very stable, which not all are.
 
When I tested with the square wave bursts, even just slightly over 1.0 would sustain oscillation after the input went silent.

All versions of this filter are very non-linear, and the amount of resonace is frequency dependent. I agree that if you go a little over 1 with this model, it sustains noticeably better, which is why I first allowed it to go to 1.1. But as I said, it's at the expense of clearly more aliasing at (very) high cutoffs, which I personally dislike more than the reduced sustain in resonance.
 
The alternative is to limit the cutoff frequency to allow more resonance. If ~11kHz is acceptable for the highest cutoff the combination
#define MAX_RESONANCE ((float)1.07)
#define MAX_FREQUENCY ((float)(AUDIO_SAMPLE_RATE_EXACT * 0.249f))
sounds OK to me.

It might also be possible to set the max resonance in a frequency dependent way, but it would take some experimenting to figure out the right combinations.
 
Last edited:
On a somewhat related note, a further addition that might be worthwhile would be to change the line in the update routine that reads:

float u = input - (z1[3] - 0.5f * input) * Ktot;

to:

float u = input - (z1[3] - pbg * input) * Ktot;

where pbg is a new exposed parameter ranging form 0 to 1 that boosts the passband gain (set to 0.5 in the existing code).

Lowering it to 0 reduces the passband signal at high resonance, so it lets the 'resonant' part of the signal dominate the output more.
 
Ok, I've put those limits into the github code.

https://github.com/PaulStoffregen/Audio/commit/565608d2eff9617c0911d4e0b00db299b328cb09

Personally, I don't see the harm in allowing people to push the resonance a little too high. You can always just not use the higher numbers if controlling from Arduino code, or slightly attenuate your control signal if modulating at full audio bandwidth.

If the results aren't horrible, maybe this is the sort of thing that would be better done by guidance in the documentation, rather than hard limiting in the software? I'd rather give people something that's "flexible" and "fun" rather than perfectly safe. If they *really* want more self oscillation, why not let them have it? Well, of course with a warning in the documentation that exceeding 1.0 with higher cutoff frequency means risking some artifacts.
 
Understood. With the cutoff limited to 11kHz, a resonance of 1.1 doesn't sound terrible to me, just not quite as good as 1.07. But I don't like the results much with higher cutoffs. The full model does better in this regard, but it also requires a much high CPU load (and is oversampled).

If you don't think the passband gain parameter is worth adding, an alternative would be to just lower the value of 0.5f in the update routine's line:

float u = input - (z1[3] - 0.5f * input) * Ktot;

to 0.25, or maybe even to 0 if it's to behave more like the full model in which the passband is down 12dB at self oscillation.
 
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?
 
Back
Top