Porting moog ladder filters to audio objects?

Some of those filters are very compute intensive, requiring that tanh be called four or more times per sample. However, the MusicDSPModel filter seems to be easy on the cpu so I've been trying to get it to work on a Teensy4 with audio board - but no luck yet.
The input samples to each of the filters in MoogLadders is an array of floats. I generate a sine or square wave using AudioSynthWaveform, copy the int16_t samples from an input queue to a float array, process that array with the filter and then copy the float array back to an int16_t array and pass that to the output queue.
Playing the input to the output with no processing passes the synthesized waveform through correctly. Can't figure out what the Process function needs to get it to output some audio.
Here's the complete sketch.
View attachment MusicDSP.zip

Pete
 
Got it to generate audio. The samples that are input to the Process function must be normalized as a fraction of full scale.
I've added another of the filters, RKSimulationModel, but not played with it much.
This code uses a 55Hz square wave.
View attachment MusicDSP_1.zip

Pete
 
Got it to generate audio. The samples that are input to the Process function must be normalized as a fraction of full scale.
I've added another of the filters, RKSimulationModel, but not played with it much.
This code uses a 55Hz square wave.
View attachment 19748

Pete

I've had a chance to test it out. To evaluate i quickly assigned midi controllers to the cutoff and resonance value. Realtime resonance adjustment via midi works (and sounds good), but when i attempt to change the cutoff it kills the audio (requiring reboot).
If i change the value in void setup(), it adjusts the cutoff correctly, it's only when trying to adjust it in realtime.

All the same, very promising so far! i'm sure it's something small.

Code:
// See: https://forum.pjrc.com/threads/60488
// Try to implement the MusicDSPModel filter
// on Teensy 4.0 using the Play and Record
// queues

// The samples must be normalized to a fraction
// of full-scale for input to the Process function
// and then multiplied by full-scale afterwards.

// If this is defined, the sine wave will
// be played unmodified to the output.
// The only difference is that the Process
// function isn't called.
// This works which proves that the synthesized
// audio is being passed through the queues
// to the output.
//#define STRAIGHT_THROUGH

#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <MIDI.h>
#include <midi_UsbTransport.h>

float midicc_Array[128]; // scaled 0-1 values for midi 7-bit

// amplitude of signal
#define WAVE_AMP 32000

#define MDSPM

#ifdef MDSPM
#include "MusicDSPModel.h"
MusicDSPMoog mDSPm(44100);
#else
#include "RKSimulationModel.h"
RKSimulationMoog mDSPm(44100);
#endif

volatile float vcf_cutoff;
boolean midi_flag = HIGH;

// GUItool: begin automatically generated code
AudioSynthWaveform       waveform1;      //xy=410,296
AudioRecordQueue         queue1;         //xy=558,297
AudioPlayQueue           queue2;         //xy=748,298
AudioOutputUSB           usb1;           //xy=909,404
AudioOutputI2S           i2s1;           //xy=973,300
AudioConnection          patchCord1(waveform1, queue1);
AudioConnection          patchCord2(queue2, 0, i2s1, 0);
AudioConnection          patchCord3(queue2, 0, i2s1, 1);
AudioConnection          patchCord4(queue2, 0, usb1, 0);
AudioConnection          patchCord5(queue2, 0, usb1, 1);
AudioControlSGTL5000     sgtl5000;       //xy=418,383
// GUItool: end automatically generated code



uint32_t now = 0;
void setup(void)
{
  Serial.begin(9600);
  while(!Serial && (millis() - now) < 5000);
  // Audio connections require memory. and the record queue
  // uses this memory to buffer incoming audio.
  AudioMemory(10);

  // Enable the audio shield. select input. and enable output
  sgtl5000.enable();
  sgtl5000.volume(0.4);
//                amp   Frq  waveform
  waveform1.begin(WAVE_AMP,120,WAVEFORM_SAWTOOTH);

  // Default is 0.1
  mDSPm.SetResonance(0.2);
  // Default is 1000
  mDSPm.SetCutoff(1000);

  for (byte i = 0; i < 128; i++) { //lets create an array to deal with converting 7-bit midi (0-127) to 0-1 res
    midicc_Array [i] = 0.00787402 * i;
  }


  usbMIDI.setHandleControlChange(OnCC); // set handle for MIDI continuous controller messages

  // Start the record queue
  queue1.begin();
}

