Loop to slow for MIDI library read() method

Hitachii

Active member
Hello again everybody!

I'm using callbacks from the MIDI library found here.

There is an issue. I have an SSD1306 i2c OLED using the Adafruit_SSD1306 library that causes my loop to go from ~1ms to >30ms when it is being updated. This interferes greatly with my incoming MIDI clock signal. I can tell because I have MIDI clock simply turning the builtin LED on and off, and when performing an event which updates the OLED (turning a pot), the LED blinking slows significantly.

I've asked on Reddit how to handle this, and the only way that people have concluded to fix this is to alter the Serial library directly in the Arduino code, to have it trigger an interrupt when a MIDI message is received. This seems a bit more complicated than it should be, and I've certainly seen Arduino MIDI projects using an OLED screen elsewhere (i2c, even), so I figured that I would ask here to get a second opinion.

I was told that putting the read() method inside a timer-based ISR would be useless because I would be missing MIDI messages as they arrive. Does this sound true? If it's not true, what frequency should the timer be to receive messages accurately?

In case this helps, I tried optimizing my OLED by only updating a portion of the screen instead of clearing the display and then updating it. There was no noticeable improvement of speed, as a loop still took ~30 millis.

Ultimately, my question is: Is there a way to schedule the MIDI read() method so that it accurately reads incoming messages, if my loop is slow? Thank you!
 
You wont loose MIDI Data, if you have the read function on an interrupt. If it would be that way, you would loose MIDI Messages during the Display update too...
You just need to make sure, that your choose an interval, that does not introduce more latency, than you want to have.
Another thing: Make sure, that you need more than a single message.

MIDI.read() does not read as many midi messages that are waiting. It reads half a midi message per call. Thats why in the examples there is a while(MIDI.read()); The while is on your side and not in the library itself is because sysex messages can be longer to read as you want the app to freeze in IO. So read as many MIDI messages per interval as you can, to not have something like a strum effect, if you would want to hear a chord...

timing the reading of midi gets really tricky, as soon as you want to sync yourself to the clock (for delay and other effects, modulation etc.), as even a little difference in latency might mess up your tempo calculation.

I am building a sequencer. doing tight midi is way more easy there, as I have a constant interval handling sending, and by sending the clock first, I generate a amazingly tight midi clock.
 
Keep MIDI.read() in your loop and work more on optimising your OLED code. That's where the problem is.

If updating a portion of the screen only instead of a clear and full update makes no difference, you must be overlooking something.

Also, does it really matter if you are slow reading a clock message? There are 24 of them every beat. Maybe you can accept some latency reading the first few, or clocks 1,2,3 and 13, 14, 15 say, and use that time to update the OLED?
 
Last edited:
First, try using 1 MHz clock speed. Adafruit's library defaults to 400 kHz, so this should give you about a 2.5X speedup.

If you started with Adafruit's example, you probably have this in your program:

Code:
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

Fortunately Adafruit designed it to take a 5th parameter for the clock speed, like this:

Code:
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET, 1000000);

Usually these displays can work at 1 MHz, but you might need to add 1K pullup resistors on SDA & SCL. If the display has resistors like 10K or even 4.7K, the signals might be able to change fast enough for 1 MHz speed.

Now for some guessing about your code.... Maybe you're updating the display for every MIDI message? If so, only call the drawing functions but do not call the slow display.display() function.

Instead, use an elapsedMillis in loop() to cause the display to update at a controlled rate. Maybe like this:

Code:
void loop() {
  // other stuff your program does, like receiving MIDI messages

  static elapsedMillis display_update_timer;
  if (display_update_timer > 50) {
    display_update_timer = 0;
    display.display();  // slow display update
  }
}

