MIDI Control of NeoPixels (was: MIDI Library Dropping Notes)

Hi all,

I'm working on a little project to control NeoPixels via MIDI (over regular serial USB). I'm using the latest Arduino MIDI Library (v4.3.1). Running on an Arduino Nano.

The data flow is from my DAW (FL Studio on Windows, if anyone cares) through loopMIDI to Hairless MIDI to the Nano.

I've run into an issue where incoming MIDI messages get dropped if too many arrive at one time. Limit seems to be ~4-5 notes - about 15 bytes. This is obviously a problem. (Seems to be a theme on the forum?)

Hairless MIDI shows all the MIDI messages going out, so the outgoing data is OK.

I've tried everything I can think of, based on forums posts I've read plus my own intuition.

This includes:

  • Using HardwareSerial and not SoftwareSerial
  • Increasing the RX buffer size as high as 1024 in HardwareSerial.h
  • Increasing the baud rate to 115200 in midiSettings.h
  • Tried both true/false for Use1ByteParsing in midiSettings.h - no effect either way
  • Made the handling code as lean as possible
  • Changing the optimization in platforms.txt to -O3 (optimize for speed), confirmed in the compiler output

I can get it to work by staggering the notes slightly in FL, but that's hardly ideal. It also drops data if I send any non-trivial stream of CCs.

Any other suggestions, or obvious errors in my approach? I can't believe the Nano can't keep up, it's bursty data but not unreasonable. Like 90 bytes at once if I send 30 notes? Though I might try an 80 MHz ESP8266 (which I think is overkill).

Code:

// -------------------------------------------------------------
// Arduino control of NeoPixel strips via MIDI.

#include <MIDI.h>               // Use1ByteParsing set to false, and BaudRate set to 115200
#include <Adafruit_NeoPixel.h>
#include <HardwareSerial.h>     // Increased the buffer size to 1024

#define PIN 2

#define NUM_PIXELS  60  

#define CHANNEL_RED   1
#define CHANNEL_GREEN 2
#define CHANNEL_BLUE  3
#define CHANNEL_WHITE 4

#define CC_BRIGHTNESS 74

// Parameter 1 = number of pixels in strip
// Parameter 2 = Arduino pin number (most are valid)
// Parameter 3 = pixel type flags, add together as needed:
//   NEO_KHZ800  800 KHz bitstream (most NeoPixel products w/WS2812 LEDs)
//   NEO_KHZ400  400 KHz (classic 'v1' (not v2) FLORA pixels, WS2811 drivers)
//   NEO_GRB     Pixels are wired for GRB bitstream (most NeoPixel products)
//   NEO_RGB     Pixels are wired for RGB bitstream (v1 FLORA pixels, not v2)
Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUM_PIXELS, PIN, NEO_GRB + NEO_KHZ800);

MIDI_CREATE_DEFAULT_INSTANCE();  // Baud rate needs to be changed to 115200 in midi_Settings.h !!!!   Hairless MIDI must match

boolean changed = false;

// ----------------------------------------------------------------------------

void setup()
{
    MIDIsetup();
    strip.begin();
    PixelRefresh();
}

void loop()
{
    // Don't do a heck of a lot here.  All the fun stuff is done in the MIDI handlers.
    changed = false;

    // Call MIDI.read the fastest you can for real-time performance.
    MIDI.read();

    if (changed)
    {
        PixelRefresh();
        changed = false;
    }
}

// ----------------------------------------------------------------------------

void MIDIsetup()
{
    // Initiate MIDI communications, listen to all channels
    MIDI.begin(MIDI_CHANNEL_OMNI);

    // No MIDIThru
    MIDI.turnThruOff();

    // Connect the functions to the library
    MIDI.setHandleNoteOn(HandleNoteOn);
    MIDI.setHandleNoteOff(HandleNoteOff);
    MIDI.setHandleControlChange(HandleControlChange);
}

inline void PixelRefresh()
{
    strip.show();
}

// -----------------------------------------------------------------------------
void HandleNoteOn(byte channel, byte note, byte velocity)
{
    // This acts like a NoteOff.
    if (velocity == 0)
    {
        HandleNoteOff(channel, note, velocity);
        return;
    }

    byte brightness = velocity << 2;  // 0 to 254

    uint32_t current_color = strip.getPixelColor(note);

    byte red = (current_color >> 16) & 0xFF;
    byte green = (current_color >> 8) & 0xFF;
    byte blue = (current_color)& 0xFF;

    switch (channel)
    {
    case CHANNEL_RED:
        red = brightness;
        break;

    case CHANNEL_GREEN:
        green = brightness;
        break;

    case CHANNEL_BLUE:
        blue = brightness;
        break;

    case CHANNEL_WHITE:
        red = brightness;
        green = brightness;
        blue = brightness;
        break;

    default:  // Ignore all others
        return;
    }

    strip.setPixelColor(note, red, green, blue);
    changed = true;
}

// -----------------------------------------------------------------------------
void HandleNoteOff(byte channel, byte note, byte velocity)
{
    uint32_t current_color = strip.getPixelColor(note);

    byte red = (current_color >> 16) & 0xFF;
    byte green = (current_color >> 8) & 0xFF;
    byte blue = (current_color)& 0xFF;

    switch (channel)
    {
    case CHANNEL_RED:
        red = 0;
        break;

    case CHANNEL_GREEN:
        green = 0;
        break;

    case CHANNEL_BLUE:
        blue = 0;
        break;

    case CHANNEL_WHITE:
        red = 0;
        green = 0;
        blue = 0;
        break;

    default:  // Ignore all others
        return;
    }

    strip.setPixelColor(note, red, green, blue);
    changed = true;
}


void HandleControlChange(byte channel, byte number, byte value)
{
    switch (number)
    {
    case CC_BRIGHTNESS:
        HandleBrightness(channel, value * 2);
        break;

    default:  // Ignore all others
        return;
    }
}

uint32_t HandleBrightness(byte channel, byte brightness)
{
    byte red = 0;
    byte green = 0;
    byte blue = 0;

    switch (channel)
    {
    case CHANNEL_RED:
        red = brightness;
        break;

    case CHANNEL_GREEN:
        green = brightness;
        break;

    case CHANNEL_BLUE:
        blue = brightness;
        break;

    case CHANNEL_WHITE:
        red = brightness;
        green = brightness;
        blue = brightness;
        break;

    default:  // Ignore all others
        return 0;
    }

    for (int i = 0; i < NUM_PIXELS; i++)
    {
        strip.setPixelColor(i, red, green, blue);
    }
    changed = true;
}

// EOF

I've run into an issue where incoming MIDI messages get dropped if too many arrive at one time.

Are you sure that the thing sending the MIDI is not going into running status mode when things get heavy?
This is normally the reason.

The other thing it could be is that you are not processing the data fast enough so that the input buffer overflows.

Good suggestions.

Not using running status though, according to the output from Hairless MIDI it's always the full three bytes.

I still can't believe a Nano can't keep up with bursts of 20 bytes or so. The default buffer is still 64 bytes, and I increased it to 1024 bytes. Plus I can see it fails on the very first group of messages.

I'm going to roll my own MIDI handler to see if I can measure what's going on more accurately.

My current theory is that it's interrupt contention between the NeoPixel library and HardwareSerial, as per

Interrupt problems · FastLED/FastLED Wiki · GitHub .

Currently experimenting with FastLED.

You could have a point there. As the neopixels need the same interrupt free time to transfer a given amount of data I don't think one library can transfer the data any faster than another. Do you drop data when you are not driving them?

You could try splitting your leds into smaller strings, the gaps between sending to each will let the serial interrupt work.

Note it is not the buffer size that is the limit, it is getting the byte out of the buffer from the UART. There is a UART overrun flag in the registers, you could try checking that.

Not sure how to determine if I'm dropping data without any sort of output though :wink:

This is almost certainly the issue. Great suggestion on the overrun flag, I'll check that to confirm.

Now, how to alleviate it? I suppose I could have two controllers, one for receiving/buffering the data and a second to drive the NeoPixels, and an interrupt-aware protocol between the two. Or upgrade to a high-speed Teensy. Just thinking out loud.

Ugh, this project was so easy in my head :wink:

I know there are "better" LED strips out there with less stringent timing requirements, but the 60-led WS2812 strip is exactly what I was looking for aesthetically. Have to rethink...

What about splitting the strong into two or more, like I suggested before. Keep the data transfer time less than one MIDI byte long.

Interesting approach...would really complicate the wiring in my opinion though, and I'm hoping to avoid cutting the weatherproof strips.

The scheme that's currently in my head is with two controllers - One to receive/buffer the MIDI data, and a second to run the NeoPixels like hinted at above. There would be a GPIO line running from the second back to the first saying "hey, I'm not talking to the Neopixels now so go ahead and send." Then SoftwareSerial to send the data from the first to the second.

Still some details to work out and test obviously.

Then SoftwareSerial to send the data from the first to the second.

You will have to do this on a per byte basis not a per message basis.

Why do you say that?

I got it working great, with short messages of three bytes (one MIDI message each). I might need to add a timeout to the receive I think so it doesn't deadlock if bytes are lost, but it seems pretty robust after an hour of testing.

If you're curious:

Very short video of it in operation:

Why do you say that?

Because the problem was in removing a byte from the UART into the RX buffer, causing the UART to over run and so loose a byte. So you would have to control byte movement not message movement.

But thinking about it with your arrangement you actually have a buffer with in effect two bytes, one in the UART buffer and one up the spout, so to speak ( being received ).

I know this is an old post, but I thought I would share something I've found.

I've seen a lot of posts about dropping midi messages and it possibly being related to buffer overrun. That might be the case, but I thought the same thing until I learned about MIDI Running Status:

https://www.midikits.net/midi_analyser/running_status.htm

