MIDI input / LED strip display timing issue

So I've set up an Arudino to receive MIDI input and use that to control an APA102 LED strip, but am having some trouble figuring out how to get the LEDs to display more synchronously. As a test, I have each MIDI note corresponding to a single LED on the strip - note value 0 turns on/off LED position 0, value 1 controls LED position 1, etc.

When I have the loop run FastLED.show() by itself, the strip displays all the LEDs correctly (i.e. no MIDI messages dropped), up to 25 notes - anything more than that, and MIDI messages get dropped (i.e. an LED not turning on). Problem is, there is a noticeable delay between each note, and so you can see each LED light up one by one in order.

As an experiment, I tried putting a conditional around the show function:

if (!MIDI.read()) { FastLED.show(); }

...which I know is not ideal, but it looks like I do get a more synchronous result. There's still a slight delay between LEDs lighting up, but it's definitely much better than before. The problem now is that random MIDI messages get missed regularly. Furthermore, the Arduino will crash occasionally, which I still have yet to figure out the reason why.

It seems like without the "if" statement, the Arduino is at least still reading all the MIDI correctly and queuing the data up, but is taking time to display everything. Having the "if" statement for some reason makes the output side faster, but in turn other problems get introduced.

#include "FastLED.h"
#include <MIDI.h>


FASTLED_USING_NAMESPACE

#if defined(FASTLED_VERSION) && (FASTLED_VERSION < 3001000)
#warning "Requires FastLED 3.1 or later; check github for latest code."
#endif

MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI);

#define DATA_PIN_C 3
#define CLK_PIN_C   4
#define COLOR_ORDER BGR
#define LED_TYPE APA102
#define NUM_LEDS_C 50
#define BRIGHTNESS 30


CRGB leds_C1[NUM_LEDS_C];


//MIDI variables
byte midi_on = 0x90;
byte midi_off = 0x80;
byte location_byte;
byte channel;
byte note;
byte velocity;
byte midiNote[2][128];
boolean newNote = false;




void setup() {
  delay(3000);
  MIDI.begin(MIDI_CHANNEL_OMNI);

  // tell FastLED about the LED strip configuration
  FastLED.addLeds<LED_TYPE,DATA_PIN_C,CLK_PIN_C,COLOR_ORDER>(leds_C1, NUM_LEDS_C).setCorrection(TypicalLEDStrip);

  // set master brightness control
  FastLED.setBrightness(BRIGHTNESS);
  set_max_power_in_volts_and_milliamps(5, 10000);
}



