MIDI Master Clock and Transport

Status
Not open for further replies.

garcho

Member
I want to make a MIDI master clock to sync a number of devices to one tempo and control their individual start/stop functions.

I only have 2 arms to start or stop more than 2 devices, and I don't want to have one sequencer start or stop the entire MIDI chain. So what I envision is to dial in a BPM with an encoder, displayed on an OLED screen, and have start/stop buttons for 8 different MIDI DIN outputs. Because I'm only interested in sending System Messages, using one TX and sending multichannel data won't work. Everything is DIN, no USB. Therefore, I believe, I'll need to use multiple serial TX, like on the Teensy 4.1, which has 8. Perhaps I'm wrong, I'm a newb! So the big questions are:

How to sync multiple serial outputs? Is it possible to run 8 serial outputs that will produce 8 MIDI clock signals that are basically in sync with each other? MIDI is 31250 while Teensy 4.1 is 600MHz, so serial sync might not matter? Is that a correct assumption? So, if I have something like an interrupt that includes sending MIDI clock messages to all serial outputs once every 1/24 of a beat, I can forget about each TX being microscopically off from each other? Are there any other concerns I haven't thought of? A million?

How to sync button presses? Let's say there are 8 start/stop buttons, one for each TX. And let's say, for instance, I want a drum machine and sequencer to start or stop exactly together, so I try to simultaneously press the buttons that correspond to their respective TX outputs and the System Messages are sent at the same time. But time for MIDI is measured in 1/31250ths, right? So how to make sure it is the exact same time and not say, a 1/32 or 1/64 note off? Is there a way I can make sure start and stop messages only go out on say, 1/4 notes?
One half-baked idea I had: Let's say once every 24 clock pulses, 8 EEPROM addresses are read. And in the loop, there are 8 buttons being read. When button1 is pressed, a "1" is written to EEPROM(1). At the next conclusion of the 24 pulse cycle, when EEPROM is read, if there is a "1" in address 1, then send a start/stop message along TX1. I guess you'd have to keep track of start/stop state so next time the button is pressed, the opposite message is sent. Then rewrite EEPROM(1) as a "0", so no messages are sent until the next time button1 is pressed.
To be able to change the duration variable to be 1/8, 1/4, 1/2, 1/1, etc., would be ideal. Kind of like the LED blinking, I was thinking if Teensy was counting MIDI clock pulses, you could % by 12 for 1/8 notes, or 24 for 1/4, 48 for 1/2, or 96 for 1/1. Then you could for instance, set the start/stop to go out on whole notes, leisurely hit buttons 3, 4, and 7 within a whole note's duration, and next time the 96 pulse cycle concludes, the stop/start messages go out on TX 3, 4, and 7. That would be great for performing.
Does that make sense? Would something like that work? I'm completely inexperienced with using EEPROM. What's a better way of doing it? I haven't tried this out at all yet. Is there a way to count MIDI clock pulses? Like, each time the MIDI clock interrupt is called, part of the function is to ++ a counter? Then modulo the int and every time it == 0 send out the call to read EEPROM?

Here is where I'm at, code posted below. It's based on an Arduino Uno MIDI master clock code by Eunjae Im. It's just two outputs for now, to make it easier to read. Right now it basically works well, but I really want TX1 and TX2 (and 3-8, obviously) to sync up to the 1/4 (or 1/8, 1/2, 1/1) note flawlessly.

Thanks for your time, and for reading this long post. I have some electronics experience in the analog realm but my only coding experience is via Arduino and it's limited. Let me know what else I can do to make any help I might receive easier to give. Cheers y'all!

Code:
#include <Bounce2.h>
Bounce debouncer1 = Bounce();
Bounce debouncer2 = Bounce();

#include <Adafruit_SSD1306.h>
#define OLED_RESET 4
Adafruit_SSD1306 display(OLED_RESET);

#include <TimerOne.h>
#include <Encoder.h>