Basically it means that you don't always get a command byte where you expect. Instead, if the command byte winds up being the same as the previous command byte, the command byte might be missing and instead you will just get the data bytes.

The following is a midi test that displays the midi commands received. I have a Mega, so am using Serial RX1 instead of RX so I can output debug messages. Here's my code:

//MIDI TEST

#define LED_PIN     32

#define MIDI_NOTE_OFF   0x80
#define MIDI_NOTE_ON    0x90
#define MIDI_AFTERTOUCH 0xA0
#define MIDI_CONT_CONTR 0xB0
#define MIDI_PATCH_CHNG 0xC0
#define MIDI_CHAN_PRES  0xD0
#define MIDI_PITCH_BEND 0xE0
#define MIDI_NON_MUSIC  0xF0

byte prevCommandByte = 0;


// This function will be automatically called when a NoteOn is received.
// It must be a void-returning function with the correct parameters,
// see documentation here:
// http://arduinomidilib.fortyseveneffects.com/a00022.html

void handleNoteOn(byte channel, byte pitch, byte velocity)
{

  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level


}

void handleNoteOff(byte channel, byte pitch, byte velocity)
{

  digitalWrite(LED_BUILTIN, LOW);   // turn the LED off (LOW is the voltage level

}

void printByte(String s, byte b) {
  Serial.print(s);
  Serial.print(" ");
  Serial.println(b,HEX);
}


void checkMIDI() {

  byte commandByte;
  byte noteByte;
  byte velocityByte;

  boolean bIsMidiRunning = false;


  if (Serial1.available() >= 3) {
    commandByte = Serial1.read();//read first byte

    //May not be a command byte if we are getting a MIDI running message
    if (commandByte < 0x80) {
      bIsMidiRunning = true;
      noteByte = commandByte;
      commandByte = prevCommandByte;

    //Else it is a command byte
    } else {
      prevCommandByte = commandByte;
      bIsMidiRunning = false;
      
      //Remove channel from byte
      if (commandByte < 0xf0)
      {
        // Channel message, remove channel nibble.
        commandByte = commandByte & 0xf0;
      }      
    }
    



    Serial.println("------------------");
    if (commandByte == MIDI_NOTE_ON) { //if note on message
      if (bIsMidiRunning == false) noteByte = Serial1.read();//read next byte
      velocityByte = Serial1.read();//read final byte
      handleNoteOn(0, noteByte, velocityByte);
      
      printByte("Note On command",commandByte);
      printByte("  note", noteByte);
      printByte("  velocity", velocityByte);

    } else if (commandByte == MIDI_NOTE_OFF ) {
      if (bIsMidiRunning == false) noteByte = Serial1.read();//read next byte
      velocityByte = Serial1.read();//read final byte
      handleNoteOff(0, noteByte, velocityByte);

      printByte("Note Off command",commandByte);
      printByte("  note", noteByte);
      printByte("  velocity", velocityByte);

    } else if (commandByte == MIDI_AFTERTOUCH ) {
      if (bIsMidiRunning == false) noteByte = Serial1.read();//read next byte
      velocityByte = Serial1.read();//read final byte

      printByte("Aftertouch command",commandByte);
      printByte("  byte 1", noteByte);
      printByte("  byte 2", velocityByte);
      

    } else if (commandByte == MIDI_CONT_CONTR ) {
      if (bIsMidiRunning == false) noteByte = Serial1.read();//read next byte
      velocityByte = Serial1.read();//read final byte

      printByte("MIDI Controller command",commandByte);
      printByte("  byte 1", noteByte);
      printByte("  byte 2", velocityByte);
      

    } else if (commandByte == MIDI_PATCH_CHNG ) {
      if (bIsMidiRunning == false) noteByte = Serial1.read();//read next byte
      velocityByte = Serial1.read();//read final byte

      printByte("Patch Change command",commandByte);
      printByte("  byte 1", noteByte);
      printByte("  byte 2", velocityByte);
      

    } else if (commandByte == MIDI_CHAN_PRES ) {
      if (bIsMidiRunning == false) noteByte = Serial1.read();//read next byte
      //no second parameter

      printByte("Channel Pressure? command",commandByte);
      printByte("  byte 1", noteByte);
      

    } else if (commandByte == MIDI_PITCH_BEND ) {
      if (bIsMidiRunning == false) noteByte = Serial1.read();//read next byte
      velocityByte = Serial1.read();//read final byte
      printByte("Pitch Bend command",commandByte);
      printByte("  byte 1", noteByte);
      printByte("  byte 2", velocityByte);
      

    } else if (commandByte == MIDI_NON_MUSIC ) {
        //No parameters
      printByte("Non Music command",commandByte);
        

    } else {
      printByte("ERROR - BAD command", commandByte);

    }

  }



}

void setup() {

  delay(300); // sanity delay

  Serial.begin(9600);
  
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level

  // Initiate MIDI communications
  Serial1.begin(31250);


  Serial.println ("Setup done "); 
  

}


void loop() {

  checkMIDI();


}