void loop() {

  if (MIDI.read()) {
    location_byte = MIDI.getType();
    channel = MIDI.getChannel();
    note = MIDI.getData1();
    velocity = MIDI.getData2();
    if (location_byte == midi_on || location_byte == midi_off) newNote = true;
  }
  
  if (newNote) {
    if (location_byte == midi_on) {
      midiNote[channel][note] = 1;
    }
    if (location_byte == midi_off) {
      midiNote[channel][note] = 0;
    }
    newNote = false;
  }

  channel = 1;


  if (midiNote[1][60] == 1) { leds_C1[0]  = CRGB::Blue;}
  else if (midiNote[1][60] == 0) { leds_C1[0]  = CRGB::Black;}
  if (midiNote[1][61] == 1) { leds_C1[1]  = CRGB::Red;}
  else if (midiNote[1][61] == 0) { leds_C1[1]  = CRGB::Black;}
  if (midiNote[1][62] == 1) { leds_C1[2]  = CRGB::Green;}
  else if (midiNote[1][62] == 0) { leds_C1[2]  = CRGB::Black;}
  if (midiNote[1][63] == 1) { leds_C1[3]  = CRGB::Blue;}
  else if (midiNote[1][63] == 0) { leds_C1[3]  = CRGB::Black;}
  if (midiNote[1][64] == 1) { leds_C1[4]  = CRGB::Red;}
  else if (midiNote[1][64] == 0) { leds_C1[4]  = CRGB::Black;}
  if (midiNote[1][65] == 1) { leds_C1[5]  = CRGB::Green;}
  else if (midiNote[1][65] == 0) { leds_C1[5]  = CRGB::Black;}
  if (midiNote[1][66] == 1) { leds_C1[6]  = CRGB::Blue;}
  else if (midiNote[1][66] == 0) { leds_C1[6]  = CRGB::Black;}
  if (midiNote[1][67] == 1) { leds_C1[7]  = CRGB::Red;}
  else if (midiNote[1][67] == 0) { leds_C1[7]  = CRGB::Black;}
  if (midiNote[1][68] == 1) { leds_C1[8]  = CRGB::Green;}
  else if (midiNote[1][68] == 0) { leds_C1[8]  = CRGB::Black;}
  if (midiNote[1][69] == 1) { leds_C1[9]  = CRGB::Blue;}
  else if (midiNote[1][69] == 0) { leds_C1[9]  = CRGB::Black;}
  if (midiNote[1][70] == 1) { leds_C1[10]  = CRGB::Red;}
  else if (midiNote[1][70] == 0) { leds_C1[10]  = CRGB::Black;}
  if (midiNote[1][71] == 1) { leds_C1[11]  = CRGB::Green;}
  else if (midiNote[1][71] == 0) { leds_C1[11]  = CRGB::Black;}

  if (midiNote[1][72] == 1) { leds_C1[12]  = CRGB::Blue;}
  else if (midiNote[1][72] == 0) { leds_C1[12]  = CRGB::Black;}
  if (midiNote[1][73] == 1) { leds_C1[13]  = CRGB::Red;}
  else if (midiNote[1][73] == 0) { leds_C1[13]  = CRGB::Black;}
  if (midiNote[1][74] == 1) { leds_C1[14]  = CRGB::Green;}
  else if (midiNote[1][74] == 0) { leds_C1[14]  = CRGB::Black;}
  if (midiNote[1][75] == 1) { leds_C1[15]  = CRGB::Blue;}
  else if (midiNote[1][75] == 0) { leds_C1[15]  = CRGB::Black;}
  if (midiNote[1][76] == 1) { leds_C1[16]  = CRGB::Red;}
  else if (midiNote[1][76] == 0) { leds_C1[16]  = CRGB::Black;}
  if (midiNote[1][77] == 1) { leds_C1[17]  = CRGB::Green;}
  else if (midiNote[1][77] == 0) { leds_C1[17]  = CRGB::Black;}
  if (midiNote[1][78] == 1) { leds_C1[18]  = CRGB::Blue;}
  else if (midiNote[1][78] == 0) { leds_C1[18]  = CRGB::Black;}
  if (midiNote[1][79] == 1) { leds_C1[19]  = CRGB::Red;}
  else if (midiNote[1][79] == 0) { leds_C1[19]  = CRGB::Black;}
  if (midiNote[1][80] == 1) { leds_C1[20]  = CRGB::Green;}
  else if (midiNote[1][80] == 0) { leds_C1[20]  = CRGB::Black;}
  if (midiNote[1][81] == 1) { leds_C1[21]  = CRGB::Blue;}
  else if (midiNote[1][81] == 0) { leds_C1[21]  = CRGB::Black;}
  if (midiNote[1][82] == 1) { leds_C1[22]  = CRGB::Red;}
  else if (midiNote[1][82] == 0) { leds_C1[22]  = CRGB::Black;}
  if (midiNote[1][83] == 1) { leds_C1[23]  = CRGB::Green;}
  else if (midiNote[1][83] == 0) { leds_C1[23]  = CRGB::Black;}
  
  if (midiNote[1][84] == 1) { leds_C1[24]  = CRGB::Blue;}
  else if (midiNote[1][84] == 0) { leds_C1[24]  = CRGB::Black;}
  if (midiNote[1][85] == 1) { leds_C1[25]  = CRGB::Red;}
  else if (midiNote[1][85] == 0) { leds_C1[25]  = CRGB::Black;}
  if (midiNote[1][86] == 1) { leds_C1[26]  = CRGB::Green;}
  else if (midiNote[1][86] == 0) { leds_C1[26]  = CRGB::Black;}
  if (midiNote[1][87] == 1) { leds_C1[27]  = CRGB::Blue;}
  else if (midiNote[1][87] == 0) { leds_C1[27]  = CRGB::Black;}
  if (midiNote[1][88] == 1) { leds_C1[28]  = CRGB::Red;}
  else if (midiNote[1][88] == 0) { leds_C1[28]  = CRGB::Black;}
  if (midiNote[1][89] == 1) { leds_C1[29]  = CRGB::Green;}
  else if (midiNote[1][89] == 0) { leds_C1[29]  = CRGB::Black;}


//  if (!MIDI.read()) { FastLED.show(); }
  FastLED.show();
}