float samplebuf[AUDIO_BLOCK_SAMPLES];
void loop(void)
{
 
  short *bp;
  // When an input buffer becomes available
  // process it
  if (queue1.available() >= 1) {
    bp = queue1.readBuffer();
    // Copy the int16 samples into floats
    for(int i = 0;i < AUDIO_BLOCK_SAMPLES;i++) {
      samplebuf[i] = *bp++/(float)WAVE_AMP;
    }
    // Free the input audio buffers
    queue1.freeBuffer();
    usbMIDI.read();

#ifndef STRAIGHT_THROUGH
    // This processes and returns floating point samples
    mDSPm.Process(samplebuf,AUDIO_BLOCK_SAMPLES);
#endif
    bp = queue2.getBuffer();

    // copy the processed data back to the output
    for(int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) {
      *bp++ = samplebuf[i]*(float)WAVE_AMP;
    }

    // and play it back into the audio queues
    queue2.playBuffer();
  }
  
  
  // volume control
  static int volume = 0;
  // Volume control
  int n = analogRead(15);
  if (n != volume) {
    volume = n;
    sgtl5000.volume(n / 1023.);
  }
  
}

//handle all incoming midi messages
void OnCC(byte channel, byte controller, byte value) {
  switch (controller) {
    case 0:
      //do nothing
      break;
    case 1:
      mDSPm.SetResonance(midicc_Array [value]);
      break;
    case 2:
    int scaledVcf = midicc_Array [value] * 10000;
    mDSPm.SetCutoff(scaledVcf);
      break;
  }

}
//////////////////////////////////////////////////////////////////////////////////////
'
 
If i comment out
Code:
SetResonance(resonance);
in MusicDSPModel.h, i can control the cutoff, but i feel that might be there for a reason.

I'm also hearing little "clicks" in the audio, which doesn't seem to change when giving audio library more memory or increasing the buffer in the asio driver.

Code:
	virtual void SetCutoff(float c) override
	{
		cutoff = 2.0 * c / sampleRate;

		p = cutoff * (1.8 - 0.8 * cutoff);
		k = 2.0 * sin(cutoff * MOOG_PI * 0.5) - 1.0;
		t1 = (1.0 - p) * 1.386249;
		t2 = 12.0 + t1 * t1;

		//SetResonance(resonance); //works when commenting this out
	}
 
Got it to generate audio. The samples that are input to the Process function must be normalized as a fraction of full scale.
I've added another of the filters, RKSimulationModel, but not played with it much.
This code uses a 55Hz square wave.
View attachment 19748

Pete

Is there a trick to getting RKSimulationModel to work. Simply commenting out
HTML:
//#define MDSPM
leaves me with no audio? I want to hear if these "clicks" are in other filter models.
 
When I just comment the define of MDSPM (in MusicDSP_1) there is a low-volume low-frequency hum. Playing with other values of resonance and cutoff does make the output from RKS completely disappear.

The reason that SetCutoff calls SetResonance is presumably because SetCutoff calculates new values for t1 and t2 which are used in the calculation of resonance but don't know why that should kill the filter.

I'm going to play around with adding more of the filters.

Pete
P.S. I've also added white noise as an optional (compile time) source for the filters
 
The hum might be a sign of the cutoff being too low. The cutoff value is in Hz so 1000 = 4-pole filter with a 24db rolloff beginning at 1000hz. Better to set the cutoff higher for better clarity initially;

Code:
// Default is 0 (no resonance)
  mDSPm.SetResonance(0.0);
  // Default is 10,000hz
  mDSPm.SetCutoff(10000);

I've been using the sdrawplay to just play a clap sample through it, i don't hear the clicks i heard when using the waveform. I've been using midiCC messages to controll the cutoff and resonance while play the sample, there aren't actually any issues until i comment back in the resonance function inside the cutoff function.

