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
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
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
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; } } //////////////////////////////////////////////////////////////////////////////////////
If i comment outin MusicDSPModel.h, i can control the cutoff, but i feel that might be there for a reason.Code:SetResonance(resonance);
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 }
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;
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.Code:// Default is 0 (no resonance) mDSPm.SetResonance(0.0); // Default is 10,000hz mDSPm.SetCutoff(10000);
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.
Ah I think i figured out why this wasn't working, funny its obvious now.
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);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); }
* see attached
I should also mention this did have a noticable effect on workings of the filter, it sounds more stable now.
Last edited by martianredskies; 04-19-2020 at 03:26 PM.
#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.
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
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
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.
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
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.
ah. Maybe with a 32bit float you could do fixed point integer conversion (int32_t) to speed it up, but 64-bit...
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];That array takes 50% ram on the T3.6Code://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);![]()
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;
the interpolation expands it to 32-bit.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++; }