Modifying Synth parameters on-the-fly creating timing issues (T 4.1 + Audio shield)

rsoric

New member
Greetings! I'm working on a Teensy 4.1 based project - an FM drum machine. It has 6 rotary encoders for user input and a LCD to show the current parameter being tweaked. It recieves MIDI info about which notes (drums) to play via the built in usbMIDI interface. See the attatched photo for a pic of the hardware on a custom PCB!

I'm running into an issue with timing. Sending notes into the device with usbMIDI works great, but when I added the functionality of also polling the rotary encoders for parameter tweaking, I'm getting noticable delays/stuttering in the drum sounds as an individual parameter is being tweaked. I've made the rotary encoders be polled every 5 ms, and if i don't touch them, there seem to be no to very minimal stutters in playing of the drum sounds.

Are the stuttershappening because of the time it's required to calculate/process the tweaking of the parameter via the audio objects? Eg. kick_1_car_env.decay(kickParams[i-1]->getValue());

I'm attatching my code below if you wish to review it but please keep in mind that so far I've only added and worked with the Kick drum, so all note on and note off messages are playing the kick drum envelope. If you have any questions about the code, feel free to ask!

main.cpp:
Code:
#include "Arduino.h"
#include <LiquidCrystal_I2C.h>
#include <RotaryEncoder.h>
#include <parameters.h>
#include <elapsedMillis.h>

#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

// GUItool: begin automatically generated code
AudioSynthNoiseWhite chat_1_mod;        // xy=93,416
AudioSynthWaveformSine chat_2_mod;      // xy=92.99996948242188,516
AudioSynthWaveformSine kick_1_mod;      // xy=148.00003051757812,112
AudioSynthWaveformSine kick_2_mod;      // xy=148,228
AudioEffectEnvelope chat_2_mod_env;     // xy=290.99993896484375,519
AudioEffectEnvelope chat_1_mod_env;     // xy=301.9999694824219,421
AudioEffectEnvelope kick_1_mod_env;     // xy=325,144
AudioEffectEnvelope kick_2_mod_env;     // xy=324.9999694824219,260
AudioSynthWaveformModulated chat_2_car; // xy=486.9998779296875,540.0000305175781
AudioSynthWaveformModulated kick_1_car; // xy=499.99993896484375,179.00003051757812
AudioSynthWaveformModulated kick_2_car; // xy=499.9999084472656,295.0000305175781
AudioSynthWaveformModulated chat_1_car; // xy=504.9999084472656,440.0000305175781
AudioEffectEnvelope kick_1_car_env;     // xy=678,212
AudioEffectEnvelope kick_2_car_env;     // xy=677.9999694824219,328
AudioEffectEnvelope chat_1_car_env;     // xy=689,441
AudioEffectEnvelope chat_2_car_env;     // xy=689.9999389648438,547
AudioMixer4 kick_mixer;                 // xy=905,323
AudioMixer4 chat_mixer;                 // xy=925,427
AudioMixer4 master;                     // xy=1183,386
AudioOutputI2S i2s1;                    // xy=1410,385.0000305175781
AudioConnection patchCord1(chat_1_mod, chat_1_mod_env);
AudioConnection patchCord2(chat_2_mod, chat_2_mod_env);
AudioConnection patchCord3(kick_1_mod, kick_1_mod_env);
AudioConnection patchCord4(kick_2_mod, kick_2_mod_env);
AudioConnection patchCord5(chat_2_mod_env, 0, chat_2_car, 0);
AudioConnection patchCord6(chat_1_mod_env, 0, chat_1_car, 0);
AudioConnection patchCord7(kick_1_mod_env, 0, kick_1_car, 0);
AudioConnection patchCord8(kick_2_mod_env, 0, kick_2_car, 0);
AudioConnection patchCord9(chat_2_car, chat_2_car_env);
AudioConnection patchCord10(kick_1_car, kick_1_car_env);
AudioConnection patchCord11(kick_2_car, kick_2_car_env);
AudioConnection patchCord12(chat_1_car, chat_1_car_env);
AudioConnection patchCord13(kick_1_car_env, 0, kick_mixer, 0);
AudioConnection patchCord14(kick_2_car_env, 0, kick_mixer, 1);
AudioConnection patchCord15(chat_1_car_env, 0, chat_mixer, 0);
AudioConnection patchCord16(chat_2_car_env, 0, chat_mixer, 1);
AudioConnection patchCord17(kick_mixer, 0, master, 0);
AudioConnection patchCord18(chat_mixer, 0, master, 1);
AudioConnection patchCord19(master, 0, i2s1, 0);
AudioConnection patchCord20(master, 0, i2s1, 1);
AudioControlSGTL5000 sgtl5000_1; // xy=1085,205
// GUItool: end automatically generated code