If you have a way of varying the cutoff value in realtime, i'd be curious if the audio cuts out on your end when adjusting the cutoff parameter.

Good luck on the the other filters. I'd be keen to hear other filter models after playing around with the mDSPm example.

When I just comment the define of MDSPM (in MusicDSP_1) there is a low-volume low-frequency hum. Playing with other values of resonance and cutoff does make the output from RKS completely disappear.

The reason that SetCutoff calls SetResonance is presumably because SetCutoff calculates new values for t1 and t2 which are used in the calculation of resonance but don't know why that should kill the filter.

I'm going to play around with adding more of the filters.

Pete
P.S. I've also added white noise as an optional (compile time) source for the filters
 
The reason that SetCutoff calls SetResonance is presumably because SetCutoff calculates new values for t1 and t2 which are used in the calculation of resonance but don't know why that should kill the filter.

Ah I think i figured out why this wasn't working, funny its obvious now.

Code:
virtual void SetResonance(float r) override
	{
		resonance = r * (t2 + 6.0 * t1) / (t2 - 6.0 * t1);
	}
	
	virtual void SetCutoff(float c) override
	{
		cutoff = 2.0 * c / sampleRate;

		p = cutoff * (1.8 - 0.8 * cutoff);
		k = 2.0 * sin(cutoff * MOOG_PI * 0.5) - 1.0;
		t1 = (1.0 - p) * 1.386249;
		t2 = 12.0 + t1 * t1;

		SetResonance(resonance);
	}

The problem is when they call setResonance(resonance) at the end of the cutoff function, the value is bunk because the resonance value needs to be recalculated using the new t1 & t2 variables. Solution is to store (float r) as a local variable (i.e float rValue), and change setResonance(resonance); to setResonance(rValue);

* see attached

I should also mention this did have a noticable effect on workings of the filter, it sounds more stable now.
 

Attachments

  • MusicDSPModel.h
    1.9 KB · Views: 112
Last edited:
last comment as its 3am here. I was able to swap in the Stilsonmodel.h vs RKS and the former works fine, so i think there is an issue with the code provided for the RKS model.


When I just comment the define of MDSPM (in MusicDSP_1) there is a low-volume low-frequency hum. Playing with other values of resonance and cutoff does make the output from RKS completely disappear.

The reason that SetCutoff calls SetResonance is presumably because SetCutoff calculates new values for t1 and t2 which are used in the calculation of resonance but don't know why that should kill the filter.

I'm going to play around with adding more of the filters.

Pete
P.S. I've also added white noise as an optional (compile time) source for the filters
 
#define WAVE_AMP 32000

should this instead be (see below)? I didn't understand at first, but now i understand the ampl scaling. If full scale in teensy library is +/- 32767 (signed integer), we'd want to divide by this amount to get +/-1.0 in the moog models, right?

#define WAVE_AMP 32767

Also i see it's easy enough to swap out the models now so i tried the stilson model, which sounds good also, but I needed to set a lower limit of 100 for the cutoff value, or the sound would dissapear and not come back until a reboot.

I've attached the modified header.
 

Attachments

  • StilsonModel.h
    5.1 KB · Views: 103
I have since modified my code to define a WAVE_MAX 32767 and then WAVE_AMP of around 32000 and used those in appropriate places.
I've got several of the models in the sketch and then pick one using a #define.
I've also added another one that I found here.
But I haven't been able to get any of them to make what I would consider to be an impressive Moog-like sound - but then, I don't really know what I'm doing!.
This the latest iteration of the sketch.
View attachment MusicDSP_3.zip

Pete
 
Thanks for these, I'd love a good Moog like LP filter and I had a play about with the sketch.

I added a pot for cut-off freq and res and a couple of extra waveforms to thicken the sound for auditioning.

I noticed a few of the models have different ranges of res so added that to the #ifdefs.

I think the RKSimulationModel is best but agree none of them knock it out of the park.

I added the Oberheim model, doesn't sound much like a Moog but does sound nice (probably my favourite of them all).

I put the updated files here, cheers Paul
 
Thanks for that Paul. I need to get around to adding two pots so that varying the resonance and cutoff are easier.

Pete
 
Would be great,...if..

