Realtime MIDI Sequencer

ExNoise

Member
good afternoon, i am working on a project that takes input from a usb midi controller on the host port, saves each event to a struct containing type, data1, data2, channel, and timestamp as shown here:

Code:
struct MidiEvent {
  uint32_t delta_ms;
  midi::MidiType type ;
  midi::DataByte data1;
  midi::DataByte data2;
  midi::Channel channel;
};
records each event to a temporary file during record mode
Code:
void recordTick() {
  if (!recordFile) {
    return;
  }

  while (midi2.read()) { 
    uint32_t now = millis();
    Ev.delta_ms = now - lastEventMillis; // Delta t.o.v. vorige event
    lastEventMillis = now;
    Ev.type = midi2.getType();
    Ev.data1 = midi2.getData1();
    Ev.data2 = midi2.getData2();
    Ev.channel = midi2.getChannel();
  
    recordFile.write((const byte *)&Ev, sizeof(Ev));
and plays it back in playback mode.
Code:
void playTick() {
  if (!playFile) {
    Serial.println("no file found");
    return;
  }

  if (!hasNextEvent) {
    if (LOOP_PLAYBACK) {
      restartPlayback();
    } else {
      stopPlay();
    }
    return;
  }

  uint32_t elapsed = millis() - playbackStartMillis; // Hoe lang spelen we al
  while (hasNextEvent && elapsed >= playbackElapsedAtNext) {
   if (nextEvent.type == 144) {                         
   MIDI.sendNoteOn(nextEvent.data1, nextEvent.data2, nextEvent.channel);
    Serial.println();
    Serial.print("type=");
    Serial.print(nextEvent.type);
    Serial.print("channel=");
      Serial.print(nextEvent.channel);
      Serial.print(", note=");
      Serial.print(nextEvent.data1);
      Serial.print(", velocity");
      Serial.print(nextEvent.data2);
    Serial.print(", time=");
    Serial.print (elapsed);
  
    } else if (nextEvent.type == 128) {
    MIDI.sendNoteOff(nextEvent.data1, nextEvent.data2, nextEvent.channel);
    Serial.println();
    Serial.print("type=");
    Serial.print(nextEvent.type);
    Serial.print("channel=");
      Serial.print(nextEvent.channel);
      Serial.print(", note=");
      Serial.print(nextEvent.data1);
      Serial.print(", velocity");
      Serial.print(nextEvent.data2);
    Serial.print(", time=");
    Serial.print (elapsed);
  
    } ;
    if (!readNextEvent()) {
      if (LOOP_PLAYBACK) {
        restartPlayback();
      } else {
        stopPlay();
      }
      return;
    }
    elapsed = millis() - playbackStartMillis;
 }
}

in general it kind of works except that not all events get a timestamp and sometimes note-on and note-off are reversed, so everytime i record a little melody or sequence it plays back something that uses the same notes but sounds completely different. Although i think it's interesting to hear the teensy's creative vision on the things i want it to play, I would prefer it to just play the things i recorded. I added some serial print functions to show what it's doing with the input and outputs and this is the result:
Code:
record mode
Note On, ch=1, note=29, velocity=127
, time=984
Note Off, ch=1, note=29, velocity=0
, time=173
Note On, ch=1, note=31, velocity=127
Note Off, ch=1, note=31, velocity=0
, time=172
Note On, ch=1, note=33, velocity=127
, time=344
Note Off, ch=1, note=33, velocity=0
Note On, ch=1, note=35, velocity=127
Note Off, ch=1, note=35, velocity=0
recording stopped
playmode
type=144channel=1, note=29, velocity127
, time=984
type=128channel=1, note=29, velocity0
, time=1157
type=128channel=1, note=31, velocity0
, time=1329
type=144channel=1, note=33, velocity127
, time=1673
Seems to me not every event gets assigned a timestamp so it's no surprise that certain messages don't get played back, where could the problem lie?
I tried multiple things, like adding the data to the struct in the handler function:
Code:
void handleNoteOff(byte channel, byte note, byte velocity) {
  MIDI.sendNoteOff(note, velocity, channel);
  Ev.type = midi::NoteOff;
  Ev.data1 = note;
  Ev.data2 = velocity;
  Ev.channel = channel;
i also tried putting the timer function into the handler, different structs for note on and off, different ways of saving the data (first as bytes, then as midi::dataByte al though these seem like cosmetic details to me) and a lot of other stuff but to no avail. I must admit i know more about music than software and most of this code was written in about half an hour by an acquaintance who is a software engineer but does not know that much about the specific libraries for the teensy, it makes me wonder if we've reinvented the wheel in a way that is not exactly optimal?
 
Post full code that can be compiled. There's too much logic missing to assume anything, but I would guess you have a mistake handling events that happen on the same millisecond.
 
Creative vision from the Teensy. I like that.

You could also use ChatGPT to let it explain and analysze the code for you. Possibly it can find the the problem quick if you ask and describe it.
 
@jmarsh here it is
Code:
#include <MIDI.h>
#include <SD.h>
#include "USBHost_t36.h"
MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI);

USBHost myusb;
USBHub hub1(myusb);
USBHub hub2(myusb);
USBHub hub3(myusb);
USBHub hub4(myusb);
MIDIDevice midi2(myusb);

const uint8_t PIN_RECORD = 3; // Record Button
const uint8_t PIN_PLAY = 2; // Play button
const uint8_t PIN_STOP = 4; // stop button
const uint8_t PIN_LED = 13; // Status LED
const uint8_t SD_CHIP_SELECT = BUILTIN_SDCARD; // Teensy 4.1 interne SD

const uint16_t DEBOUNCE_MS = 10; // Debounce time for buttons
const bool BUTTON_ACTIVE_LOW = true; 
const bool LOOP_PLAYBACK = true; // 
const char FILE_NAME[] = "temp.bin"; //  file with recorded events

// comment below is an attempt at making different structs for note on and off but it didn't solve much for me
//struct DatNoteON {
 // const bool type = true;
 // midi::DataByte data1;
 // midi::DataByte data2;
 // midi::DataByte channel;
//};
//DatNoteON EvNO;
//struct DatNoteOff{
  //const bool type = false;
  //midi::DataByte data1;
  //midi::DataByte data2;
  //midi::DataByte channel;
//};
//DatNoteOff EvNof;

//const byte ANO1 = 0xB0;  //all note off message, WIP
//const byte ANO2 = 0x7B;

struct MidiEvent { // Delta-time+ raw MIDI data
  uint32_t delta_ms;
  midi::MidiType type ;
  midi::DataByte data1;
  midi::DataByte data2;
  midi::Channel channel;
};
//the commented structs below were another attempt at different structs for differrent events
//MidiEvent EvNO;
//MidiEvent EvNof;
MidiEvent Ev;
struct ButtonDebounce { // Simple debounce without external library
  uint8_t pin;
  bool stableState;
  bool lastRead;
  uint32_t lastChangeMs;
  bool pressedEdge;
  bool prevPressed;

  void begin() {
    bool current = digitalRead(pin);
    stableState = current;
    lastRead = current;
    lastChangeMs = millis();
    prevPressed = BUTTON_ACTIVE_LOW ? current : !current;
    pressedEdge = false;
  }

  void update() {
    pressedEdge = false;
    bool current = digitalRead(pin);
    if (current != lastRead) {
      lastRead = current;
      lastChangeMs = millis();
    }
    if ((millis() - lastChangeMs) >= DEBOUNCE_MS && current != stableState) {
      stableState = current;
    }
    bool pressed = BUTTON_ACTIVE_LOW ? stableState : !stableState;
    if (pressed && !prevPressed) {
      pressedEdge = true;
    }
    prevPressed = pressed;
  }

  bool risingEdge() const {
    return pressedEdge;
  }
};

ButtonDebounce recButton{PIN_RECORD};
ButtonDebounce playButton{PIN_PLAY};
ButtonDebounce stopButton{PIN_STOP};

enum Mode {
  MODE_IDLE, // Waits for play or record button
  MODE_RECORD, // record
  MODE_PLAY // play
};

Mode mode = MODE_IDLE;
bool sdReady = false;

File recordFile;
File playFile;

uint32_t lastEventMillis = 0; // last timestamp during recording
MidiEvent nextEvent; // next event during playback
uint32_t playbackStartMillis = 0; // Start time playback
uint32_t playbackElapsedAtNext = 0; // time (ms) until next event
bool hasNextEvent = false; // checks if next event is ready

void handleNoteOn(byte channel, byte note, byte velocity) {
  MIDI.sendNoteOn(note, velocity, channel);
//  Ev.type = midi::NoteOn;  //here i tried to record the notes into the struct during the handler functions
//  Ev.data1 = note;
//  Ev.data2 = velocity;
//  Ev.channel = channel;
//below some print functions so i can see what's going wrong (one suspicion is that these might slow down the process?)
  if (mode == MODE_RECORD) {
   Serial.print(", time=");
   Serial.print (Ev.delta_ms);
  };
  Serial.println();
  Serial.print("type=");
  Serial.print(midi2.getType());
    Serial.print(" ch=");
    Serial.print(channel);
    Serial.print(", note=");
    Serial.print(note);
    Serial.print(", velocity=");
    Serial.print(velocity);

}

void handleNoteOff(byte channel, byte note, byte velocity) { //basically identical to the other handler function
  MIDI.sendNoteOff(note, velocity, channel);
 // Ev.type = midi::NoteOff;
 // Ev.data1 = note;
 // Ev.data2 = velocity;
 // Ev.channel = channel;
  if (mode == MODE_RECORD) {
    
    Serial.print(", time=");
    Serial.print (Ev.delta_ms);
  };
  Serial.println();
  Serial.print("type=");
  Serial.print(midi2.getType());
    Serial.print(" ch=");
    Serial.print(channel);
    Serial.print(", note=");
    Serial.print(note);
    Serial.print(", velocity=");
    Serial.print(velocity);
}
void startRecord();
void stopRecord();
void recordTick();
void startPlay();
void stopPlay();
void playTick();
bool readNextEvent();
void restartPlayback();

void setup() {
  uint8_t buttonMode = BUTTON_ACTIVE_LOW ? INPUT : INPUT_PULLUP; // Kies input type
  pinMode(PIN_RECORD, INPUT_PULLUP);
  pinMode(PIN_PLAY, INPUT_PULLUP);
  pinMode(PIN_STOP, INPUT_PULLUP);
  pinMode(PIN_LED, INPUT_PULLUP);
  digitalWrite(PIN_LED, LOW);
  delay(1500);
  myusb.begin();
  Serial.begin(115200);
  MIDI.begin(MIDI_CHANNEL_OMNI);
  midi2.setHandleNoteOn(handleNoteOn);
  midi2.setHandleNoteOff(handleNoteOff);

  sdReady = SD.begin(SD_CHIP_SELECT); // Teensy 4.1 SD init
  if (!sdReady) {
    Serial.println("SD init failed");
  }


  recButton.begin();
  playButton.begin();
  stopButton.begin();
}

void loop() {
  myusb.Task();
    midi2.read();
  recButton.update();
  playButton.update();
  stopButton.update();
 
  bool recPressed = recButton.risingEdge(); // Alleen bij nieuwe druk
  bool playPressed = playButton.risingEdge();
  bool stopPressed = stopButton.risingEdge();

  switch (mode) {
    case MODE_IDLE:
      if (recPressed) {
        startRecord();
      } else if (playPressed) {
        startPlay();
      }
      break;
    case MODE_RECORD:
      recordTick();
      if (stopPressed) {
        stopRecord();
      } else if (playPressed) {
        stopRecord();
        startPlay();
      }
      break;
    case MODE_PLAY:
      playTick();
      if (stopPressed) {
        stopPlay();
      } else if (recPressed) {
        stopPlay();
        startRecord();
      }
      break;
  }
}

void startRecord() {
  if (!sdReady) {
    return;
  }

  if (recordFile) {
    recordFile.close();
  }

  SD.remove(FILE_NAME); // removes old recording
  recordFile = SD.open(FILE_NAME, FILE_WRITE);
  if (!recordFile) {
    Serial.println("Record open failed");
    
    //veranderen naar ledknipper
    return;
  }
  Serial.println("record mode");
 
  mode = MODE_RECORD;
  lastEventMillis = millis(); // starting point for delta times 
}

void stopRecord() {
  if (recordFile) {
    recordFile.flush();
    recordFile.close();
  }
  Serial.println("recording stopped");
  mode = MODE_IDLE;
  digitalWrite(PIN_LED, LOW);
}

void recordTick() {
  if (!recordFile) {
    return;
  }

  while (midi2.read()) { // Lees alle pending USB MIDI berichten
    uint32_t now = millis();
   // MidiEvent evNoteOn; //another attempt at recording different events to diffent structs
    Ev.delta_ms = now - lastEventMillis; // time passed since last event
    lastEventMillis = now;
    Ev.type = midi2.getType();
    Ev.data1 = midi2.getData1();
    Ev.data2 = midi2.getData2();
    Ev.channel = midi2.getChannel();
  
    recordFile.write((const byte *)&Ev, sizeof(Ev));
 
    //MidiEvent evNoteOff;  //
   // EvNof.delta_ms = now - lastEventMillis;
   // lastEventMillis = now;
   // recordFile.write((const byte *)&EvNof, sizeof(EvNof));
   // Serial.print(", time=");
   // Serial.print (EvNof.delta_ms);
   // Serial.println ();
  }
  //while (midi2.read(midi::NoteOff)) {// more leftovers
    //recordFile.write((const byte *)&Ev, sizeof(Ev));
    //Serial.print(", time=");
    //Serial.print (Ev.delta_ms);
  //}
}

void startPlay()  {
  if (!sdReady) {
    return;
  }

  if (playFile) {
    playFile.close();
  }

  playFile = SD.open(FILE_NAME, FILE_READ);
  if (!playFile || playFile.size() < (int)sizeof(MidiEvent)) {
    if (playFile) {
      playFile.close();
    }
    return;
  }

  playbackElapsedAtNext = 0;
  if (!readNextEvent()) {
    stopPlay();
    return;
  }
  Serial.println("playmode");
  playbackStartMillis = millis();
  mode = MODE_PLAY;
  digitalWrite(PIN_LED, LOW);
}

void stopPlay() {
  if (playFile) {
    playFile.close();
  };
  Serial.println("playing stopped");
  hasNextEvent = false;
  mode = MODE_IDLE;
  //MIDI.send(byte ANO1, byte ANO2): // sending all notes off, WIP
  digitalWrite(PIN_LED, LOW);
}

bool readNextEvent() {
  if (playFile.readBytes((char *)&nextEvent, sizeof(nextEvent)) != sizeof(nextEvent)) {
    hasNextEvent = false;
    return false;
  }

  playbackElapsedAtNext += nextEvent.delta_ms; // Accumuleer naar absolute tijd
  hasNextEvent = true;
  return true;
}

void restartPlayback() {
  if (!playFile.seek(0)) {
    stopPlay();
    return;
  }

  playbackElapsedAtNext = 0;
  if (!readNextEvent()) {
    stopPlay();
    return;
  }

  playbackStartMillis = millis(); // new starting point loop


void playTick() {
  if (!playFile) {
    Serial.println("no file found");
    return;
  }

  if (!hasNextEvent) {
    if (LOOP_PLAYBACK) {
      restartPlayback();
    } else {
      stopPlay();
    }
    return;
  }

  uint32_t elapsed = millis() - playbackStartMillis; // playback time
  while (hasNextEvent && elapsed >= playbackElapsedAtNext) {
   if (nextEvent.type == 144) {                         //
   MIDI.sendNoteOn(nextEvent.data1, nextEvent.data2, nextEvent.channel);
// more print functions for troubleshooting  
  Serial.println();
    Serial.print("type=");
    Serial.print(nextEvent.type);
    Serial.print("channel=");
      Serial.print(nextEvent.channel);
      Serial.print(", note=");
      Serial.print(nextEvent.data1);
      Serial.print(", velocity");
      Serial.print(nextEvent.data2);
    Serial.print(", time=");
    Serial.print (elapsed);
  
    } else if (nextEvent.type == 128) {
    MIDI.sendNoteOff(nextEvent.data1, nextEvent.data2, nextEvent.channel);
    Serial.println();
    Serial.print("type=");
    Serial.print(nextEvent.type);
    Serial.print("channel=");
      Serial.print(nextEvent.channel);
      Serial.print(", note=");
      Serial.print(nextEvent.data1);
      Serial.print(", velocity");
      Serial.print(nextEvent.data2);
    Serial.print(", time=");
    Serial.print (elapsed);
  
    } ;
    if (!readNextEvent()) {
      if (LOOP_PLAYBACK) {
        restartPlayback();
      } else {
        stopPlay();
      }
      return;
    }
    elapsed = millis() - playbackStartMillis;
 }
}
 
Code:
void loop() {
  myusb.Task();
    midi2.read();

This extra call to midi2.read() is handling events outside of the recordTick() function, so they're not getting recorded.
 
Code:
void loop() {
  myusb.Task();
    midi2.read();

This extra call to midi2.read() is handling events outside of the recordTick() function, so they're not getting recorded.
Thanks for the explanation this fixed everything, i put this call in the switch under case MODE_IDLE (so it reads and relays the messages in idle mode too) and now it all works as intended. Can’t quite express the relief after messing around for about 4 days but i will say i respect this ability to easily find bugs a lot!
 
the next thing i want to implement is a pulse out to trigger envelopes or sample and hold modules. For this i wrote a struct that divides the length of the recorded sequence into 4 bars which form the "duty cycle " of the pulse being sent out. this cycle gets divided into 4 beats, the length of one beat being the time the pulse is On and the length of the beat subtracted from the length of the dutycycle being the time the pulse is off. it seems to work when putting 8000 ms for the length of the sequence and 4 bars with 4 beats, however when i try to change any of these values it will either remain in the on or off state. the code is just a modified blink without delay example sketch and the output is a led that shines on 2 ldrs connected to a 9v battery (the slow reaction time of the ldrs actually smoothes out the pwm pulses pretty well so it saves me from building a whole smoothing filter plus amplifier combination to boost these signals). can anybody see what's wrong?
Code:
const uint8_t PIN_SYNC = 6; // pulse out

int syncState = 0;


unsigned long previousMillis = 0;  // will store last time pulse was changed

struct ClockPulse {
 uint32_t stopTime = 8000; //length of the recorded sequence
  const uint32_t bars = 4;  // time division
  const uint32_t beats = 4;
  uint32_t pulseCycle = stopTime / bars; //pulse cycle for now will be one bar assuming 4 are recorded
  uint32_t pulseOn = pulseCycle / beats; //length of pulse to be sent
  uint32_t pulseOff = pulseCycle - pulseOn;  // time pulse remains off
  //
 
  };
ClockPulse Quarters;
void setup() {
  // set the digital pin as output:
  pinMode(PIN_SYNC, OUTPUT);
 

}

void loop() {
  unsigned long currentMillis = millis();
  if (syncState == 0) {
    if (currentMillis - previousMillis >= Quarters.pulseOff) {
      
      previousMillis += Quarters.pulseOn;
      syncState = 255;
      
    } else {
      syncState = 0;
    }
  } 
  else if (syncState == 255); {
    if (currentMillis - previousMillis >= Quarters.pulseOn) {
    //
      previousMillis += Quarters.pulseOff;
      syncState = 0;
    } else {
      syncState = 255;
      }
  }
    // set the LED with the ledState of the variable:
    analogWrite(PIN_SYNC, syncState);
    
    
 
}
 
Shouldn't it be the other way round with
Code:
 ...+= Quarters.pulseOn;
and
Code:
... += Quarters.pulseOff;
 
Shouldn't it be the other way round with
Code:
 ...+= Quarters.pulseOn;
and
Code:
... += Quarters.pulseOff;
Yes eventually the led being high would result in the voltage over the LDR going low so the led should be high during the pulseOff part of the duty cycle but this is just a first draft to test if the basic logic works and i decided to test it like this
 
i wrote this little sketch to test the different resistances and the amount of pwm noise at different frequencies (sorry for the weird commands)
and built a simple voltage divider that scales it to 0-6v, a level that my synth can process, the LDR's are soldered together in series and folded around the LED like little headphones and the different pwm signals translate to a nice range of cv for my fake MS20. it's pretty cool to type "high" and hear the filter levels change accordingly. typing high and low also works for triggering the sample and hold circuit which i eventually want controlled in a rhythmic fashion by my clockstruct program. when i run my clockstruct program i put the same values in (high becomes 255, low becomes 0,) and i put the same pwm frequency. however when i run the clockstruct program, the voltage level only drops from 6 to 5v and the signal on my oscilloscope looks considerably more noisy (especially in the low period), where could this lie?
Code:
#define Full 256
#define High 255
#define Dacht 224
#define Dkwart 192
#define Dmid 159
#define Mid 127
#define Macht 96
#define Kwart 64
#define Achtst 32
#define Zest 16
#define Low 1
#define Off 0
const uint8_t PIN_SYNC = 6; // pulse out


int syncState = Low;

void setup() {
  Serial.begin(9600);
  analogWriteFrequency(PIN_SYNC, 10000);
}
void loop()  {
  if (Serial.available()) {
    String command = Serial.readStringUntil('\n');
    if (command == "low") {
      syncState = Low;
    } else if (command == "mid") {
      syncState = Mid;
    } else if (command == "high") {
      syncState = High;
    } else if (command == "off") {
      syncState = Off;
    } else if (command == "dkwart") {
      syncState = Dkwart;
    } else if (command == "kwart") {
      syncState = Kwart;
    } else if (command == "dacht") {
      syncState = Dacht;
    } else if (command == "dmid") {
      syncState = Dmid;
    } else if (command == "macht") {
      syncState = Macht;
    } else if (command =="achtst")  {
      syncState = Achtst;
    } else if (command == "zest") {
      syncState = Zest;
    }
      else if (command == "full") {
     syncState = Full;
    }
  analogWrite(PIN_SYNC, syncState);
  Serial.println(command);
  }
}

now here is my clock generator program

Code:
#define High 255
#define Low 0
const uint8_t PIN_SYNC = 6; // pulse out

int syncState = High;


unsigned long previousMillis = 0;  // will store last time pulse was changed

struct ClockPulse {
 uint32_t stopTime = 10000; //length of the recorded sequence
  const uint32_t bars = 4;  // time division
  const uint32_t beats = 4;
  uint32_t pulseCycle = stopTime / bars; //pulse cycle for now will be one bar assuming 4 are recorded
  uint32_t pulseOn = pulseCycle / beats; //length of pulse to be sent
  uint32_t pulseOff = pulseCycle - pulseOn;  // time pulse remains off
  //
 
  };
ClockPulse Quarters;
void setup() {
  // set the digital pin as output:
  Serial.begin(9600);
  pinMode(PIN_SYNC, OUTPUT);
  analogWriteFrequency(PIN_SYNC, 10000);
  Serial.println(Quarters.pulseCycle);
  Serial.println(Quarters.pulseOn);
  Serial.println(Quarters.pulseOff);

}

void loop() {
  unsigned long currentMillis = millis();
  if (syncState == High) {
    if (currentMillis - previousMillis >= Quarters.pulseOn) {
      previousMillis += Quarters.pulseOff;
      syncState = Low;
      
    } else {
      syncState = High;
    }
  } 
  else if (syncState == Low) {
    if (currentMillis - previousMillis >= Quarters.pulseOff) {
      previousMillis += Quarters.pulseOn;
      syncState = High;
    } else {
      syncState = Low;
      }
  }
    // set the LED with the ledState of the variable:
    analogWrite(PIN_SYNC, syncState);
    
    
 
}
 
Back
Top