// IO objects
static LiquidCrystal_I2C lcd(0x27, 16, 2);
static RotaryEncoder encoder1(33, 34, RotaryEncoder::LatchMode::TWO03);
static RotaryEncoder encoder2(35, 36, RotaryEncoder::LatchMode::TWO03);
static RotaryEncoder encoder3(37, 38, RotaryEncoder::LatchMode::TWO03);
static RotaryEncoder encoder4(28, 27, RotaryEncoder::LatchMode::TWO03);
static RotaryEncoder encoder5(30, 29, RotaryEncoder::LatchMode::TWO03);
static RotaryEncoder encoderMenu(31, 32, RotaryEncoder::LatchMode::FOUR3);
static RotaryEncoder encoders[5] = {encoder1, encoder2, encoder3, encoder4, encoder5};

static int encoder1pos = 0;
static int encoder2pos = 0;
static int encoder3pos = 0;
static int encoder4pos = 0;
static int encoder5pos = 0;
static int encoderMenupos = 0;
static int encoderPositions[5] = {encoder1pos, encoder2pos, encoder3pos, encoder4pos, encoder5pos};

static String drums[] = {"Kick", "CHat", "OHat", "Snre", "Clap", "Rim ", "Tone"};
static int currentDrumIndex = 1;
int numOfDrums = 7;
elapsedMillis encoderPollTimer;
unsigned int encoderPollDelay;

// Parameters
static parameter kickParam1("CARRIER 1 PITCH ", 30, 100);
static parameter kickParam2("CARRIER 1 DECAY ", 5, 1750);
static parameter kickParam3("MOD 1 AMPLITUDE ", 0, 1);
static parameter kickParam4("MOD 2 AMPLITUDE ", 0, 1);
static parameter kickParam5("MOD 2 PITCH     ", 20, 500);
static parameter * kickParams[5] = {&kickParam1, &kickParam2, &kickParam3, &kickParam4, &kickParam5};

void initKick()
{
  // MOD 1
  kick_1_mod.frequency(0.1);
  kick_1_mod.amplitude(kickParam3.getValue()); // PARAM 3
  kick_1_mod_env.attack(0);
  kick_1_mod_env.hold(0);
  kick_1_mod_env.decay(750);
  kick_1_mod_env.sustain(0);

  // CARRIER 1
  kick_1_car.begin(0.7, kickParam1.getValue(), WAVEFORM_SINE); // PARAM 1
  kick_1_car_env.attack(0);
  kick_1_car_env.hold(0);
  kick_1_car_env.decay(kickParam2.getValue()); // PARAM 2
  kick_1_car_env.sustain(0);

  // MOD 2
  kick_2_mod.frequency(kickParam5.getValue()); // PARAM 5
  kick_2_mod.amplitude(kickParam4.getValue()); // PARAM 4
  kick_2_mod_env.attack(0);
  kick_2_mod_env.hold(0);
  kick_2_mod_env.decay(56);
  kick_2_mod_env.sustain(0);

  // CARRIER 2
  kick_2_car.begin(0.4, 225, WAVEFORM_SINE);
  kick_2_car_env.attack(0);
  kick_2_car_env.hold(0);
  kick_2_car_env.decay(15);
  kick_2_car_env.sustain(0);
}

void updateKickParam(int i)
{
  switch (i)
  {
    case 1:
      kick_1_car.frequency(kickParams[i-1]->getValue());
      break;
    case 2:
      kick_1_car_env.decay(kickParams[i-1]->getValue());
      break;
    case 3:
      kick_1_mod.amplitude(kickParams[i-1]->getValue());
      break;
    case 4:
      kick_2_mod.amplitude(kickParams[i-1]->getValue());
      break;
    case 5:
      kick_2_mod.frequency(kickParams[i-1]->getValue());
      break;
  }
}