I imagine this wouldn't be too hard, but my understanding of audio library isn't strong enough yet. Is anyone interested in giving it a try? Most of these have very friendly licenses.

https://github.com/ddiakopoulos/MoogLadders

The Moog filter would be a fantastic addition to the library. I have wished that was there ever since I started messing with Teensys. I even think the option would be worth a bit of greed for resources. The only thing that has ever made me question the idea is that the best and most representative version of it was the MiniMoog ladder. This however was the result of a mathematical mistake that wasn't noticed until the 500th unit was built. It was considered that it should repaired before any units were sold but on a hunch they released it as it was. It was the mistake overdriving the filter that made that synth so influential and sought after. I think that the tricky thing about this as a ported object would be reproducing the effect digitally of that accidental overdrive and if this were introduced over the top of the core design of the more accurate modular design, would that be taxing resources too much. Dang I hope someone comes up with it though. It would be welcome with or without the overdrive I think. I also don't think there would be any problems with licensing. Those designs are now public domain I suspect (although that should be looked into) and they are very well publicly documented now. I built a contemporary re-design of it recently. There are a lot of them out there.
 
I came across this paper "Non-Linear Digital Implementation of the Moog Ladder Filter" (Proceedings of DaFX04, University of Napoli) by Antti Huovilainen and found an implementation for Reason JSFX here that I converted and added.

It is the #define ANTTI. Sound nice and quite Moog'ish but loses stability with res >0.6 . It should be able to go higher so I'll have another look at it. The maths are beyond my and it uses quite a few tanh calcs - I'm sure it could be optimised.

I updated the files at GitHub if anyone is interested.
 
I came across this paper "Non-Linear Digital Implementation of the Moog Ladder Filter" (Proceedings of DaFX04, University of Napoli) by Antti Huovilainen and found an implementation for Reason JSFX here that I converted and added.

It is the #define ANTTI. Sound nice and quite Moog'ish but loses stability with res >0.6 . It should be able to go higher so I'll have another look at it. The maths are beyond my and it uses quite a few tanh calcs - I'm sure it could be optimised.

I updated the files at GitHub if anyone is interested.

There are some tanh approximations you could try, see for an example this thread on stackexchange:
https://math.stackexchange.com/questions/107292/rapid-approximation-of-tanhx
or this on kvr vst:
https://www.kvraudio.com/forum/viewtopic.php?t=262823
 
Perhaps this is an area where a lookup table could be helpful? Linear interpolation is pretty fast, maybe coupled with a 12-bit tanh table (4kb).

I’ve been learning the audio library since I first posted this am I feel I’m at a stage where I can help contribute.
 
Perhaps this is an area where a lookup table could be helpful? Linear interpolation is pretty fast, maybe coupled with a 12-bit tanh table (4kb).

I’ve been learning the audio library since I first posted this am I feel I’m at a stage where I can help contribute.

Seriously? The function used in the filter is actually "double tanh (double x)" in C(++, whatever), double means 64-bit floating point, both the parameter and return value are doubles.
 
I know the tanh() in these filters is at 64-bit precision, but the audio feeding them is 16-bit. This experiment is a waste of RAM space, but i setup an array[65535] and stored tanh +/- 1.0 in int16_t format.

I wanted to see how fast a read of -32768 -> 32767 was done first using a lookup table (including conversion to/from integer to float) and then using the the tanh(); function. Even after converting between
float and int, the lookup table was more than 3x faster. I'm interested in a test next that uses a much smaller array (4096 Vs 65536) and fills in the data with linear interpolation.

The tanh()'s are saturation functions, doubles would sound more smooth...but even nastiness can sometimes sound good. Just look at the wasp filter which abused hex inverters as opamps :)

Code:
#include <time.h>
#include <TimeLib.h>

Code:
int16_t        sampleDivisions[65535];

