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:
parameters.h, object for managing the parameters:
Any sort of help/insight is welcome!
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!