#define LED_PIN1 2
#define LED_PIN2 3

Encoder myEnc(22, 23); 

const int button_start1 = 9; 
const int button_start2 = 10; 

volatile int blinkCount1 = -1; 

#define BLINK_TIME 12

#define MIDI_START 0xFA
#define MIDI_STOP 0xFC
#define MIDI_TIMING_CLOCK 0xF8

#define CLOCKS_PER_BEAT 24
#define MINIMUM_BPM 20
#define MAXIMUM_BPM 300

volatile unsigned long intervalMicroSeconds;

int bpm;

bool playing1 = false;
bool playing2 = false; 

bool display_update = false;

void setup(void) {

  debouncer1.attach(button_start1, INPUT_PULLUP);
  debouncer1.interval(5);
  debouncer2.attach(button_start2, INPUT_PULLUP);
  debouncer2.interval(5);
  
  Serial1.begin(31250); 
  Serial2.begin(31250); 
  
  bpm = 120;
   
  Timer1.initialize(intervalMicroSeconds);
  Timer1.setPeriod(60L * 1000 * 1000 / bpm / CLOCKS_PER_BEAT);
  Timer1.attachInterrupt(sendClockPulse);  

  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.setTextColor(WHITE); 
  display.setTextSize(3);
  display.setCursor(35,10);
  display.print(bpm);
  display.display();
}

void bpm_display() { 
  updateBpm();  
  display.clearDisplay();
  display.setTextSize(3);
  display.setCursor(0,0);  
  display.setTextColor(WHITE, BLACK);
  display.print("     ");
  display.setCursor(35,10);
  display.print(bpm);
  display.display();
  display_update = false;
}

int oldPosition;

void loop(void) {

  debouncer1.update();
  if (debouncer1.fell()) {
    startOrStop1();
  }

  debouncer2.update();
  if (debouncer2.fell()) {
    startOrStop2();
  }

  byte i = 0;
  long newPosition = (myEnc.read()/4);
  if (newPosition != oldPosition) {    
    if (oldPosition < newPosition) {
      i = 2;
    } else if (oldPosition > newPosition) {
      i = 1;
    }
    oldPosition = newPosition;
  }

  if (i == 2) {
        bpm++;
        if (bpm > MAXIMUM_BPM) {
          bpm = MAXIMUM_BPM;
        }
        bpm_display();          
      } else if (i == 1) {
        bpm--;
        if (bpm < MINIMUM_BPM) {
          bpm = MINIMUM_BPM;
        }
        bpm_display();
      }  
}

void startOrStop1() {
  if (!playing1) {
    Serial1.write(MIDI_START);
  } else {
    digitalWrite(LED_PIN1, LOW);
    Serial1.write(MIDI_STOP);
  }
  playing1 = !playing1;
}

void startOrStop2() {                   
  if (!playing2) {
    Serial2.write(MIDI_START);
  } else {
    digitalWrite(LED_PIN2, LOW);
    Serial2.write(MIDI_STOP);
  }
  playing2 = !playing2;
}

void sendClockPulse() {  
  Serial1.write(MIDI_TIMING_CLOCK);
  Serial2.write(MIDI_TIMING_CLOCK);

  blinkCount1 = (blinkCount1 + 1) % CLOCKS_PER_BEAT;
  
  if (playing1) {
    if (blinkCount1 == 0) {
      digitalWrite(LED_PIN1, HIGH);      
    } else {
      if (blinkCount1 == BLINK_TIME) {
        digitalWrite(LED_PIN1, LOW); }
    }
  } 
  
  if (playing2) {
    if (blinkCount1 == 0) {
      digitalWrite(LED_PIN2, HIGH);      
    } else {
      if (blinkCount1 == BLINK_TIME) {
        digitalWrite(LED_PIN2, LOW); } 
    } 
  }  
}   // end sendClockPulse