Code:
  //tanh tests

  uint32_t startmilli, endmilli;
  //startmilli = millis();
  uint32_t arrayCount = 0;
  for (int32_t i = -32768; i < 32768; ++i) {
    float   Val_1 = tanh((float)i / 32768);
    int16_t Val_2 = Val_1 * 32768;
    sampleDivisions[arrayCount] = Val_2;
    arrayCount++;
    }
  startmilli = millis();

  for (int32_t i = -32768; i < 32768; ++i) {
    float   Val_1 = i / 32768.00; //convert to +/-1
    Val_1 = sampleDivisions[i + 32768] / 32768.00;
    Serial.println(Val_1); 
    }
  endmilli = millis();
  Serial.printf("\nlookup table read took %lu milliseconds\n", endmilli - startmilli);

  startmilli = millis();

  for (int32_t i = -32768; i < 32768; ++i) {
    float   Val_1 = i / 32768.00; //convert to +/-1
    Serial.println(tanh(Val_1)); 
    }
  endmilli = millis();
  Serial.printf("\ntanh read took %lu milliseconds\n", endmilli - startmilli);

That array takes 50% ram on the T3.6 :)
 
Played around with an interpolated tanh table (4096 samples) Vs the real computational function. Interestingly there are some interesting rounding errors that deviate the results slightly every pass, maybe that would give it some character?

I'm curious how this function would react in the filters...with interpolation it is still twice the speed of tanh in realtime.
Code:
#include <time.h>
#include <TimeLib.h>

int16_t tanhArrayI [4096];
float   tanhArrayF[4096];

uint32_t startmilli, endmilli;

void setup() {

  //fill a float [4097] array +/- 1.0 tanh function
  uint32_t arrayCount = 0;
  for (int32_t i = -32768; i <= 32768; i += 16) {
    float  Val_1 = (float)i / 32768.00;
    tanhArrayF[arrayCount] = tanh(Val_1);
    arrayCount++;
  }
  //fill a int16_t [4097] array +/- 1.0 tanh function

  startmilli = millis();
  //tanh table with linear interpolation
  for (int32_t i = -32768; i <= 32768; i++) {
    float audioFp = (float)i / 32768.00;
    //Serial.print("index : ");
    //Serial.println(i);
    Serial.println(tanhArrayFloat(audioFp), 8);
  }

  endmilli   = millis();

  Serial.printf("\ntanh (interpolation table) read took %lu milliseconds\n", endmilli - startmilli);

  delay(5000);


  startmilli = millis();
  //regular tanh computation
  for (int32_t i = -32768; i <= 32768; i++) {
    float audioFp = (float)i / 32768;
    Serial.println(tanh(audioFp), 8);
  }

  endmilli   = millis();

  Serial.printf("\ntanh read took %lu milliseconds\n", endmilli - startmilli);

  Serial.println("Comparing interpolated table results Vs tanh results");

  delay(2000);

  for (int32_t i = -32768; i <= 32768; i++) {
    Serial.print("index : ");
    Serial.println(i);
    float audioFp = (float)i * 0.00003052;
    Serial.print(tanhArrayFloat(audioFp), 8);
    Serial.println("< interpolated table tanh");
    Serial.print(tanh(audioFp), 8);
    Serial.println("< tanh function");
    
  }


}


void loop() {
  // put your main code here, to run repeatedly:

}

////////////////////////////////////////////////////////////////////////
//linear interpolation algorithm
__inline float tanhArrayFloat(float i) {
  float remainder = ((float)i + 1.00) * 2048.00; //scale +/- 1.0 float to array index
  uint32_t index = remainder; //truncate
  return tanhArrayF[index] + (tanhArrayF[index + 1] - tanhArrayF[index]) * (remainder - index);
}
//////////////////////////////////////////////////////////////////////////////////////
 
You know I was going to suggest a piecewise function, out of curiosity how did you derive the lookup table? I was recently looking at this: https://arxiv.org/pdf/1809.09534.pdf researching activation functions for lstm networks.

The lookup table is generated using the standard tanh() function;

Code:
//fill a float [4097] array +/- 1.0 tanh function
  uint32_t arrayCount = 0;
  for (int32_t i = -32768; i <= 32768; i += 16) {
    float  Val_1 = (float)i / 32768.00;
    tanhArrayF[arrayCount] = tanh(Val_1);
    arrayCount++;
  }

the interpolation expands it to 32-bit.
 
Back
Top