This way you program will be able to process all the MIDI messages (the drawing functions are very fast since they only alter Teensy's memory) and only every 50ms spend time updating the display over slow I2C communication.

You could try to use interrupts or IntervalTimer, but interrupts can be a very painful path when you need to share data. Really do not recommend.

In an ideal world, we would have a non-blocking DMA based library like Kurt's ILI9341_t3n. Then the display update happens in the background without blocking the CPU. Sadly, it just doesn't exist (yet) for this popular display using I2C. If you're not fully locked into this SSD1306 display, and if you can spare the pins and fit a large screen into your project, moving to a ILI9341 display might be a good choice. ILI9341_t3n really is the best way.
 
Last edited:
Keep MIDI.read() in your loop and work more on optimising your OLED code. That's where the problem is.

If updating a portion of the screen only instead of a clear and full update makes no difference, you must be overlooking something.

Also, does it really matter if you are slow reading a clock message? There are 24 of them every beat. Maybe you can accept some latency reading the first few, or clocks 1,2,3 and 13, 14, 15 say, and use that time to update the OLED?

I don’t know how fast the display is to render, but I had the issue my sequencer, that even the highly speed optimized ili9341_t4 api, that used DMA etc. was not fast enough to fit into a midi pulse. The ssd 1306 libriaries I tested were way worse in performance :(

Modern midi sequencing does not have that high of a resolution, because they can write down large numbers in their marketing sheet, but because it is needed. Latency in music is very noticeable, jitter in MIDI is one of the worst things you can have. Loosing half a ms on a tone might be okay, but not being able to exactly determine the speed of the song by having jitter on the midi clocks means, that you have really bad artifacts on effects like delay and reverb or in worst case, your FX is not in sync with the music. The biggest example for this is Waldorfs Blofeld. The MIDI syncing is “so bad” that their FX are unusable, if you want to sync it to a beat :(
I tried to move around my performance issues with the display by going down to 48ppqn (that’s double the clock speed) and it was awfull, if you want to do more than simple 4 on the floor. I am okay with my 192ppqn now, but I totally see (and hear) why Akai goes with double that resolution on their MPCs. and when sequencers need such a resolution, sound sources need to be able to handle that to fit into such a setup.
 
- The solution would be a good multitasking. Sadly, there is no easy to use way for this.
you could move some code to an interrupt, but this requires a rewrite of large portions of existing code.
- I2C is order of magnitudes slower than SPI.
Use the SPI ILI9341 Display, with full screen buffer. This transfers the display data via DMA and should not influence the timing that much.
Speeding up I2C does not help. 1MHZ is only 2.5 times faster. And the i2c code is blocking ( in 2022...)
 
Thanks everyone. Before anyone answered I tried a few things:

- Making a sketch for the OLED by itself without 'clearDisplay()', and having the display show the value of one potentiometer on the screen and nothing else. The value wrote over itself using the 'WHITE,BLACK' text color setting. This took roughly the same amount of time as it does with a screen full of values. The difference was negligible, really. Paul, I have yet to try the 1MHZ speed though. That seems promising.

- I made a timer going at different speeds, up to 40khz just to see what would happen. Using the method of comparing the micros between ticks, it varied wildly up to 1 bpm in either direction. Some BPMs were more stable than others. Strangely, changing the timer frequency did not affect the clock discrepancy of intervals above 3khz. It looks like wild jitter but I use my external clock source for other instruments all the time and it's fairly stable.

Since this is a sequencer, and the internal sequencing is rock solid, I've decided just to receive transport signals for now, and sync up the tempos manually. Next time I think I'm going to use an entirely different MCU for handling the OLED since it wants to be like that! I'll probably opt to use SPI as well :).
 
Hey!

I can write down, how I do it, maybe there is some inspiration for you:

i have 2 ways to execute code: (in the written down order)

1. the main loop:
It reads the input; processes the input, updates data-structures etc. this also does SD Card work, If I execute load or save of the project
It checks if data was changed, that effects the display. If so, it updates the Framebuffer
It checks if data was changed, that effects the RGB LEDS. If so, it updates them using Paul’s DMA library
It calls the Update function on the display.

2. The interrupt timer
it has a very high priority, as this is handling the MIDI Sequencing
It is timed on 6000000/BPM/192 microseconds if I use internal clock, or on the evaluated time form incoming time with the external clock message
Every time the BPM value changes, the interval is updated on the timer.
It sends the clock if needed
It ticks the “midi player” that handles the voices (to send a note off if the duration of the note is reached) (that’s just some cpp object)
It checks the modulation every pulse and sends a CC Message if the target is CC and the value is different to the last one.
It checks all tracks, in the current bar, the current step if the offset matches the current pulse. If so the note is prepared
before the note is played, a lot of calculation is done, for modulation on note relevant values, propability, special features like chord modes, midi FX etc.
The arpeggiator is ticked
All evaluated note events are passed to the “midi player” that handles the voices, does slides and sends the noteOn to the midi port
It progresses the sequencer by one pulse and checks if pattern ended, checks if a new pattern has to be queued, if so, if there is a program change that need to be send and if so, does it etc.
It reads as many incoming midi events as possible before the duration would exceed the time until the next pulse.
for MIDI Reading I iterate through all MIDI Ports (4 USB, 5 DIN) and read one Message each, to not priorities one over the other. As there is mostly only notes or the clock coming, I think Most of the time, all waiting messages are handled before the pulse ends.
If the midi sequencer is not playing, the interrupt is still running, but is skipping the modulation and sequencer part. That way the still „playing“ notes are stopped, when they should end (if not forces to be off) and the incoming midi is still read and handled.
So I priorities my midi sequencing over incoming midi, but still handle midi in the same 192ppqn Resolution.

With that implementation I am even able to save a big project (project is around 6MB big, if “completely” filled) onto the SD card, while the sequencer is playing without any interruption of the MIDI tightness. as I have 16MB psram available, I am currently working on loading in a second project to be able to switch them quantized, so playing more than one track is nice. Currently I can load a project while the sequencer is playing and the sequencer just playes the new data. It showed, that it is no performance issue, but timing it perfectly should be done by the Maschine :)

I must admit, that I use my sequencer as midi clock 99,9% of the time, but my tests with recording midi notes didn’t show much problems with latency. The only thing I have this way is, that my sequencer lays back around 1 pulse, as the clock and transport messages are read just at the end of the pulse. If I would use external clocks more often (non of my hardware have such tight midi clocks as my own sequencer :-( and I built the sequencer, because no sequencer I was able to put my hands on, did. what I wanted, so I just ignore all sequencing on the other devices ); I would move the read to the top of the interrupt, or make this configurable in the options menu.
 
Last edited:
Back
Top