1
votes

Idea: A friend and I are building a metronome with an (Elegoo) Arduino-Mega which additionaly can send out a MIDI-Clock signal over the serialport. Blinking LED in sync with set BPM value, BPM-Control over rotary encoders and everything else works just fine. Only sending the MIDI signal over serial gives us headaches.

The Problem: The MIDI Clock signal (0xF8) needs to be sent 24 times on each beat. Therefore we simply calculated the time between the clock ticks and after the time interval is passed, we send a 0xF8 over Serial. Easy. But when we hooked it up to a Ditto X4 Guitar Looper the LED blinking of our metronome and that of the looper went out of sync. So we wrote a little script in C# .NET to verify what is being sent over Serial and it turns out, depending of the set BPM some Messages aren't being sent at all or are delayed which leads to the looper calculating a different BPM than we try to send out (Screenshot of script output).

But we are completely lost here. Why are some Messages delayed/not sent? Even on "normal" Baud rates like 9600 the Problem is the same. And it doesn't seem to scale with Arduino CPU usage or set BPM:

Set BPM:      Lost Message every x Messages:
  300                      24-26
  150                      10-12
  50                       4-5

We also tested an Arduino Uno R3 (also from Elegoo) but the Problem is the same.

This sample script can be used to replicate the Problem:

#include <Arduino.h> //Einbinden der Arduino Bibliothek

//Timer Variables
unsigned long startTimeMIDI = 0;
unsigned long currentTime = 0;
unsigned long intervalLED;
unsigned long intervalMIDI;

short counter_BPM = 300 * 2; // Internally we use BMP*2

void setup()
{
  Serial.begin(31250); //Forced by the MIDI standard
  while ( !Serial ) /*wait for serial init*/ ;
}

void loop()
{
  currentTime = micros();

  intervalLED = (120000000/counter_BPM); //60000000*(BPM/2)
  intervalMIDI = intervalLED/24; //Midi Clock has to be sent 24 times for each beat

  if (currentTime - startTimeMIDI > intervalMIDI){
    Serial.write(0xF8);    //send MIDI Clock signal
    startTimeMIDI = currentTime;  //reset timer value
  }
}

This is the C# script used to monitor what is sent:

static void Main(string[] args)
    {
        serial = new SerialPort("COM4", 31250);
        serial.Open();

        int cycleSize = 50; //Averaging over 50 Values

        long[] latencyList = new long[cycleSize+1];

        Stopwatch watch = new Stopwatch();
        watch.Start();

        int n = 0;
        while(true)
        {
            n++;
            watch.Start();

            int response = serial.ReadByte();
            watch.Stop();

            long latency = watch.ElapsedTicks/(Stopwatch.Frequency/(1000L*1000L));
            watch.Reset();

            if (n <= cycleSize)
            {
                latencyList[n] = latency;
            }
            else
            {
                latencyList[n % cycleSize] = latency;
            }

            double average = latencyList.Average();
            Console.WriteLine("" + n + " " + latency.ToString("000000") + "µs - response:" + response + " - Average: " + average.ToString("##0.00") + " - BPM: " + (int)(60000000/(average * 24)));

        }
}

EDIT: (May 9, 2020) I need to clarify the Problem with the guitar Looper: Since the looper is used to sync it's effects to the rest of the Band this is the most important Problem. The blinking of the Arduinos BPM LED (we tapped it and came close enough at a variety of set BPMs to consider it accurate enough) and the blinking of the Loopers LED drift away from each other too fast to be acceptable. We put the LEDs right beside each other and they go from being in sync to blinking alternately in the matter of ~30 seconds so in a live concert everything would fall apart. Since the loopers LED blinking is triggered by the MIDI input it receives, we looked at the consistency of the sent clock signals and discovered the odd delay between signals.