Side note - I know having a million "if" statements is not ideal, but I've tried messing around with using "switch/case" statements instead and still seem to get the same issue, so it doesn't seem to be a huge factor here.

Any suggestions?

i have been playing with the same idea using shiftPWM and midi... i know there are probable more efficient ways of doing it but the best way i could get it to work was using a switch to handle the Midi on/off.

Im very much new to this Arduino stuffs but im hooked lol.

const int ShiftPWM_latchPin=8;
const bool ShiftPWM_invertOutputs = false; 
const bool ShiftPWM_balanceLoad = false;

#include <ShiftPWM.h>   // include ShiftPWM.h after setting the pins!
#include <MIDI.h>  // Add Midi Library

MIDI_CREATE_DEFAULT_INSTANCE();

unsigned char maxBrightness = 255;
unsigned char pwmFrequency = 75;
int numRegisters = 3;
int numRGBleds = numRegisters*8/3;
int note = 0;


void rgbLedRainbow(unsigned long cycleTime, int rainbowWidth);
void alternatingColors(void);
void hueShiftAll(void);

unsigned long startTime = 0; // start time for the chosen fading mode

void setup(){
  Serial.begin(9600);

  // Sets the number of 8-bit registers that are used.
  ShiftPWM.SetAmountOfRegisters(numRegisters);
  ShiftPWM.SetPinGrouping(8); 
  ShiftPWM.Start(pwmFrequency,maxBrightness);

  MIDI.begin(MIDI_CHANNEL_OMNI); // Initialize the Midi Library.
  // OMNI sets it to listen to all channels.. MIDI.begin(2) would set it 
  // to respond to notes on channel 2 only.
  MIDI.setHandleNoteOn(MyHandleNoteOn); // This is important!! This command
  // tells the Midi Library which function you want to call when a NOTE ON command
  // is received. In this case it's "MyHandleNoteOn".
  MIDI.setHandleNoteOff(MyHandleNoteOff); // This command tells the Midi Library 
  // to call "MyHandleNoteOff" when a NOTE OFF command is received.
}


void loop() { // Main loop
  MIDI.read(); // Continuously check if Midi data has been received.
}

void MyHandleNoteOn(byte channel, byte note, byte velocity) { 



switch (note){
  
  case 1:
  //ShiftPWM.SetAll(velocity*2);
    alternatingColors();
  break;
  case 3:
    rgbLedRainbow(3000,24);
  break;
  case 6:
  ShiftPWM.SetAllHSV(0, 255, velocity*2);
  break;
  case 8:
  ShiftPWM.SetAllHSV(240, 255, velocity*2);
  break;
  case 10:
  ShiftPWM.SetAllHSV(120, 255, velocity*2);
  break;
  case 13:
    hueShiftAll();
  break;



  case 0:
    ShiftPWM.SetHSV(0, 0,0, velocity*2);
  break;
  case 2:
    ShiftPWM.SetHSV(1, 0,0, velocity*2);
  break;
  case 4:
    ShiftPWM.SetHSV(2, 0,0, velocity*2);
  break;
  case 5:
    ShiftPWM.SetHSV(3, 0,0, velocity*2);
  break; 
  case 7:
    ShiftPWM.SetHSV(4, 0,0, velocity*2);
  break;
  case 9:
    ShiftPWM.SetHSV(5, 0,0, velocity*2);
  break;
  case 11:
    ShiftPWM.SetHSV(6, 0,0, velocity*2);
  break;
  case 12:
    ShiftPWM.SetHSV(7, 0,0, velocity*2);
  break; 
}
}
void MyHandleNoteOff(byte channel, byte note, byte velocity) { 



switch (note){
  
  case 1:
 
    ShiftPWM.SetAll(0);
 
  break;
  case 3:

    ShiftPWM.SetAll(0);

  break;
  case 6:

    ShiftPWM.SetAll(0);

  break;
  case 8:

    ShiftPWM.SetAll(0);

  break;
  case 10:

    ShiftPWM.SetAll(0);

  break;
  case 13:

    ShiftPWM.SetAll(0);

   break;  
////////////
case 0:
    ShiftPWM.SetHSV(0, 0,0,0);
  break;
  case 2:
    ShiftPWM.SetHSV(1, 0,0,0);
  break;
  case 4:
    ShiftPWM.SetHSV(2, 0,0,0);
  break;
  case 5:
    ShiftPWM.SetHSV(3, 0,0,0);
  break; 
  case 7:
    ShiftPWM.SetHSV(4, 0,0,0);
  break;
  case 9:
    ShiftPWM.SetHSV(5, 0,0,0);
  break;
  case 11:
    ShiftPWM.SetHSV(6, 0,0,0);
  break;
  case 12:
    ShiftPWM.SetHSV(7, 0,0,0);
  break; 
}

}

void hueShiftAll(void){  // Hue shift all LED's
  unsigned long cycleTime = 10000;
  unsigned long time = millis()-startTime;
  unsigned long hue = (360*time/cycleTime)%360;
  ShiftPWM.SetAllHSV(hue, 255, 255); 
}

void alternatingColors(void){ // Alternate LED's in 6 different colors
      unsigned long holdTime = 2;
      unsigned long time = millis()-startTime;
      unsigned long shift = (time/holdTime)%6;
      for(unsigned int ran=0; ran<numRGBleds; ran++){
        switch((ran+shift)%6){
        case 0:
          ShiftPWM.SetRGB(ran,255,0,0);    // red
          break;
        case 1:
          ShiftPWM.SetRGB(ran,0,255,0);    // green
          break;
        case 2:
          ShiftPWM.SetRGB(ran,0,0,255);    // blue
          break;
        case 3:
          ShiftPWM.SetRGB(ran,255,128,0);  // orange
          break;
        case 4:
          ShiftPWM.SetRGB(ran,0,255,255);  // turqoise
          break;
        case 5:
          ShiftPWM.SetRGB(ran,255,0,255);  // purple
          break;
        }
      }
    }
    
void rgbLedRainbow(unsigned long cycleTime, int rainbowWidth){
  // Displays a rainbow spread over a few LED's (numRGBLeds), which shifts in hue. 
  // The rainbow can be wider then the real number of LED's.
  unsigned long time = millis()-startTime;
  unsigned long colorShift = (360*time/cycleTime)%360; // this color shift is like the hue slider in Photoshop.

  for(unsigned int led=0;led<numRGBleds;led++){ // loop over all LED's
    int hue = ((led)*360/(rainbowWidth-1)+colorShift)%360; // Set hue from 0 to 360 from first to last led and shift the hue
    ShiftPWM.SetHSV(led, hue, 255, 255); // write the HSV values, with saturation and value at maximum

  }
  
}

The problem with using the FastLed library with an APA102 LED strip is that the libiary was designed initially for the WS2812 strips and they require precise timing to work. Therefor the interrupts are turned off during the data transfer from Arduino to strip.
Serial inputs require the interrupts to be on in order to work, therefore you will miss messages.

However the APA102 LED strip does not need precise timing as it has a clock and data line, so the transfer could happen with the interrupts still enabled. This means if you write your own bit banging transfer function you will not miss MIDI messages.

As an experiment, I tried putting a conditional around the show function:

if (!MIDI.read()) { FastLED.show(); }

It is a bit crude and results in a lot unnecessary callings of the show method. Use a Boolean variable to control this. Set it when ever you get a positive result from a MIDI read, and include that variable in the test for calling the show method, and then clear that variable directly after you call show.

        if (!MIDI.read() && ! shown) { FastLED.show(); shown = true}

