Forum Rule: Always post complete source code & details to reproduce any issue!
Page 1 of 2 1 2 LastLast
Results 1 to 25 of 26

Thread: Porting moog ladder filters to audio objects?

  1. #1
    Senior Member
    Join Date
    May 2019
    Location
    Brisbane, QLD
    Posts
    104

    Porting moog ladder filters to audio objects?

    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

  2. #2
    Senior Member
    Join Date
    Nov 2012
    Posts
    1,376
    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.
    MusicDSP.zip

    Pete

  3. #3
    Senior Member
    Join Date
    Nov 2012
    Posts
    1,376
    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.
    MusicDSP_1.zip

    Pete

  4. #4
    Senior Member
    Join Date
    May 2019
    Location
    Brisbane, QLD
    Posts
    104
    Quote Originally Posted by el_supremo View Post
    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.
    MusicDSP_1.zip

    Pete
    Awesome! excited to try it out on the T3.6!

  5. #5
    Senior Member
    Join Date
    May 2019
    Location
    Brisbane, QLD
    Posts
    104
    Quote Originally Posted by el_supremo View Post
    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.
    MusicDSP_1.zip

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

  6. #6
    Senior Member
    Join Date
    May 2019
    Location
    Brisbane, QLD
    Posts
    104
    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
    	}

  7. #7
    Senior Member
    Join Date
    May 2019
    Location
    Brisbane, QLD
    Posts
    104
    Quote Originally Posted by el_supremo View Post
    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.
    MusicDSP_1.zip

    Pete
    Is there a trick to getting RKSimulationModel to work. Simply commenting out
    HTML Code:
    //#define MDSPM
    leaves me with no audio? I want to hear if these "clicks" are in other filter models.

  8. #8
    Senior Member
    Join Date
    Nov 2012
    Posts
    1,376
    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

  9. #9
    Senior Member
    Join Date
    May 2019
    Location
    Brisbane, QLD
    Posts
    104
    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.

    Quote Originally Posted by el_supremo View Post
    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

  10. #10
    Senior Member
    Join Date
    May 2019
    Location
    Brisbane, QLD
    Posts
    104
    Quote Originally Posted by el_supremo View Post
    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.
    Attached Files Attached Files
    Last edited by martianredskies; 04-19-2020 at 03:26 PM.

  11. #11
    Senior Member
    Join Date
    May 2019
    Location
    Brisbane, QLD
    Posts
    104
    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.


    Quote Originally Posted by el_supremo View Post
    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

  12. #12
    Senior Member
    Join Date
    May 2019
    Location
    Brisbane, QLD
    Posts
    104
    #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.
    Attached Files Attached Files

  13. #13
    Senior Member
    Join Date
    Nov 2012
    Posts
    1,376
    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.
    MusicDSP_3.zip

    Pete

  14. #14
    Senior Member houtson's Avatar
    Join Date
    Aug 2015
    Location
    Scotland
    Posts
    111
    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

  15. #15
    Senior Member
    Join Date
    Nov 2012
    Posts
    1,376
    Thanks for that Paul. I need to get around to adding two pots so that varying the resonance and cutoff are easier.

    Pete

  16. #16

    Would be great,...if..

    Quote Originally Posted by martianredskies View Post
    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.

  17. #17
    Senior Member houtson's Avatar
    Join Date
    Aug 2015
    Location
    Scotland
    Posts
    111
    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.

  18. #18
    Quote Originally Posted by houtson View Post
    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/quest...ation-of-tanhx
    or this on kvr vst:
    https://www.kvraudio.com/forum/viewtopic.php?t=262823

  19. #19
    Senior Member
    Join Date
    May 2019
    Location
    Brisbane, QLD
    Posts
    104
    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.

  20. #20
    Quote Originally Posted by martianredskies View Post
    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.

  21. #21
    Senior Member
    Join Date
    May 2019
    Location
    Brisbane, QLD
    Posts
    104
    ah. Maybe with a 32bit float you could do fixed point integer conversion (int32_t) to speed it up, but 64-bit...

  22. #22
    Senior Member
    Join Date
    May 2019
    Location
    Brisbane, QLD
    Posts
    104
    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

  23. #23
    Senior Member
    Join Date
    May 2019
    Location
    Brisbane, QLD
    Posts
    104
    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);
    }
    //////////////////////////////////////////////////////////////////////////////////////

  24. #24
    Senior Member
    Join Date
    Dec 2019
    Posts
    125
    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.

  25. #25
    Senior Member
    Join Date
    May 2019
    Location
    Brisbane, QLD
    Posts
    104
    Quote Originally Posted by RABB17 View Post
    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.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •