1

In order to finish a synth project that I'm working on I built a MIDI interface. To test it, I drew up some code which (is supposed to) light up an LED when any key is pressed. The light turns off again when the key is released. This will later be merged with another branch of code here to tell me which note is playing. This sounds simple enough but the devil's in the details. Due to my project's size constraints I am using ATMEL's ATtiny85 chip instead of my Arduino Uno. The ATtiny doesn't support the Arduino Serial library and by extension the standard MIDI library. As I can't make use of simple functions like MIDI.read() I've opted to work with an alternative Software Serial technique I adapted from an Instructable. Unfortunately, the results have been less than exemplary. Simply put, the LED almost never lights up. I have conducted two test only 1 of which somewhat succeeded:

1)

if(velocityByte > 0 ){digitalWrite(LED, HIGH);}
if(velocityByte == 0 ){digitalWrite(LED, LOW);}

Initially, I had figured that would be the most successful of my options. My keyboard* is binary about its ouput velocity. There are no pressure sensors so "on" is on and "off" is off. Unfortunately, however, the code seems to think otherwise.

2)

if(commandByte == noteOn ){digitalWrite(LED, HIGH);}
if(commandByte != noteOff ){digitalWrite(LED, LOW);}

Though originally less sure of this one, I've found it to be better than the other at catching notes. noteOn is the integer 144. While it does in fact trigger it does so only very rarely - once every 10 presses or so.

I believe I have however, whittled down the possible number of problems to 2:

1) Something is up with one or more of my calls to Serial. I've never really felt super confident with the author's method of obtaining data from Serial. For one, I have no confidence that I can insure I have collected the correct byte. If the code were to somehow "miss" counting a byte would it not assign, say, a commandByte to a noteByte? Is a better method available?

2) My keyboard's MIDI protocol is incompatible with my if-statement cases. At first I was under the assumption that my keyboard never sends a true "note off" message of any kind but instead a velocity = 0 message. But my test casts much doubt on this being the case and because of this, I don't really know how the keyboard handles note off messages. Does anyone else know?

*A Yamaha YPT-230

//MIDI INPUT LED TEST
//Adapted from code written
//by Instructables user Amanda Ghassaei

#include <SoftwareSerial.h>

SoftwareSerial mySerial(0, 1);  //RX, TX

#define LED 2

byte commandByte;
byte noteByte;
byte velocityByte;

byte noteOn = 144;
byte noteOff = 128;

void setup(){
  mySerial.begin(31250);
  digitalWrite(LED, LOW); 
  delay(1000);
}

void checkMIDI(){
  do{
    commandByte = mySerial.read();//read first byte
    noteByte = mySerial.read();//read next byte
    velocityByte = mySerial.read();//read final byte

    if (commandByte == noteOn){
      digitalWrite(LED, HIGH);
    }
    if(commandByte != noteOn){
      digitalWrite(LED, LOW);
    }  

  }
  while (mySerial.available() > 2);//when at least three bytes available
}


void loop(){
  checkMIDI();
}
/*
void gotNoteOn(commandByte, noteByte, velocityByte){
  digitalWrite(LED,HIGH);
}

void gotNoteOff(commandByte, noteByte, velocityByte){
  digitalWrite(LED,LOW);
}
*/

UPDATE Following a suggesting, I have replaced the do .. while loop with an ordinary while loop. Relevant changes below:

void checkMIDI(){
  while(mySerial.available() > 2){
    commandByte = mySerial.read();
    if (velocityByte > 0x00){//if note on message
      digitalWrite(LED, HIGH);
    }
    if(velocityByte == 0x00){
      digitalWrite(LED, LOW);
    }  
  }
}  

I'd like to think it's an improvement but if so only a slight one. Curiously, it almost works backwards - The default state of the LED is on while pressing a key produces no change pressing and holding causes the light to briefly shut off. But even this is sporadic. It may also be worth noting that I elected to use hex '0x00' as opposed to decimal '0'.

UPDATE II / NOTE Having slept on the problem I think I now understand why I am encountering this issue in the first place. The author of the Instructables code I adapted used an Arduino for both MIDI In as well as MIDI Out. Had they simply hooked one Arduino Uno to the other in an in/out configuration they could have perfectly ensured there were no running status issues by just never sending any running status messages. This would explain the lack of conformity to the actual realities of MIDI on the input side. If you can control exactly what data you are sending on one end (MIDI Out) it becomes fairly trivial to receive that data on the other end (MIDI In). Generalizing, the author may have assumed that a true keyboard behave the same thing and failed to actually verify this. Thoughts?

UPDATE III Following the lead of CL but deviating somewhat because I didn't fully understand what CL was doing, I have written up a highly derivative, new code. It is based around what I now know to be the highly fluid nature of MIDI messages and seeks to intelligently discriminate important messages from non-important messages based on content. It does not work (except for LED test at the beginning) but seems like a promising step nonetheless.

//MIDI Input LED Test

#include <SoftwareSerial.h>

SoftwareSerial mySerial(0, 1);  //RX, TX

#define LED 2  //Test LED

void setup(){
  mySerial.begin(31250);
  digitalWrite(LED, HIGH); //Test
  delay(1000);
  digitalWrite(LED, LOW); 
  delay(1000);
}

void loop(){
  checkMIDI();
} 

void checkMIDI(){
/*Two of the three independent types of bytes are always
distinct and can be identified simply by their values.
A Status byte (on channel 1 at least) is always either
0x80 (off) or 0x90 (on). No other type of message
(Note, Velocity) can ever take this value and so if
this is the value of the byte received I can be
assured it is a Status byte.

The Note and Velocity bytes are a bit (haha) dicier.
They have overlapping ranges with both taking values of
anywhere between 0x00 to 0x7F. *Ordinarily*. A caveat
is that my Yamaha YPT-230 is binary as far as velocity
goes. A YPT-230 data is either 0x00 (full off) or 0x7F
(full on). As long as a Note byte is never 0 or 127 I can
use this to discriminate between these two types
in this manner.
*/   

  if(mySerial.available() > 0){
    byte b = mySerial.read();

    if(b == 0x80 || 0x90){
      byte commandByte = b;
    }else if(b == 0x00 || 0x7F){
      if(b == 0x00){
        digitalWrite(LED, LOW);  //If vel = 0 turn LED off
      }else if(b == 0x7F){
        digitalWrite(LED, HIGH); //If vel = 127 turn LED on
      }
      //velocityByte = b;
    }else{
      byte noteByte = b;  //Do nothing with this for right now.
    }
  }
}
  • Have you tried using UsiSerial yet? – Ignacio Vazquez-Abrams Aug 9 '15 at 23:02
  • Never heard of it. I'll read up. Thanks. – Patagonian Rat Aug 9 '15 at 23:20
  • Is your LED tied high or low? – Ignacio Vazquez-Abrams Aug 10 '15 at 2:23
  • If that is your amended code, then you are no longer reading the velocityByte. – Nick Gammon Aug 10 '15 at 2:36
  • I suggest you take a closer look at the code on my page about MIDI - for one thing some MIDI devices return a "running status" which means that instead of the command byte being sent every time, it assumes that if you get a byte with the high-order bit clear, it is the same command as last time. If you just blindly read 3 bytes every time you will be interpreting all sorts of things as something they are not. – Nick Gammon Aug 10 '15 at 2:46
3

I suggest you first find what your Yamaha is sending you. I have a sketch that decodes MIDI for the Uno (assuming you have a Uno lying around). Run that, and see if you got note on with velocity zero, or note off. Then you are a little closer to solving the problem.


This part is quite wrong:

  do{
    commandByte = mySerial.read();//read first byte
    noteByte = mySerial.read();//read next byte
    velocityByte = mySerial.read();//read final byte

  ...

  }
  while (mySerial.available() > 2);//when at least three bytes available

You are reading 3 bytes, even if none are available. At least change it to:

while (mySerial.available() > 2)
  {
        commandByte = mySerial.read();//read first byte
        noteByte = mySerial.read();//read next byte
        velocityByte = mySerial.read();//read final byte

      ...

  }

The do ... while construct tests at the end of the loop. You must ensure you have 3 bytes "available" before reading 3 of them, not after.


Example code for ATtiny85

The code below is an adaptation of the MIDI decoder in my link above. This one just reads the incoming MIDI and turns on the LED on a "note on" and turns it off on a "note off" (or note on with velocity zero). Tested, works perfectly.

//  MIDI_decoder for ATtiny85
//  
// ATMEL ATTINY 25/45/85 / ARDUINO
// Pin 1 is /RESET
//
//                  +-\/-+
// Ain0 (D 5) PB5  1|    |8  Vcc
// Ain3 (D 3) PB3  2|    |7  PB2 (D 2) Ain1 
// Ain2 (D 4) PB4  3|    |6  PB1 (D 1) pwm1
//            GND  4|    |5  PB0 (D 0) pwm0
//                  +----+


#include <SoftwareSerial.h>

const byte LED = 0;   // pin 5 on ATtiny85

// Plug MIDI into pin D2 (MIDI in serial) (pin 7 on ATtiny85)

SoftwareSerial midi (2, 3);  // Rx, Tx

const int noRunningStatus = -1;

int runningStatus;
unsigned long lastRead;
byte lastCommand;

void setup() {
  //  Set MIDI baud rate:
  midi.begin(31250);
  runningStatus = noRunningStatus;
  pinMode (LED, OUTPUT);
} // end of setup

void RealTimeMessage (const byte msg)
  {
    // ignore realtime messages
  } // end of RealTimeMessage

// get next byte from serial (blocks)
int getNext ()
  {

  if (runningStatus != noRunningStatus)
    {
    int c = runningStatus;
    // finished with look ahead
    runningStatus = noRunningStatus;
    return c;
    }

  while (true)
    {
    while (midi.available () == 0)
      {}
    byte c = midi.read ();
    if (c >= 0xF8)  // RealTime messages
      RealTimeMessage (c);
    else
      return c;
    }
  } // end of getNext

const char * notes [] = { "C ", "C#", "D ", "D#", "E ", "F ", "F#", "G ", "G#", "A ", "A#", "B " };

byte note;
byte octave;
byte velocity;

// interpret a note in terms of note name and octave
void getNote ()
  {
  note = getNext ();
  octave = note / 12;
  note %= 12;
  }  // end of getNote

void getVelocity ()
  {
  velocity = getNext ();
  }

// show a control message 
void showControlMessage ()
  {
   byte message =  getNext () & 0x7F;
   byte param = getNext ();
  }  // end of showControlMessage

// read a system exclusive message 
void showSystemExclusive ()
  {
  int count = 0;
  while (true)
    {
    while (midi.available () == 0)
      {}
    byte c = midi.read ();
    if (c >= 0x80)
      {
      runningStatus = c;
      return;  
      }

    } // end of reading until all system exclusive done
  }  // end of showSystemExclusive

void loop() 
{
  byte c = getNext ();
  unsigned int parameter;

  if (((c & 0x80) == 0) && (lastCommand & 0x80))
    {
    runningStatus = c;
    c = lastCommand; 
    }

  // channel is in low order bits
  int channel = (c & 0x0F) + 1;

  // messages start with high-order bit set
  if (c & 0x80)
    {
    lastCommand = c;
    switch ((c >> 4) & 0x07)
      {
      case 0:   // Note off
        getNote ();
        getVelocity ();
        digitalWrite (LED, LOW);
        break;

      case 1:   // Note on
        getNote ();
        getVelocity ();
        if (velocity == 0)
          digitalWrite (LED, LOW);
        else
          digitalWrite (LED, HIGH);
        break;

      case 2:  // Polyphonic pressure
        getNote ();
        parameter = getNext ();  // pressure
        break;

      case 3: // Control change
        showControlMessage ();
        break;

      case 4:  // Program change
        parameter = getNext ();  // program
        break;

      case 5: // After-touch pressure
        parameter = getNext (); // value
        break;

      case 6: // Pitch wheel change 
        parameter = getNext () |  getNext () << 7; 
        break;

      case 7:  // system message
        {
        lastCommand = 0;           // these won't repeat I don't think
        switch (c & 0x0F)
          {
          case 0: // Exclusive
            parameter = getNext (); // vendor ID
            showSystemExclusive ();
            break;

          case 1: // Time code
            parameter = getNext () ;
            break;

          case 2:  // Song position 
            parameter = getNext () |  getNext () << 7; 
            break;

          case 3: // Song select
            parameter = getNext () ;  // song
            break;

          case 4:    // reserved
          case 5:    // reserved
          case 6:    // tune request
          case 7:    // end of exclusive
          case 8:    // timing clock
          case 9:    // reserved
          case 10:   // start
          case 11:   // continue
          case 12:   // stop
          case 13:   // reserved
          case 14:   // active sensing
          case 15:   // reset
             break;

          }  // end of switch on system message  

        }  // end system message
        break;
      }  // end of switch
    }  // end of if
  else
    {
    // unexpected, ignore 
    }

}  // end of loop

Suggested schematic

MIDI schematic


From a comment:

the 4N35 is too slow to work reliably with MIDI signals

Below is an updated schematic that uses a faster opto-coupler.

MIDI schematic with 6N137

Admittedly the 4N35 is below spec for its rise time (it is supposed to be less than 2 μs rise time, however the 4N35 is slower at between 3 µs and 7 µs). However during my testing it worked OK. It sounds like the faster rise time is specified to allow for multiple devices to be connected in series, a slow rise time will introduce distortion.

  • Thanks for the point on the do ... while loop. I had no idea it worked like that. I'll correct the code right now and respond with my results. – Patagonian Rat Aug 10 '15 at 0:50
  • Hmm, I'll get back with full results but so far it's not measurably any better... – Patagonian Rat Aug 10 '15 at 1:27
  • I've looked at and uploaded your code - it's great stuff! Two quick questions as I look through it thoroughly: Why is it so much longer than CL's suggested code? Are all the case statements strictly necessary? – Patagonian Rat Aug 12 '15 at 10:07
  • His code is not a complete sketch, it demonstrates decoding the MIDI only. He also ignores a lot of things (other than note on and note off) for simplicity. I think you possibly do this at your peril, because some commands are followed by different numbers of bytes, so you won't be certain what each byte means unless you decode the command. And if you don't have the running status (byte with the high-order bit on) you won't know for sure when you are starting a new command. My code above isn't particularly long. – Nick Gammon Aug 12 '15 at 20:43
  • That seems very plausible. I've been doing a lot of reading these past few days and it's become quite apparent that MIDI is much trickier than I was lead to believe! – Patagonian Rat Aug 12 '15 at 22:03
2

This is not how the MIDI protocol works.

The incoming stream can contain other commands than note-on or note-off, and with running status, it is impossible to predict the length of any single command.

You need at least some kind of state machine to remember where in the command you are:

byte nr;

for (;;) {
    byte b = mySerial.read();
    if (b & 0x80) { // status byte
        if (b >= 0xf8) {
            // real-time command; ignore
        } else {
            commandByte = b;
            nr = 1;
        }
    } else { // data byte
        if ((commandByte & 0xf0) == 0x80 ||
            (commandByte & 0xf0) == 0x90) {
            if (nr == 1) {
                noteByte = b;
                nr = 2;
            } else if (nr == 2) {
                velocityByte = b;
                nr = 1;

                if ((commandByte & 0xf0) == 0x90 &&
                    velocityByte > 0) {
                    // note on
                } else {
                    // note off
                }
            } 
        }
    }
}
  • You're right. I updated my question to include a note about this. I think the author completely neglected variable command lengths. Very quickly, could I ask you why you used the bitwise '&' in your code? – Patagonian Rat Aug 10 '15 at 14:25
  • What else should I use? – CL. Aug 10 '15 at 14:28
  • No reason I can think of. It's just that I don't understand what it does. At first, I mistook it for the 'and' sign (&&) and figured you were doing boolean logic but now I know that's not the case. Could you for instance explain if b & 0x80){}? Is this identical to if(b==0x80){}? – Patagonian Rat Aug 10 '15 at 14:55
  • b & 0x80 checks if the 7th bit is set. b & 0xf0 extracts the upper nybble. My code is the minimum needed to pase MIDI note-on/-off messages correctly. To be able to program, you need to learn more about C. – CL. Aug 10 '15 at 16:34
  • Could you tell me how well my code stacks up to what you have in my most recent update (III)? I opted to check if the incoming byte matched against a set of potential values (e.g. if(b == 0x80 || 0x90){commandByte = b;} ) given the nature of particular types of data. It doesn't work but would it be viable? – Patagonian Rat Aug 10 '15 at 17:02
0

Got it! After doing the requisite amount of research... I've developed a much improved checking system which allows me to truly demarcate between messages during running status much like Nick Gammon's code. While the code seems to work perfectly now it was still a bit glitchy when I tried it the other day. Perhaps I accidentally fixed it, most likely it'll crop back up again. How does it look?

//MIDI_Input_0x01    
#include <SoftwareSerial.h>

SoftwareSerial mySerial(0,1);

const int LED = 2;

int isStatus();
int isAftertouch();
int isRealTimeCategory();

int dataByte;
int velocityByte;
int result;

void setup()
{
  mySerial.begin(31250);
  pinMode(LED, OUTPUT);


  digitalWrite(LED, HIGH);
  delay(1000);
  digitalWrite(LED, LOW);
  delay(1000);
}

void loop()
{

  while(mySerial.available())
  {
    //This is the protocol for reading new stuff
    byte myByte = mySerial.read();
    if(isRealTimeCategory(myByte))
    {
      ;
    }
    else
    {
      if(isStatus(myByte))
      {

        result = (myByte | 0x80);       //0b10000000
        switch(result)
        {
          case 0b10000000:
            digitalWrite(LED, LOW);
            break;
          case 0b10010000:
            digitalWrite(LED, HIGH);
            break;  
          case 0b10100000:
            break;
          case 0b10110000:
            break;
          case 0b11000000:
            break;
          case 0b11010000:
            break;
          case 0b11100000:
            break;
          case 0b11110000:
            break;
        }
      }
      else
      {
        byte myByte = mySerial.read();  //This is the protocol for reading new stuff
        if(isRealTimeCategory(myByte))
        {
          ;
        }
        else
        {
          if(myByte > 0)
      {
        digitalWrite(LED, HIGH);
      }
      else
      {
        digitalWrite(LED, LOW);
      }
        }
      }
    }
  }
}  //END

/**********************
//function declarations
**********************/

int isStatus(int b)
{
  if( (b & 0x80) == 0x80)
  {
    return 1;
  }
  else if( (b & 0x80) == 0)
  {
  return 0;
  }
}   

int isAftertouch(int b)
{
  if( (b & 0xf0) == 0xa0)
  {
    return 1;
  }
  else //if( (b & 0xf0) != 0xa0)
  {
    return 0;
  }
}

int isRealTimeCategory(int b)
  {
    if(b >= 0xF8)
    {
      return 1;
    }
    else
    {
      return 0;
    }
}

On top of this, I managed to tweak the code to output particular values to a shift register as I had initially intended. The method I use to differentiate between note and velocity bytes is less than ideal but works on my keyboard. Because of certain limitations it only works over a range of 13 notes but that's fine for now. Code and flowchart below.

//              MIDI_Input_0x02


#include <SoftwareSerial.h>             //The Attiny does not natively support the Serial library so this
                        //is the work-around.
SoftwareSerial mySerial(0,1);

const int latchPin = 2;
const int clockPin = 3;
const int dataPin = 4;

const int gate = 1;             //Note that initially, this was the tx pin.
                                //In this configuration absolutely every Attiny85 pin is used.
int isStatus();
int isAftertouch();
int isRealTimeCategory();

int result;
int dataByte;
int velocityByte;

//int noteApprox[13] = {1,3,5,7,9,12,14,17,20,23,26,29,32};
int noteApprox[13] = {32, 29, 26, 23, 20, 17, 14, 12, 9, 7, 5, 3, 1};
    //The above numbers are approximately 100 cents
    //apart from one another. This corresponds to notes of a scale.
    //They descend here because their corresponding
    //voltages will later become inverted in the circuit.

void setup()
{
  mySerial.begin(31250);

  pinMode(latchPin, OUTPUT);
  pinMode(clockPin, OUTPUT);
  pinMode(dataPin, OUTPUT);  
  pinMode(gate, OUTPUT);        //'Gate' controls the level of the
                    //envelope generators. The signal
                    //becomes inverted later.

  digitalWrite(gate, HIGH);     //A simple test to ensure the
  delay(1000);              //circuit is wired correctly
  digitalWrite(gate, LOW);
  delay(1000);

}

void loop()
{

  while(mySerial.available())
  {
    //This is the protocol for reading new stuff
    byte myByte = mySerial.read();      //Read the oldest byte in the buffer
    if(isRealTimeCategory(myByte))      //See Function Notes
    {
      ;
    }
    else
    {
      if(isStatus(myByte))               //See Function Notes
      {
        result = (myByte | 0x80);       //0b10000000
        switch(result)
        {
          case 0b10000000:
            digitalWrite(gate, LOW);            //0b10000000 is the note Off
            break;                      //message
          case 0b10010000:
            digitalWrite(gate, HIGH);           //10010000 is the note On
            break;                      //message
          case 0b10100000:          //Absolutely none of these are important.
            break;
          case 0b10110000:
            break;
          case 0b11000000:
            break;
          case 0b11010000:
            break;
          case 0b11100000:
            break;
          case 0b11110000:
            break;
        }
      }
      else                                     //This is the part where I assume it's a data byte.
      {                                        //this might be causing problems...
        if( (myByte > 48) && (myByte < 61) )         //<-- That's 13 notes. An octave and an extra 
        {
          digitalWrite(latchPin, LOW);
          shiftOut(dataPin, clockPin, MSBFIRST, noteApprox[myByte - 48]);   //Write to the shift
          digitalWrite(latchPin, HIGH);                     //register.
        }
        else
        {
          digitalWrite(gate, LOW);
        }

        byte myByte = mySerial.read();
        if(isRealTimeCategory(myByte))
        {
          ;
        }
        else
        {
          if(myByte > 0)                
      {
        digitalWrite(gate, HIGH);  //Velocity Byte
      }
      else
      {
        digitalWrite(gate, LOW);
          }
        }
      }
    }
  }
}  //END

/***************************
    function Declarations
***************************/

int isStatus(int b)         //Determines if the incoming byte is a Status Byte of some sort.
                    //Returns 1 if so and 0 if not.
{
  if( (b & 0x80) == 0x80)
  {
    return 1;
  }
  else if( (b & 0x80) == 0)
  {
  return 0;
  }
}   

int isRealTimeCategory(int b)           //Determines if the incoming byte is a Real Time Category byte
  {                 //I'm not positive what these *are* but they are only 1 byte long
    if(b >= 0xF8)           //a piece and easily detected (they're all greater than 11111000).
    {                           
      return 1;
    }
    else
    {
      return 0;
    }
}

/*              Bit Math Notes

    if(b & 0x80) == 0x80)

b    =  10000000
0x80 =  10010000
      & ________
        10000000 =/= 10010000

        Not a Note On byte. 
        Return 0.


b    =  10010000
0x80 =  10010000
      & ________
        10010000 == 10010000

        A Note On byte! Hooray!
        Return 1. 
*/

Flowchart

  • Your code still has issues in many cases. For example, if you get an "other" with the 0x80 bit set, you go back to your main loop but for (say) polyphonic pressure (0x82) you ignore the next byte which is the amount of pressure. That means the pressure amount could be interpreted as a note value next time around the loop. – Nick Gammon Aug 18 '15 at 20:59
  • Good catch. I hadn't put much thought into it seeing as my keyboard doesn't have polyphonic pressure. I'll look into it, thanks. – Patagonian Rat Aug 19 '15 at 19:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.