New Reverbs and Delay for the standard AudioLib (int16_t)

Pio

Well-known member
If anyone would like to try a new interesting reverbs and a modulated ping-pong delay with the standard Teensy Audio Library i've ported a few effects:


Right now there are 4 effects available:
  • Stereo Plate Reverb - the new version with pitch shifters, shimmer, freeze etc.
  • Stereo Spring Reverb
  • Stereo Reverb SC - versatile and very different from the Plate one. Very RAM2 (DMARAM) hungry - ~387k. There is an option to use it with PSRAM +T4.1.
  • Stereo Modulated Ping-Pong Delay - lots of fun in stereo. Can do chorus/flange. Also an option to use PSRAM on T.4.1 for longer delay times.
Each effect has an example sketch including an html page with WebMidi control surfaces.
Target hardware is just Teensy4.x + Audio Adapter Rev.D.
Simply compile, upload, open a corresponding html file in Chrome (browser needs to support WebMidi) and play with the controls.
Internally all effects still work on floats, having the I/O scaled to int16_t.
 
New additions:
  • Stereo 12 Stage Phaser (no demo project yet)
  • GUI
Here is a short sample of the phaser used in a simple synth project. The stereo spread function, showed at the end, adds a phase shift between the left and right modulation waveform.
 
@Pio: Likewise, I added your Stereo Reverb SC to my TeensyMIDIPolySynth (TMPS) & the resulting sound is excellent (it is also much more processor efficient than anything else I had tried) !! I had previously experimented with several other reverbs that were available, but was not very satisfied with the way any of them worked, so I elected for the TMPS not to have a reverb capability. Then you posted your Stereo Reverb SC, & I'm happy to be able to say that the TMPS now has a very nice sounding reverb !! Thanks again for your continued contributions !!

Mark J Culross
KD5RXT
 
Unfortunately it appears I spoke too soon: When I reorganized my code suddenly the _I16 effects are no longer working, and instead I'm getting "Errno 6: Device not configured" when I try to use USB Audio. I'm a little stumped, and not sure if it's my code or the _I16 effects implementations. Can anybody help point me in the right direction?

