Porting moog ladder filters to audio objects?

Hello all, I'm super excited to use the filter. Flo mentioned it's in the "official" audio library now, but I don't see it in the GUI tool. Any tips on where to find it?
 
Hi Florian, good to hear you're enjoying the filter. On my system, I get 7% for POLY_FIR, and 5% for LINEAR. To reduce it further I suggest going to 2x oversampling. However, that requires a different set of FIR coefficients. The ones from the newer filter that already runs at 2x should be fine, but they are 32pt rather than 36 so the changes to the .h file include changing both INTERPOLATION from 4 to 2, and interpolation_taps form 36 to 32.

Hi Richard,
thank you, I changed the coefficients and corrected the bug you found in the poly update routine and now it works great with relatively low CPU usage (also 4% for 2x oversampling and linear interpolation on my system).

I will also have a look at the full model, but unfortunately, I need to economise on my CPU ressources.
 
Last edited:
Hello all, I'm super excited to use the filter. Flo mentioned it's in the "official" audio library now, but I don't see it in the GUI tool. Any tips on where to find it?

Hello TigerBalm,

weird, I noticed too that somehow, there are different versions of the gui tool. (with different indexes? :confused:)
This here -> https://www.pjrc.com/teensy/gui/ does not include the filter, whereas
this here -> https://www.pjrc.com/teensy/gui/index2.html?info=AudioFilterLadder
has it in.

Greetings, flo
 
Here is an update of the new full-model filter_ladder2, which corrects a few minor issues, and adds linear interpolation as an option in the same way as the existing ladder filter.

filter_ladder2.cpp
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.
 */

//-----------------------------------------------------------
// AudioFilterLadder2 for Teensy Audio Library
// Based on Huovilainen's full model, presented at DAFX 2004
// Richard van Hoesel, March. 11, 2021
// v.1.02 includes linear interpolation option
// v.1.01 Adds a "resonanceLoudness" parameter (1.0 - 10.0)
//        to control resonance loudness 
// v.1.0 Uses 2x oversampling. Host resonance range is 0-1.0. 
//
// please retain this header if you use this code.
//-----------------------------------------------------------

#include <Arduino.h>
#include "filter_ladder2.h"
#include "arm_math.h"
#include <math.h>
#include <stdint.h>

#define MAX_RESONANCE ((float)1.015)
#define MAX_FREQUENCY ((float)(AUDIO_SAMPLE_RATE_EXACT * 0.425f))
#define fI_NUM_SAMPLES  AUDIO_BLOCK_SAMPLES * INTERPOLATION

//=== 2 xOS, 32taps (88.2kHz)
static float AudioFilterLadder2 ::finterpolation_coeffs[AudioFilterLadder2::finterpolation_taps] = {    
-129.6800658538650740E-6, 0.001562843504973615, 0.004914813078250063, 0.007186231102125209, 0.002728024844356039,-0.007973931496234599,-0.013255882861166815,-841.1646993462468340E-6,
 0.022309967341058432, 0.027240071522569135,-0.007073857882406733,-0.056523975383091875,-0.055929860712812876, 0.042278471323346570, 0.207056768528768836, 0.335633514003638445,
 0.335633514003638445, 0.207056768528768836, 0.042278471323346570,-0.055929860712812876,-0.056523975383091875,-0.007073857882406733, 0.027240071522569135, 0.022309967341058432,
-841.1646993462468340E-6,-0.013255882861166815,-0.007973931496234599, 0.002728024844356039, 0.007186231102125209, 0.004914813078250063, 0.001562843504973615,-129.6800658538650740E-6
};

void AudioFilterLadder2::initpoly()
{
  if (arm_fir_interpolate_init_f32(&interpolation, INTERPOLATION, finterpolation_taps,
     finterpolation_coeffs, finterpolation_state, AUDIO_BLOCK_SAMPLES)) {
    polyCapable = false;
    return;
  }
  if (arm_fir_decimate_init_f32(&decimation, finterpolation_taps, INTERPOLATION,
     finterpolation_coeffs, fdecimation_state, fI_NUM_SAMPLES)) {
    polyCapable = false;
    return;
  }
  // TODO: should we fill interpolation_state & decimation_state with zeros?
  polyCapable = true;
  polyOn = true;
}

void AudioFilterLadder2::interpolationMethod(AudioFilterLadderInterpolation imethod)
{
  if (imethod == LADDER_FILTER_INTERPOLATION_FIR_POLY && polyCapable == true) {
    // TODO: if polyOn == false, clear interpolation_state & decimation_state ??
    polyOn = true;
  } else {
    polyOn = false;
  }
}

void AudioFilterLadder2::resonanceLoudness(float v)  // input 1-10
{
  if(v<1) v=1;
  if(v>10) v=10;
  VTx2 =  v;   
  inv2VT = 1.0/VTx2;
}

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