The shown boolean does seem like it made the LEDs display even faster (though it's hard to be sure, since they light up so quickly), but now more MIDI messages get randomly missed. Whereas before it was somewhat consistent (every other LED would light up, but not always), now it the dropped messages are more frequent and random.

What is a "bit banging transfer function"?

Actually, something else I just discovered - it seems that the if(!MIDI.read()) conditional itself is actually interfering with the incoming MIDI somehow. When I repeatedly play a single note on the keyboard, occasionally a MIDI message will get missed, say roughly 1 out of 5. With the "shown" boolean added in, at least 4 out of 5 are missed (usually more). Yet when FastLED.show() function is called on its own without a conditional, every note on/off message gets received no problem.

At this point, I'm not which problem I should be solving then - it seems like there are two different options to explore:

  • All MIDI messages are received, but there is a perceptible delay in displaying simultaneous notes - I would need to figure out how to eliminate the delay.
  • There is less perceptible delay with the conditional, but then MIDI messages are frequently missed - I'd need to figure out how to stop messages from getting dropped.

I'm not sure which would be easier to troubleshoot. One thing I don't understand is, why would having if(!MIDI.read()) around the show function cause missed MIDI messages?

Try storing what the MIDI.read() returns and doing all the switching from that stored variable. It looks like a MIDI.read is not passive but telling the computer you can clear the buffer. Therefore if you read and it is true when you are looking for a false you miss the true response when you read again.

That worked - it looks like the MIDI was all being received, but by the time it got to the show function, MIDI.read() was not giving it the desired value, and so it wouldn't display anything.

Now all the MIDI messages are getting there, but we're back to the delay issue again - the display timing of the LEDs looks like they're about the same as if it were just the show function by itself. To see if I could maybe force all the MIDI to be stored at once before calling the show function, I tried using "while" instead of "if" for the MIDI input portion:

  while (MIDI.read()) {
    MIDIread = MIDI.read();
    location_byte = MIDI.getType();
    channel = MIDI.getChannel();
    note = MIDI.getData1();
    if (location_byte == midi_on || location_byte == midi_off) newNote = true;
  
    if (newNote) {
      if (location_byte == midi_on) {
        midiNote[channel][note] = 1;
      }
      if (location_byte == midi_off) {
        midiNote[channel][note] = 0;
      }
      newNote = false;
    }
  }

...but it doesn't seem to have any effect. I'm trying to figure out the error in logic here - I'd think that the steps would be:

  • Store all incoming MIDI messages
  • Update the LEDs based on those messages (i.e. the chunk of "if" statements following the MIDI.read() section)
  • Call FastLED.show() once to display update the LEDs all at once.

The only thing I can think of is that maybe the loop is going faster than the MIDI is actually coming in, so the "while" is possibly still only reading one incoming message at a time?

On another note, I realized I unfortunately will not be able to use shown boolean for the more complex wave functions I want to use that require the LEDs to be constantly updated, so that had to be scrapped.

On another note, I realized I unfortunately will not be able to use shown boolean for the more complex wave functions I want to use that require the LEDs to be constantly updated, so that had to be scrapped.

That is a mistake, you will not see that effect.

Can you post what you had with the Boolean variable, maybe you were not using it right.

maybe the loop is going faster than the MIDI is actually coming in,

Of course it is.

Store all incoming MIDI messages

  • Update the LEDs based on those messages (i.e. the chunk of "if" statements following the MIDI.read() section)
  • Call FastLED.show() once to display update the LEDs all at once.

That is only reproducing what the library and the Boolean system is doing.

I have:

if (MIDI.read()) {
    MIDIread = MIDI.read();
    shown = false;
    location_byte = MIDI.getType();
    channel = MIDI.getChannel();
    note = MIDI.getData1();
    if (location_byte == midi_on || location_byte == midi_off) newNote = true;
  
    if (newNote) {
      if (location_byte == midi_on) {
        midiNote[channel][note] = 1;
      }
      if (location_byte == midi_off) {
        midiNote[channel][note] = 0;
      }
      newNote = false;
    }
  }

...and then at the end:

if (!MIDIread && !shown) {
  FastLED.show();
  shown = true;
  MIDIread = false;
}

You are not doing it right

if (MIDI.read()) {
    MIDIread = MIDI.read();

It should be

MIDIread = MIDI.read();
if (MIDIread) {

As I said before you can not do successive MIDI.read() without reading the message or you loose some part of the message.

Oh yeah, duh that makes total sense. Still, doesn't having the !shown boolean cause it to only update the LEDs one frame (i.e. when MIDI is received)? To utilize more continuous FastLED animation sequences, I'd think it interferes with displaying regularly, and wouldn't it be better to not have it? Unless I should be coming up with a better placement to trigger the boolean...

Also, going back to this:

batmundo:
maybe the loop is going faster than the MIDI is actually coming in

Grumpy_Mike:
Of course it is.

This doesn't mean the delay I see is purely due to the rate of the incoming MIDI and thus unavoidable, does it? I can't imagine the MIDI rate to be THAT slow though...

Unless I should be coming up with a better placement to trigger the boolean...

I would think so.

There is a lot of wasted processing time with all those if statements. Why not just use a look up table to decide what LED to turn on or off? Then change the LEDs when you identify the note.

Yeah, look up tables are on my to-do list - I'm still relatively new at coding, so I still haven't figured out how to use them yet.

Sorry, still not quite understanding the necessity of the shown boolean. As I mentioned, I want to be able to use this MIDI input setup with more complicated functions, not just turning LEDs on and off - the simple on/off code was me just trying to troubleshoot the displaying-MIDI-to-LED delay problems. For example, if I were to split the LED strip up into different subdivisions (which I'm doing for the main project I'm trying to use this whole MIDI thing for), like so:

CRGB rawleds_S1[NUM_LEDS_S];
  CRGBSet leds_S1 (rawleds_S1, NUM_LEDS_S);  //148
    CRGBSet leds_S1_R (leds_S1(0, 36)); //37
    CRGBSet leds_S1_B (leds_S1(37, 73)); //37
    CRGBSet leds_S1_L (leds_S1(74, 110)); //37
    CRGBSet leds_S1_T (leds_S1(111, 147)); //37

And have a bunch of if statements (hopefully one day I get the look ups figured out, but for now...) to call animation functions:

  if (midiNote[0] == 1) { rainbow (leds_S1_R, NUM_LEDS_S_div); }
  if (midiNote[1] == 1) { rainbow (leds_S1_B, NUM_LEDS_S_div); }
  if (midiNote[2] == 1) { rainbow (leds_S1_L, NUM_LEDS_S_div); }
  if (midiNote[3] == 1) { rainbow (leds_S1_T, NUM_LEDS_S_div); }

...which would be calling this:

void rainbow() {
  thishue++;
  fill_rainbow(leds, NUM_LEDS, thishue, deltahue);
}

Wouldn't I want the show function to be continuously running to update the LEDs?

If you want to call up animations on a note then the animation must be written as a state machine, which given your lack of knowledge of how look up tables work is another step in complexity of programming above look up tables.

Then you have to be able to drive several at once. Just use the one strip and restrict any pattern or animation to a specific range of LED numbers.

Wouldn't I want the show function to be continuously running to update the LEDs?

No you don’t need to update any faster than 30 per second because you can’t perceive anything faster.

Grumpy_Mike:
No you don’t need to update any faster than 30 per second because you can’t perceive anything faster.

Ah, did not know that. Then would there be any reason to not do something like:

EVERY_N_MILLISECONDS(30) { FastLED.show(); }

?

Then would there be any reason to not do something like:

Yes because you only want to update when their is something to update. If you want to update faster than 30mS then you don't you write the animation that goes so fast.

Anyway what is that line. I hope it is not some sort of multi threaded libiary.

Which line are you referring to? And, what's a multi threaded library?

EVERY_N_MILLISECONDS(30) { FastLED.show(); }

Is the line.

And, what's a multi threaded library?

Good keep it that way.

Oh, it's a function defined in the FastLED library you can use to do things in timed intervals without blocking anything, as opposed to using the FastLED.delay() function.