void pollMenuEncoder()
{
  encoderMenu.tick();
  int newPos = encoderMenu.getPosition();
  if (newPos != encoderMenupos)
  {
    encoderMenupos = newPos;
    if ((int)encoderMenu.getDirection() == 1)
    {
      currentDrumIndex++;
      if (currentDrumIndex > numOfDrums - 1)
      {
        currentDrumIndex = 0;
      }
    }
    else
    {
      currentDrumIndex--;
      if (currentDrumIndex < 0)
      {
        currentDrumIndex = numOfDrums - 1;
      }
    }

    lcd.clear();
    lcd.setCursor(12, 0);
    lcd.print(drums[currentDrumIndex]);
  }
}

void pollEncoders()
{
  for (int i = 0; i < 5; i++)
  {
    encoders[i].tick();
    int newPos = encoders[i].getPosition();
    if (newPos != encoderPositions[i])
    {
      encoderPositions[i] = newPos;

      if ((int)encoders[i].getDirection() == 1)
      {
        //params[currentDrumIndex][i].increment();
        kickParams[i]->increment();
        
      }
      else
      {
        //params[currentDrumIndex][i].decrement();
        kickParams[i]->decrement();
      }

      lcd.setCursor(0, 1);
      lcd.print(kickParams[i]->getName());
      lcd.setCursor(0, 0);
      char stepValueToPrint[3];
      sprintf(stepValueToPrint, "%03d", kickParams[i]->getCurrentStep());
      lcd.print(stepValueToPrint);

      updateKickParam(i+1);
      

    }
  }

  //pollMenuEncoder();
}

void noteOnHandler(byte channel, byte note, byte velocity)
{
  kick_1_mod.phase(180);
  kick_2_mod.phase(0);
  kick_1_mod_env.noteOn();
  kick_1_car_env.noteOn();
  kick_2_mod_env.noteOn();
  kick_2_car_env.noteOn();
}

void noteOffHandler(byte channel, byte note, byte velocity)
{
  kick_1_mod_env.noteOff();
  kick_1_car_env.noteOff();
  kick_2_mod_env.noteOff();
  kick_2_car_env.noteOff();
}


void setup()
{
  //5 ms delay for polling encoders
  encoderPollDelay = 5;
  encoderPollTimer = 0;

  // Init LCD
  lcd.init();
  delay(150);
  lcd.backlight();
  lcd.setCursor(5, 0);
  lcd.print("FMDRUM");
  delay(1500);
  lcd.clear();
  lcd.setCursor(12, 0);
  lcd.print(drums[currentDrumIndex]);

  AudioMemory(40);
  sgtl5000_1.enable();
  sgtl5000_1.volume(0.5);

  initKick();

  usbMIDI.setHandleNoteOn(noteOnHandler);
  usbMIDI.setHandleNoteOff(noteOffHandler);
}

void loop()
{
  usbMIDI.read();

  if(encoderPollTimer > encoderPollDelay)
  {
    encoderPollTimer = 0;
    pollEncoders();
  }
}

parameters.h, object for managing the parameters:
Code:
#include "Arduino.h"

class parameter
{
private:
    float value;
    float stepIncrementValue;
    int currentStep;
    float min;
    float max;
    String name;

public:
    parameter(String name, float min, float max)
    {
        this->name = name;
        this->currentStep = 64;
        this->stepIncrementValue = (max - min) / 128.0;
        this->min = min;
        this->max = max;
        this->value = (currentStep * stepIncrementValue) + this->min;
    }

    void increment()
    {
        if (currentStep < 128)
        {
            this->currentStep++;
            this->value = (currentStep * stepIncrementValue) + this->min;
        }
    }

    void decrement()
    {
        if (currentStep > 0)
        {
            this->currentStep--;
            this->value = (currentStep * stepIncrementValue) + this->min;
        }
    }

    float getValue()
    {
        return this->value;
    }

    String getName()
    {
        return this->name;
    }

    int getCurrentStep()
    {
        return this->currentStep;
    }
};

Any sort of help/insight is welcome!