In short, I have tried these configurations:
1. (Works) Getting dry audio output from Synth object (defined in synth.h) and piping it through AudioEffectPlateReverb_i16 in main.cpp
2. (Works) Adding a "reverb" chain in the synth.h signal path, where the "reverb" object is either an AudioAmplifier or an AudioEffectPlateReverb (NOT the _i16 version; from Pio's older work here: https://github.com/hexeguitar/t40fx/tree/main/Hx_PlateReverb)
3. (Fails: sketch compiles but no audio out and Serial reports Errno6) Adding a reverb chain in the synth.h signal path, where the "reverb" object is either an AudioEffectPlateReverb_i16 or AudioEffectSpringReverb_i16

Here's my main.cpp and synth.h files (based on https://github.com/OliverLSanz/teensy-headless-mpe-synth), with comments outlining how to switch between the 3 options above (currently set up for Option 1):

C++:
#include <Audio.h>
#include "Synth.h"
#include <Arduino.h>
#include <Wire.h>
#include "effect_platereverb_i16.h"

//#include "USBHost_t36.h"

// Set up USB host midi interface
//USBHost myUSBHost;
//MIDIDevice slaveMidiDevice(myUSBHost);

Synth *synth = new Synth();

AudioOutputI2S           i2s1;          
AudioOutputUSB usb0;

// **** CHANGES 1/2 ************************************************************

// Option 2/3 (works if *synth->reverb is AudioAmplifier or AudioEffectPlateReverb,
// fails if *synth->reverb is AudioEffectPlateReverb_i16)
//AudioConnection patchCordOutL(*synth->getOutputL(), 0, usb0, 0);
//AudioConnection patchCordOutR(*synth->getOutputR(), 0, usb0, 1);

// Option 1 (works; tested with *synth->reverb is AudioAmplifier)
AudioEffectPlateReverb_i16 reverb;
AudioConnection patchCordReverbL(*synth->getOutputL(), 0, reverb, 0);
AudioConnection patchCordReverbR(*synth->getOutputR(), 0, reverb, 1);
AudioConnection patchCordOutL(reverb, 0, usb0, 0);
AudioConnection patchCordOutR(reverb, 0, usb0, 1);

//AudioControlSGTL5000     sgtl5000_1;

void onNoteOn(byte channel, byte note, byte velocity) {
  synth->noteOn(channel, note, velocity);
  digitalWrite(LED_BUILTIN, HIGH);
}

void onNoteOff(byte channel, byte note, byte velocity) {
  synth->noteOff(channel, note, velocity);
  digitalWrite(LED_BUILTIN, LOW);
}

void onMidiControlChange(byte channel, byte control, byte value){
  synth->controlChange(channel, control, value);
}

void onPitchChange(byte channel, int pitch){
  synth->pitchChange(channel, pitch);
}

void onAfterTouch(byte channel, byte pressure){
  //Serial.println("Received Aftertouch Channel");
  synth->afterTouch(channel, pressure);
}

void onAfterTouchPoly(byte channel, byte note, byte pressure){
  //Serial.println("Received aftertouch Poly");
  synth->afterTouch(channel, pressure);
}


int next_check{millis()+5000};

void setup() {
  Serial.begin(115200);

  //myUSBHost.begin();
    //slaveMidiDevice.setHandleNoteOff(onNoteOff);
    //slaveMidiDevice.setHandleNoteOn(onNoteOn);
    //slaveMidiDevice.setHandleControlChange(onMidiControlChange);
  //slaveMidiDevice.setHandlePitchChange(onPitchChange);
  //slaveMidiDevice.setHandleAfterTouchChannel(onAfterTouch);

  //pinMode(LED_BUILTIN, OUTPUT);

  AudioMemory(120);

  //sgtl5000_1.enable();
  //sgtl5000_1.volume(0.5);

  usbMIDI.setHandleNoteOn(onNoteOn);
  usbMIDI.setHandleNoteOff(onNoteOff);
  usbMIDI.setHandleControlChange(onMidiControlChange);
  usbMIDI.setHandlePitchChange(onPitchChange);
  usbMIDI.setHandleAfterTouch(onAfterTouch);
  usbMIDI.setHandleAfterTouchPoly(onAfterTouchPoly);
 

  // **** Changes 2/2 **********************************************************

  // Option 2/3 (nothing)

  // Option 1 (Works)
  reverb.size(1.0);     // max reverb length
  reverb.lowpass(0.3);  // sets the reverb master lowpass filter
  reverb.lodamp(0.1);   // amount of low end loss in the reverb tail
  reverb.hidamp(0.2);   // amount of treble loss in the reverb tail
  reverb.diffusion(1.0);  // 1.0 is the detault setting, lower it to create more "echoey" reverb
  reverb.wet_level(1.0f);
  reverb.bypass_set(false);

  // **** END CHANGES 2/2 ******************************************************

  // Starting sequence
  digitalWrite(LED_BUILTIN, HIGH);
  delay(1000);
  digitalWrite(LED_BUILTIN, LOW);
  Serial.println("MPE Synth Ready!");

}

void loop() {
    //myUSBHost.Task();
      //slaveMidiDevice.read();
    usbMIDI.read();

    int now{millis()};

    if (now > next_check){
      Serial.print("%, Total CPU Usage: ");
      Serial.print(AudioProcessorUsageMax());
      Serial.println("%");
      AudioProcessorUsageMaxReset();
      next_check = now + 5000;
    }



}

And here's (working) synth.h, using an AudioAmplifier as a "reverb":

C++:
#ifndef Synth_h
#define Synth_h

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

// **** CHANGES 1/5 ************************************************************
#include "hexefx_audio_I16.h"
//#include "effect_platervbstereo.h"
// **** END CHANGES 1/5 ********************************************************

#ifndef ULLONG_MAX
#define ULLONG_MAX 0xffffffffffffffff
#endif

// Number of voices
const byte voiceCount = 1; // max = 16
const byte voicesPerMixer = 4;
const byte mixerCount = voiceCount/voicesPerMixer + voiceCount%voicesPerMixer > 0? 1:0;
//const byte mixerCount = 4;

/*
 * Synth
 */
class Synth{
 
  private:
    Voice *voices[voiceCount];
    
    AudioConnection* patchCordsVoices[mixerCount + voiceCount];
    AudioMixer4 *mixersVoices0[mixerCount];
    AudioMixer4 *mixerSynOut;

    AudioConnection *patchCordReverbSendDirectL;
    AudioConnection *patchCordReverbSendDirectR;

    AudioMixer4 *reverbSendL;
    AudioMixer4 *reverbSendR;

    AudioConnection *patchCordReverbL;
    AudioConnection *patchCordReverbR;

    // **** CHANGES 2/5 ********************************************************
    AudioAmplifier *reverb; // WORKS
    //AudioEffectPlateReverb_i16 *reverb; // Errno6: Device not configured
    //AudioEffectPlateReverb *reverb; // ALSO WORKS
    //AudioEffectSpringReverb_i16 *reverb; // Errno6: Device not configured
    
    
    // **** END CHANGES 2/5 ****************************************************

    AudioConnection *patchCordDirectRetL;
    AudioConnection *patchCordDirectRetR;
    AudioConnection *patchCordReverbRetL;
    AudioConnection *patchCordReverbRetR;

    AudioMixer4 *efxReturnsL;
    AudioMixer4 *efxReturnsR;

    AudioConnection *patchCordSendOutL;
    AudioConnection *patchCordSendOutR;

    AudioAmplifier *outputL;
    AudioAmplifier *outputR;

    
  public:
    Synth();

    void noteOn(byte channel, byte note, byte velocity);
    void noteOff(byte channel, byte note, byte velocity);
    void controlChange(byte channel, byte control, byte value);
    void pitchChange(byte channel, int pitch);
    void afterTouch(byte channel, byte pressure);
    Voice **getVoices();

    AudioAmplifier * getOutputL();
    AudioAmplifier * getOutputR();

};

/**
 * Constructor
 */

// create and mix synth voices
inline Synth::Synth(){
  this->mixerSynOut = new AudioMixer4();
  this->mixerSynOut->gain(0, 1.0/float(mixerCount));
  this->mixerSynOut->gain(1, 1.0/float(mixerCount));
  this->mixerSynOut->gain(2, 1.0/float(mixerCount));
  this->mixerSynOut->gain(3, 1.0/float(mixerCount));

  for (int i = 0; i < mixerCount; i++) {
    this->mixersVoices0[i] = new AudioMixer4();
    this->mixersVoices0[i]->gain(0, 0.4);
    this->mixersVoices0[i]->gain(1, 0.4);
    this->mixersVoices0[i]->gain(2, 0.4);
    this->mixersVoices0[i]->gain(3, 0.4);
    
    this->patchCordsVoices[i] =
      new AudioConnection(*this->mixersVoices0[i], 0, *this->mixerSynOut, i%voicesPerMixer);
  }
 
  for (int i = 0; i < voiceCount; i++) {
    this->voices[i] = new Voice();
    this->patchCordsVoices[i] =
      new AudioConnection(*this->voices[i]->getOutput(), 0, *this->mixersVoices0[i/voicesPerMixer], i%voicesPerMixer);
  }

  // EFX chain
 
  this->reverbSendL = new AudioMixer4();
  this->reverbSendR = new AudioMixer4();
 
  this->reverbSendL->gain(0, 1.0);
  this->reverbSendR->gain(0, 1.0);

  this->patchCordReverbSendDirectL =
    new AudioConnection(*this->mixerSynOut, 0, *this->reverbSendL, 0);
  this->patchCordReverbSendDirectR =
    new AudioConnection(*this->mixerSynOut, 0, *this->reverbSendR, 0);
 
  // ***** CHANGES 3/5  ********************************************************
  this->reverb = new AudioAmplifier(); //  WORKS
  // this->reverb = new AudioEffectPlateReverb(); // WORKS
  //this->reverb = new AudioEffectSpringReverb_i16(); // Errno6
  //this->reverb = new AudioEffectPlateReverb_i16(); // errno6
  // ***** END 3/5 *************************************************************

  this->patchCordReverbL =
    new AudioConnection(*this->reverbSendL, 0, *this->reverb, 0);

  // CHANGES 4/5 ***************************************************************
 
  // Option 1
  this->patchCordReverbR =
    new AudioConnection(*this->reverbSendR, 0, *this->reverb, 0);
 
  // Options 2/3
  //this->patchCordReverbR =
  //  new AudioConnection(*this->reverbSendR, 0, *this->reverb, 1);
  // END CHANGES 4/5 ***********************************************************

  this->efxReturnsL = new AudioMixer4();
  this->efxReturnsL->gain(0, 0.5);
  this->efxReturnsL->gain(1, 0.5);
  this->patchCordDirectRetL =
    new AudioConnection(*this->mixerSynOut, 0, *this->efxReturnsL, 0);
  this->patchCordReverbRetL =
    new AudioConnection(*this->reverb, 0, *this->efxReturnsL, 1);

  this->efxReturnsR = new AudioMixer4();
  this->efxReturnsR->gain(0, 0.5);
  this->efxReturnsR->gain(1, 0.5);
  this->patchCordDirectRetR =
    new AudioConnection(*this->mixerSynOut, 0, *this->efxReturnsR, 0);
  this->patchCordReverbRetR =
    new AudioConnection(*this->reverb, 0, *this->efxReturnsR, 1);

  this->outputL =
    new AudioAmplifier();
  this->outputL->gain(1.0);
  this->patchCordSendOutL =
    new AudioConnection(*this->efxReturnsL, *this->outputL);
 
  this->outputR =
    new AudioAmplifier();
  this->outputR->gain(1.0);
  this->patchCordSendOutR =
    new AudioConnection(*this->efxReturnsR, *this->outputR); 

 
  // Configure Reverb

  // **** CHANGES 5/5 **********************************************************

  this->reverb->gain(1.0); // use when reverb is AudioAmplifier; Works
 
  // Use when reverb is AudioEffectPlateReverb, AudioEffectPlateReverb_i16, or AudioEffectSpringReverb_i16
  //this->reverb->size(1.0);     // max reverb length
  //this->reverb->lowpass(0.3);  // sets the reverb master lowpass filter
  //this->reverb->lodamp(0.1);   // amount of low end loss in the reverb tail
  //this->reverb->hidamp(0.2);   // amount of treble loss in the reverb tail
  //this->reverb->diffusion(1.0);  // 1.0 is the detault setting, lower it to create more "echoey" reverb
 
  // Use only for _i16 effects
  //this->reverb->wet_level(1.0f);
  //this->reverb->bypass_set(false);
 
  // **** END CHANGES 5/5 ******************************************************
 

}

/**
 * Note on
 */
inline void Synth::noteOn(byte channel, byte note, byte velocity){
  bool foundOne = false;
  int oldestVoice = 0;
  unsigned long oldestVoiceTime = ULLONG_MAX;

  for (int i = 0; i < voiceCount; i++) {
    // Search for the oldest voice
    if(this->voices[i]->last_played < oldestVoiceTime){
      oldestVoiceTime = this->voices[i]->last_played;
      oldestVoice = i;
    }
    
    // Search for an inactive voice
    if(!this->voices[i]->isActive()){
      this->voices[i]->noteOn(channel, note, velocity);
      foundOne = true;
      break;
    }
  }

  // No inactive voice then will take over the oldest note
  if(!foundOne){
    this->voices[oldestVoice]->noteOn(channel, note, velocity);
  }
}

/**
 * Note off
 */
inline void Synth::noteOff(byte channel, byte note, byte velocity){
  for (int i = 0; i < voiceCount ; i++) {
    if(this->voices[i]->currentNote == note){
      this->voices[i]->noteOff(channel, note, velocity);
    }
  }
}

/**
 * Return the audio output
 */
inline AudioAmplifier * Synth::getOutputL(){
  return this->outputL;
}
inline AudioAmplifier * Synth::getOutputR(){
  return this->outputR;
}


/**
 * Control Change
 */
void Synth::controlChange(byte channel, byte control, byte value){
  for (int i = 0; i < voiceCount ; i++) {
    if(this->voices[i]->channel == channel && this->voices[i]->isActive()){
      this->voices[i]->controlChange(channel, control, value);
    }
  }
}


void Synth::pitchChange(byte channel, int pitch){
  for (int i = 0; i < voiceCount ; i++) {
    if(this->voices[i]->channel == channel && this->voices[i]->isActive()){
      this->voices[i]->pitchChange(channel, pitch);
    }
  }
}

void Synth::afterTouch(byte channel, byte pressure){
    //Serial.print("Received aftertouch");
  for (int i = 0; i < voiceCount ; i++) {
    if(this->voices[i]->channel == channel && this->voices[i]->isActive()){
      this->voices[i]->afterTouch(channel, pressure);
    }
  }
}

inline Voice** Synth::getVoices(){
  return this->voices;
}

#endif
 
Last edited:
After a bit of testing:
1. If i try the reverb_i16 outside the Synth class it works.
2. reverb_i16 used inside the Synth class result in a boot loop. This is probably why it shows Errno6.

I think the problem is caused by spreading the object creation and making connections all over the place instead of doing it the way the GUI it generates:
1. Create objects
2. Make all connections
There is a 4 channel mixer in each Voice object doing practically nothing. It could be used to set the volume for each voice, but the same can be done using the voice mixers.
There is also a huge audio memory pool allocated in your code: 120. Reverb uses a lot of DMARAM, AudioMemory is allocated there , too. This was also causing a problem. Reducing it to 24 fixed the boot loop. The max usage reported with the 16 voice polyphony is 17.

Btw, if there will be only one stereo input (stereo synth out) coming into the Reverb, you can use it's internal dry/wet mixer instead of the 4 channel Send mixers. There is one potential issue with the external mixer: setting the dry signal (Voice) and the reverb channel to 1 will result in output exceeding the -1 - +1 range and clipping. The reverb.mix( 0.0f ... 1.0f) controls the mix ratio and keeps the output amplitude in safe range.
It greatly simplify the internal routing, making it just a simple Synth->Reverb->Amplifier in series.
I played a bit with the code and here is a setup that compiles and works for me, using USB audio as output, sound sample included.
 

Attachments

  • usbaudiosynth.zip
    367.2 KB · Views: 1
Nice! Have fun!
I've also started working on a 8 voice poly-synth based on this one:
I guess, there will be more synth related components in this library.
One idea i'd like to implement is an envelope component with stereo output and volume/pan controls. Each voice will have a stereo output with Pan available also as a modulation input, thinking: LFO, autopanning or envelope controlled pan for each voice separately. Or just randomly pick the panorama setting for each appearing note. With reverb it can create very nice wide sounds.

I wonder if there is a WebMIDI based sequencer existing somewhere on the depths of inet. I've found some, but these were generating sounds, i'd like it to send a MIIDI stream to a chosen port instead. Combined with the touchMIDI it would be a nice tool to demo all the effects in a synth environment.
 
Back
Top