void AudioFilterLadder2::inputDrive(float drive) 
{
  if(drive<1)
      overdrive = 1.0f;
  else 
  if(drive>3)
     overdrive =  3.0f;  
  else
    overdrive = drive;   
}

void AudioFilterLadder2::portamento(float pc)
{
  FcPorta = pc; 
}

void AudioFilterLadder2::compute_coeffs(float freq, float res) // tuned for 2x os
{
    float fc, freq2, freq3;          
    if (res > MAX_RESONANCE) res = MAX_RESONANCE ;
    if (res < 0.0f) res = 0.0f;    if (freq < 5) freq = 5;
    if (freq > MAX_FREQUENCY) freq = MAX_FREQUENCY;       
    fc = freq *  FC_SCALER;                             
    freq2 = freq*freq;
    freq3 = freq2 * freq;      
    float qa = 0.998f + 3.5e-5f * freq - 8e-10f * freq2 - 4e-14f * freq3; 
    K = 4.0f * res * qa;       
    float fa = 1.0036f - 2e-5f *freq + 1e-9f*freq2   - 1.75e-14f * freq3;  
    Gx2Vt = float((1.0f - exp(-fc * fa)) * VTx2);                                      
}

void AudioFilterLadder2::frequency(float c)
{
    if (c < 1) c = 1;
    if (c > MAX_FREQUENCY) c = MAX_FREQUENCY;  
    targetFbase = c; 
    //Fbase = c;   
}

void AudioFilterLadder2::resonance(float res)
{  
    res *= MAX_RESONANCE;                                 // normalize so host specifies 0-1 rather than 0-1.015
    if (res > MAX_RESONANCE) res = MAX_RESONANCE ;
    if (res < 0.0f) res = 0.0f;
    Qbase = res;
}

void AudioFilterLadder2::octaveControl(float octaves)
{
  if (octaves > 10.0f) {             // increased range compared to previous filters to span full audio range with 0-1 envelope
    octaves = 10.0f;
  } else if (octaves < 0.0f) {
    octaves = 0.0f;
  }
  octaveScale = octaves / 32768.0f;
}

static inline float fast_tanh(float x)
{
  if(x>3) return 1;
  if(x<-3) return -1;
  float x2 = x * x;
  return x * (27.0f + x2) / (27.0f + 9.0f * x2);
}

bool AudioFilterLadder2::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;
}