void updateBpm() { 
  long interval = 60L * 1000 * 1000 / bpm / CLOCKS_PER_BEAT;  
  Timer1.setPeriod(interval);
}
 
So, I made the buttons in the loop set flags to true/false, then the interrupt counts clock messages and the start or stop is triggered by % 24 (or 12, 48, 96 etc.) and reading the flag. I'll post the finished code for the whole project once it's built, works properly, and I clean up the code.
 
If I have one interrupt for all 8 UARTs, and each one sends a MIDI clock message one after the other like it's written in the code, then am I right in assuming the delay between UART 1 and UART 8 would only be 13ns? (1/600,000,000)*8
 
am I right in assuming the delay between UART 1 and UART 8 would only be 13ns? (1/600,000,000)*8

No, probably not a valid assumption. The exact timing behavior will depend on many fine details of the internal design of those UARTs, which NXP doesn't publish. But we do know some things. The UARTs run from 24 MHz, and they divide clock clock down to a multiple of the baud rate. We also know the code to write data into the UART buffers, and the interrupt code to transfer bytes between those buffers and the UART's FIFO also takes many instructions.

While these are all much more than 13 ns, they are very short compared to the 320,000 ns required to transmit a single byte at 31250 baud.

The only way to really know is the measure. So I set up a quick test, just for you (and anyone else who later files this thread) to check. The short answer is +/- 1 bit time.

First, here's the code I ran on a Teensy 4.1:

Code:
void setup() {
  Serial1.begin(31250);
  Serial2.begin(31250);
  Serial3.begin(31250);
  Serial4.begin(31250);
  Serial5.begin(31250);
  Serial6.begin(31250);
  Serial7.begin(31250);
  Serial8.begin(31250);
}

void loop() {
  Serial1.write(0xF8);
  Serial2.write(0xF8);
  Serial3.write(0xF8);
  Serial4.write(0xF8);
  Serial5.write(0xF8);
  Serial6.write(0xF8);
  Serial7.write(0xF8);
  Serial8.write(0xF8);
  delay(15);
}

My oscilloscope only has 4 channels, so I connected to TX1, TX2, TX7, TX8, so we can see the first 2 and the last 2 outputs.

scope.jpg
 
The next result is the timing varies by approx +/- 1 bit time, which is about 32 us at 31250 baud. Other very minor differences can also be seen. Here's a screen grab from the scope:

file.png

A seemingly strange result is Serial7 output happens before Serial1, even though it was written later. I believe what's happening here is the UARTs are actually running at some frequency, perhaps 500 kHz, which is 16 times the baud rate, though I didn't do anything to dig deeper in the actual settings used. So writes that happen very quickly in software are probably waiting until the next 2 us clock on each UART, and all 8 of them are not running perfectly in sync with each other. In this test, it looks like Serial7 happens to be getting it's divided clock slightly ahead of the others.

But this picture doesn't tell the whole story. While the waveforms line up like this most of the time, occasionally the flicker or jump quite a bit. Since I can't capture video of the on-screen movement, I'll turn on the screen persistence to try to show you the image.

file2.png

Here you can see the green waveform often starts 30 us before the yellow, and the blue and red occasionally (but rarely) start about 30 us later. Remember, the scope is triggering on the yellow waveform, which may also be occasionally different, which would show up as all 3 shifting.

So while there's a lot of fine details in the timing, the net result is the 8 outputs usually come out very close to each other, but occasionally the timing can vary by about 1 bit time at 31250 baud, or about 32 us.

For some perspective, the speed of sound is 343 meters per second in room temperature air at sea level pressure. So 32 us the amount of time sound travels about 1 cm.
 
Wow! Thank you for your time and very informative replies!

Like I said, I can't yet hear anything "off", though it's only on a breadboard and needs to be built properly and played with for a while to know for sure.

Any thoughts on a different way to have MIDI clock messages sent to multiple devices, while only sending MIDI system messages to some?
 
Status
Not open for further replies.
Back
Top