3
Have you though about using the MIDI libray? It might not solve your problem, but might be of some help when dealing with MIDI and Arduino. Do you also this problem if you are watching the output on the Arduino Serial terminal?Tom
Why do you have a semi-colon at the end of if (currentTime - startTimeMIDI > intervalMIDI){; ?Tom
Last one: when wiring, are you using decoupling like in the MIDI standard?Tom
@Tom_C Thanks for pointing that out. I fixed it, it was a simple copy-paste error. On the other Topic: Yes I already tried using the MIDI Library. But it turns out if you look at the source code the library does exactly the same thing concerning MIDI-Clock signals(sending 0xF8 over serial). Since we don't need any other MIDI functions this way it seems easier.TheStriker0815
Have you tried to debug it on ocilloscope or logic analyzer? Interfaces like Serial are heavily buffered on computers and its hard to check latencies on itKIIV

3 Answers

0
votes

The script computes the time for the next message relative to the time when the previous message was actually sent. This means that any delays will add up.

Instead, compute the next time as a fixed interval from when the last message should have been sent:

void setup()
{
  Serial.begin(31250); //Forced by the MIDI standard
  while ( !Serial ) /*wait for serial init*/ ;

  intervalLED = (120000000/counter_BPM); //60000000*(BPM/2)
  intervalMIDI = intervalLED/24; //Midi Clock has to be sent 24 times for each beat

  startTimeMIDI = micros() + intervalMIDI;
}

void loop()
{
  if (micros() >= startTimeMIDI) {
    Serial.write(0xF8);             //send MIDI Clock signal
    startTimeMIDI += intervalMIDI;  //next timer value
  }
}
0
votes

Why are some Messages delayed/not sent? Even on "normal" Baud rates like 9600 the Problem is the same. And it doesn't seem to scale with Arduino CPU usage or set BPM

This appears to be an issue with how you are measuring the time between messages.

The C# code uses the Stopwatch class which has this note:

On a multiprocessor computer, it does not matter which processor the thread runs on. However, because of bugs in the BIOS or the Hardware Abstraction Layer (HAL), you can get different timing results on different processors. To specify processor affinity for a thread, use the ProcessThread.ProcessorAffinity method.

My emphasis added from https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.stopwatch?view=netcore-3.1

So you may get different timing results when the console app switches processor cores during its execution.

You can avoid this issue by setting the processor affinity when the console app runs by using:

  • ProcessThread.ProcessorAffinity in code
  • using START and the /affinity flag
  • the Task Manager

Failing this, an alternate way to measure the period between messages would be an CRO/DSO or even using one Arduino to measure the output of another.

But when we hooked it up to a Ditto X4 Guitar Looper the LED blinking of our metronome and that of the looper went out of sync.

As for your actual application, it isn't clear from the question how quickly and by how much the LED and looper are out of sync.

There are several issues to consider to varying degrees:

  • micros() resolution is 4 µs. With integer rounding issues and polling delays the messages from the Arduino might be out by 4-8 µs per beat.
  • Arduino clock frequency accuracy. From the images on the Elegoo website, it appears that their boards use ceramic resonators which are less stable and accurate than crystals.

These differences are small in the grand scheme of things but more importantly irrelevant if the Arduino is the "conductor" sending out the clock signal. My guess is that the LED on the connected MIDI device is not deriving its frequency from the Arduino's MIDI clock signal.

0
votes

After more than a year, I'll finally answer my own question. In the meantime I had the possibility to use an oscilloscope to analyze the timing. As it turns out, the LED and the output of the serial port drift clearly visible apart. I don't exactly know why, the serial output of the Arduino is handled asynchronous to the cpu clock as far as I know, but I didn't expect such a significant drift.

The quite simple solution for our problem in particular was to simply switch from the native serial port to a software serial port. This way the cpu handles not only the blinking but also the serial communication and thus no drift can be seen on the oscilloscope.

As for the comments and debugging regarding the C# script, some were already right that such accurate timing doesn't work reliably with such a simple approach. The arduino did indeed not drop messages but "only" drift heavily.