void AudioFilterLadder2::update(void)
{
  audio_block_t *blocka, *blockb, *blockc;
  float ftot, qtot;
  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;
        ftot = Fbase;
      }
      if (!blockc) {
        QmodActive = false;
        qtot = Qbase;
      }
      float blockOut[AUDIO_BLOCK_SAMPLES]; 
      
      if (polyOn == true) {
        float blockIn[AUDIO_BLOCK_SAMPLES];   
        float blockOS[fI_NUM_SAMPLES], blockOutOS[fI_NUM_SAMPLES];                
        for (int i=0; i < AUDIO_BLOCK_SAMPLES; i++)
          blockIn[i] = blocka->data[i]  * overdrive *  float(INTERPOLATION /32768.0f);
        arm_fir_interpolate_f32(&interpolation, blockIn, blockOS, AUDIO_BLOCK_SAMPLES);             
        for (int i=0; i < AUDIO_BLOCK_SAMPLES; i++) {                   
          Fbase = FcPorta * Fbase + (1-FcPorta) * targetFbase; 
          ftot = Fbase;            
          if (FCmodActive) {
            float FCmod = blockb->data[i] * octaveScale;
            ftot = Fbase * fast_exp2f(FCmod);                    
          }
          if (QmodActive) {
            float Qmod = blockc->data[i] * (1.0f/32768.0f);
            qtot = Qbase + Qmod;
          }            
          compute_coeffs(ftot,qtot) ;                          
          for(int os = 0; os < INTERPOLATION; os++)  
          {      
            float input = blockOS[i * INTERPOLATION + os];                            
            filter_y1 = filter_y1 + Gx2Vt * (fast_tanh((input - K * filter_out) * inv2VT) - save_tan1);
            save_tan1=  fast_tanh(filter_y1 * inv2VT);                  
            filter_y2 = filter_y2 + Gx2Vt * (save_tan1 - save_tan2);
            save_tan2 = fast_tanh(filter_y2 * inv2VT);                  
            filter_y3 = filter_y3 + Gx2Vt * (save_tan2 - save_tan3);  
            save_tan3 = fast_tanh(filter_y3 * inv2VT);                   
            filter_y4 = filter_y4 + Gx2Vt * (save_tan3 - fast_tanh(filter_y4 * inv2VT));                             
            filter_out = (filter_y4 + filter_y5) * 0.5f;
            filter_y5 = filter_y4;   
            blockOutOS[i*INTERPOLATION + os] = filter_out;
          }      
        }
        arm_fir_decimate_f32(&decimation, blockOutOS, blockOut, fI_NUM_SAMPLES);  
      }     
      else {         
        for (int i=0; i < AUDIO_BLOCK_SAMPLES; i++) {                  
          Fbase = FcPorta * Fbase + (1-FcPorta) * targetFbase;   
          if (FCmodActive) {
            float FCmod = blockb->data[i] * octaveScale;
            ftot = Fbase * fast_exp2f(FCmod);                    
          }
          if (QmodActive) {
            float Qmod = blockc->data[i] * (1.0f/32768.0f);
            qtot = Qbase + Qmod;
          }            
          compute_coeffs(ftot,qtot) ;                        
          float total = 0.0f;
          float interp = 0.0f;
          float input = blocka->data[i] * overdrive * (1.0f/32768.0f);
          for (int os = 0; os < INTERPOLATION; os++) {
            float inputOS = (interp * oldinput + (1.0f - interp) * input); 
            filter_y1 = filter_y1 + Gx2Vt * (fast_tanh((inputOS - K * filter_out) * inv2VT) - save_tan1);
            save_tan1=  fast_tanh(filter_y1 * inv2VT);                  
            filter_y2 = filter_y2 + Gx2Vt * (save_tan1 - save_tan2);
            save_tan2 = fast_tanh(filter_y2 * inv2VT);                  
            filter_y3 = filter_y3 + Gx2Vt * (save_tan2 - save_tan3);  
            save_tan3 = fast_tanh(filter_y3 * inv2VT);                   
            filter_y4 = filter_y4 + Gx2Vt * (save_tan3 - fast_tanh(filter_y4 * inv2VT));                             
            filter_out = (filter_y4 + filter_y5) * 0.5f;
            filter_y5 = filter_y4;   
            total += filter_out * (1.0f / (float)INTERPOLATION);
            interp += (1.0f / (float)INTERPOLATION);            
          } 
          blockOut[i] = total;  
          oldinput = input;
        }      
      }                 
      float absblk;
      for (int i=0; i < AUDIO_BLOCK_SAMPLES; i++) {  
        absblk = 1.25f * blockOut[i];
        if(absblk<0) absblk=-absblk;        
        if(absblk > 1)
           if(absblk > peak)
              peak = absblk;
      }     
      for (int i=0; i < AUDIO_BLOCK_SAMPLES; i++) {         
        if(peak>1){ 
          pgain = 0.99f * pgain + 0.01f/peak;    // fast attack    
          peak *=0.99995f;
        }   
        blocka->data[i] = blockOut[i]  * pgain * float(0.85f * float(32767.0));  
      } 
      transmit(blocka);
      release(blocka);
      if (blockb) release(blockb);
      if (blockc) release(blockc);
}

filter_ladder2.h
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.
 */

//-----------------------------------------------------------
// AudioFilterLadder2 for Teensy Audio Library
// Based on Huovilainen's full model, presented at DAFX 2004
// Richard van Hoesel, March. 11, 2021
// v.1.02 includes linear interpolation option
// v.1.01 Adds a "resonanceLoudness" parameter (1.0 - 10.0)
//        to control resonance loudness 
// v.1.0 Uses 2x oversampling. Host resonance range is 0-1.0. 
//
// please retain this header if you use this code.
//-----------------------------------------------------------

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

#ifndef filter_ladder2_h_
#define filter_ladder2_h_

#include "Arduino.h"
#include "AudioStream.h"
#include "arm_math.h"
#include <Audio.h>
#define MOOG_PI ((float)3.14159265358979323846264338327950288)  

class AudioFilterLadder2: public AudioStream
{
public:
  AudioFilterLadder2() : AudioStream(3, inputQueueArray) { initpoly(); };
  void frequency(float FC);
  void resonance(float reson);
  void octaveControl(float octaves);
  void inputDrive(float drive);
  void portamento(float pcf);   
  void compute_coeffs(float freq, float res);  
  void initpoly(); 
  void interpolationMethod(AudioFilterLadderInterpolation im);
  void resonanceLoudness(float v);  
  virtual void update(void);
  
private:
  static const int INTERPOLATION = 2;
  static const int finterpolation_taps = 32;
  static const float FC_SCALER = 2.0f * MOOG_PI / (INTERPOLATION * AUDIO_SAMPLE_RATE_EXACT);
  float finterpolation_state[(AUDIO_BLOCK_SAMPLES-1) + finterpolation_taps / INTERPOLATION];
  arm_fir_interpolate_instance_f32 interpolation;
  float fdecimation_state[(AUDIO_BLOCK_SAMPLES*INTERPOLATION-1) + finterpolation_taps];
  arm_fir_decimate_instance_f32 decimation;   
  static float finterpolation_coeffs[finterpolation_taps];
  