298654959_772349267389710_2109310858742285367_n.jpg
 
Greetings! I'm working on a Teensy 4.1 based project - an FM drum machine. It has 6 rotary encoders for user input and a LCD to show the current parameter being tweaked. It recieves MIDI info about which notes (drums) to play via the built in usbMIDI interface. See the attatched photo for a pic of the hardware on a custom PCB!

I'm running into an issue with timing. Sending notes into the device with usbMIDI works great, but when I added the functionality of also polling the rotary encoders for parameter tweaking, I'm getting noticable delays/stuttering in the drum sounds as an individual parameter is being tweaked. I've made the rotary encoders be polled every 5 ms, and if i don't touch them, there seem to be no to very minimal stutters in playing of the drum sounds.

Any sort of help/insight is welcome!

View attachment 29149

@rsoric:

I don't know if what you are experiencing originates from the same underlying cause, but in the latest version of my TeensyMIDIPolySynth (T4.1), I am also using a touch display using the RA8875 controller. I noticed that as I made changes to the "electronic sliders" painted on the display, the the note commands coming over MIDI were affected, resulting in the audio sometimes being delayed & sometimes dropping note commands (either noteOn or noteOff). The solution that I used to overcome this particular challenge in my case was to add a second Teensy (T4.0) which is strictly responsible for interacting with the Audio Adapter. This left the original T4.1 to manage the display & touchscreen. The T4.1 also still manages the MIDI interfaces (traditional MIDI via the standard 6N138, usbMIDI, & MIDI via USBhost) & anything that comes in from any of the MIDI interfaces is sent to the T4.0 via a shared Serial4 running at 500kbaud between the two Teensy processors). Note: this approach also solved another minor problem: with only the T4.1 (before the T4.0 was added), whenever the screen was touched, a faint audio whine was induced into the output audio stream . . . that problem is also completely gone with the dual-T4.x approach !!

Just a suggestion (which worked for me) & hope this helps in some way . . .

Mark J Culross
KD5RXT
 
Mark, Thanks for your reply! I understand that making the teensy multitask can produce these kinds of effects. Unfortunately I can't add any more PCB's/get a new board due to deadlines for my project (it's for uni). I'm going to try and figure it out in software. If it's worth anything, I'm already using a different i2c interface for the audio shield (the default one) and for writing to the display (i used i2c1 on the T4.1.).

Thanks again
-Rob
 
Have you tried removing the LCD update calls and "flying blind" to see if the problems go away? Looking at the library, it seems it's a pretty time-consuming job to update it, which would at least affect the MIDI response. As it's a uni project, you might be able to get hold of an oscilloscope, bolt in code to toggle pins at crucial places, and look for how long various things take and how they affect the audio engine. (For the latter, putting a pin high and low at the start and end of software_isr() in AudioStream.cpp is a really good diagnostic.)
 
Another thing to try - rather than checking all 5 encoders in one call to pollEncoders() every 5 ms try cycling around each of them, 1 every millisecond.
 
Another thing to try - rather than checking all 5 encoders in one call to pollEncoders() every 5 ms try cycling around each of them, 1 every millisecond.

Thank you for your input! That's a great, logical approach that actually helped the performance noticably.

I have since also implemented a teensythreads thread for usbmidi.read() and it also improved the performance. For now, the drums sound quite 'in time' and small delays are barely noticable! So there was definite improvement.
 
Have you tried removing the LCD update calls and "flying blind" to see if the problems go away? Looking at the library, it seems it's a pretty time-consuming job to update it, which would at least affect the MIDI response. As it's a uni project, you might be able to get hold of an oscilloscope, bolt in code to toggle pins at crucial places, and look for how long various things take and how they affect the audio engine. (For the latter, putting a pin high and low at the start and end of software_isr() in AudioStream.cpp is a really good diagnostic.)

Hi, thanks for your reply, I haven't had time to try it this way, unfortunately as I'm busy wrapping up the project to meet the deadline and the LCD is definately going to be a part of the final design.

In my post above this one I have already mentioned that adding teensythreads for usbmidi.read() and implementing the 'one by one' encoder method as @houtson mentioned have improved the performance significantly.

I can't wait to share this project with you all when it's finished!
 
Back
Top