  float Gx2Vt, K, filter_res, filter_y1, filter_y2, filter_y3, filter_y4, filter_y5, filter_out; 
  float VTx2 = 5; 
  float inv2VT = 1.0/VTx2;
  float Fbase=1000, Qbase=0.5;
  float overdrive=1;
  float ftot=Fbase, qtot=Qbase;
  bool  resonating();
  bool  polyCapable = false;
  bool  polyOn = false;    // FIR is default after initpoly()
  float octaveScale = 1.0f/32768.0f;
  float z0[4] = {0.0, 0.0, 0.0, 0.0};
  float z1[4] = {0.0, 0.0, 0.0, 0.0};
  float pbg=0;
  float save_tan1=0;
  float save_tan2=0;
  float save_tan4=0;
  float save_tan3=0;
  float peak=1.0;
  float pgain=1.0;
  float targetFbase = 1000;
  float FcPorta = 0;   
  float oldinput=0;  
  audio_block_t *inputQueueArray[3];
};

#endif
 
I want to experiment with the Audio Library version of this filter. As suggested by Paul in message #50 of this thread, I downloaded a zip version of https://github.com/PaulStoffregen/Audio and unzipped it to {Documents}/Arduino/libraries but it wasn't successful. I tried to run the example in message #50 and it didn't recognize the new filter class.

Is there a better way to get the ladder filter into my Arduino 1.8.13 environment?
 
I'd put it in your sketchbook/libraries - check the compiler versbose output as this should confirm which libraries are
being used.
 
The sketches on my windows 10 machine are located in {Documents}/Arduino/libraries. That is where I put it. The compiler said it found two Audio.h files and it automatically chose the original Audio.h.

I did finally got it to work by temporarily changing the name of the ladder filter Audio.h to temp_Audio,h and used #include temp_Audio.h in the sketch. I thought that if I put it in {Documents}/Arduino/libraries it was supposed to take precedence, but I guess that was an incorrect assumption.
 
I am running my project at 96000 Hz. Before I proceed I want to check an assumption I am making.

If, for example, I run the four stage ladder filter AudioFilterLadder::update() at a 88000 Hz rate and if I modify the ladder filter code to eliminate any interpolation, is that equivalent to running the ladder filter at 44000 Hz with INTERPOLATION = 2?
 
Hello good people,

I'm from the Axoloti world and just received my Teensy 4.0 board with the Audio Shield. Having fun testing out the capabilities and I'm impressed so far. I'd like to "port" a project of mine that's a Moog Taurus Bass Pedal emulator. I started this project as a Pure-Data model a few years back and I did use a model of Antti Huovilainen's Moog filter. It is excellent! Now, imagine my excitement when I discovered your thread!

Now, I've played with the "Ladder" filter that's included in the Audio library but I can't seem to figure out how can I use the updated (full model) version of this filter. Maybe I'm using the wrong dev tools? I'm using the basic Arduino/Teensyduino IDE. Thanks in advance guys and sorry for reviving an old thread.

Marc aka Khorus

PS: For the curious mind, here's a video of my current Taurus emulator that runs on Axoloti Core. https://www.facebook.com/727063099/videos/250096457328671/
 
My full model version of the filter isn't available through the standard audio library. You will have to copy the cpp and h files from my post #204 above and include them manually, either as stand alone files in your project directory (probably the easiest solution), or else incorporate them into your local version of the audio library.

Cheers,
Richard
 
Richard, I'm using your filter and it sounds amazing. It growls gracefully, my respect!

Have you ever looked into the following paper, written by
Stefan D'Angelo (Arturia's VA plugins & Neural DSP) and the legendary Vesa Välimäki?

An improved virtual analog model of the Moog ladder filter
Abstract:
In this paper we derive a novel circuit-based model for the Moog filter and discretize it using the bilinear transform. The proposed nonlinear digital filter compares favorably against Huovilainen's model, which is the best previous white-box model for the Moog filter. The harmonic distortion characteristics of the proposed model match closely with those of a SPICE simulation. Furthermore, the novel model realistically enters the self-oscillation mode and maintains it. The proposed model requires only 12 more basic operations per output sample than Huovilainen's model, but includes the same number of nonlinear functions, which dominate the computational load. The novel nonlinear digital filter is applicable in virtual analog music synthesis and in musical audio effects processing.
 
Richard, I'm using your filter and it sounds amazing. It growls gracefully, my respect!

Have you ever looked into the following paper, written by
Stefan D'Angelo (Arturia's VA plugins & Neural DSP) and the legendary Vesa Välimäki?

An improved virtual analog model of the Moog ladder filter
Abstract:

Yes, I've looked at Stefan's papers in the past. I was hoping to get around to trying out some of his ideas but haven't yet, and they're not trivial to implement if I remember correctly.
 
Back
Top