Close

MIDI I/O - Part 2 (Processing MIDI messages)

A project log for Vintage Toy Synthesiser

A wooden toy piano converted into a standalone digital synthesiser.

liam-laceyLiam Lacey 08/31/2018 at 07:110 Comments

(Original post date: 24/01/16)

In my last log I talked about the implementation of the electronics needed for adding a MIDI interface to my vintage toy synthesiser. As a suitable follow-on, within this post I thought I'd talk in-depth about MIDI message processing; specifically about five factors of the MIDI message format that make processing MIDI messages more complicated than it appears, or at least in regards to allowing full compatibility with all MIDI gear. As I'm not using any MIDI library (which would typically be used to handling the processing of MIDI messages) to develop the software for this project, I needed to write this code from scratch; C code which I have shared at the bottom of this blogpost.

Basics to MIDI Processing

MIDI is a form of serial communication, therefore bytes are transmitted one-byte at a time rather than in chunks or packets. Therefore to process MIDI messages coming from a serial port, each byte needs to be read and processed individually.

The first byte of any MIDI message, the 'status' byte, will always have a value of 128 or above, with the following 'data' bytes having a value of less than 128, therefore the first part to correctly processing MIDI messages is to check the value of each byte against this value - if the byte is 128 or above you know you have just received the start of a new MIDI message, or if the byte is 127 or below you that this byte is part of the previous MIDI message.

However the second part to correctly processing MIDI messages is to know the length of each MIDI message based on the status byte value, so that you know when you've received a full MIDI message which can now be used. Different MIDI messages have different lengths, and as the status byte represents the message type, this will indicate how many data bytes we should expect to receive after the status byte. Once we've received the correct amount of data bytes after a status byte, we know we've received a full MIDI message which can now be used within the software/system.

An example of using the above two rules in a MIDI processing algorithm:

  1. From a serial port you read a byte of value 176. Because it is a value greater than 127 you know it is a status byte, and the start of a new message, so you store this byte at the start of a message buffer.
  2. You look up what the status byte represents - it is CC message on MIDI channel 0. As it is a CC, you expect to receive two data bytes next to complete this message, and you flag that so far you have only received one out of three bytes.
  3. You read a second byte of value 1. As you've flagged that we've currently received just one byte of a CC message, this must be the first data byte, or the CC number. You store this value in the second index of the message buffer, and flag that so far you have received two out of three bytes.
  4. You read a third byte of value 23. As you've flagged that we've currently received two bytes of a CC message, this must be the second data byte, or the CC value. You store this value in the third index of the message buffer. As you have now received all three bytes of a CC message, you flag that this message have been fully received and can be used to trigger an event within your application.

The above rules mean that invalid message can be caught and discarded rather than the system attempting to use them. For example, if a new status byte is received before all the data bytes of the last message are received, you know to discard the previous message in the message buffer and start storing and processing a new message. Or if too many data bytes are received after a status byte, as you would have already processed the MIDI message and are waiting to receive a status byte to start processing a new message, any extra data bytes would just be ignored.

It is worth mentioning that there is one type of MIDI message that can't be processed in this way - System Exclusive (or SysEx) messages. However these are easier to process - they always start with status byte value 240, and end with a byte of value 247, with a variable number of data bytes in-between.

Advanced MIDI Processing

As mentioned at the start, there are some factors to the MIDI message format that mean processing MIDI messages isn't always as simple as just checking for a status byte and a message length. Here are five factors that could be unknown to some MIDI developers, with examples of how these factors can be processed in my example code at the bottom.

1. Note-on Note-off Messages

This factor is fairly well know to MIDI developers, but can be easily forgotten from time to time. Note-on and Note-off messages have their own set of status byte values, however 'note-off' messages can also be sent using the note-on message format but with a velocity value (2nd data byte) of zero. Many older MIDI controllers and synthesisers use this feature of MIDI in conjunction with the next factor I'll talk about, running status.

Therefore to correctly processing note-on messages, if it has a third byte of value 0, this message must actually be converted to or used as a note-off message (but with the same channel and note number).

2. Running Status

Running Status is a feature of MIDI that allows messages to be sent without the status byte, if the status byte is the same as that of the previous message. This feature was used in a lot of older MIDI equipment which had limited processing power available, as it allows less bytes to be sent. This explains why some MIDI keyboards send note-off messages as note-on messages, as it allows any number of keys to be pressed and released but without needing to send a new status byte on each event. Only Voice Category messages (e.g. note, CC, aftertouch) can be sent using running status.

To correctly process running status messages, after receiving a full voice category MIDI message, instead of setting the byte counter back to 0 at this point you set it to a value of 1. This will put your processing algorithm into a state where it thinks it has already received a status byte and is waiting for the messages data bytes. If a status byte is received instead at this point it just restarts processing a new message.

3. 14-Bit CCs

The majority of MIDI messages have 7-bit value bytes, due to data bytes not being able to have a value greater than 127. This isn't always enough resolution for control, therefore it is possibly to send 14-bit CC values by sending a pair of CCs instead. CC controller 0-31 can be used to send 14 bit values by following the CC with a second CC with a controller value of 32-63, where the combined 14-bit value is split between the value byte (3rd byte) of each message. For example, if you want to send a mod wheel message with a 14 bit resolution value, split the value into two 7 bit values (known as coarse and fine values), send the coarse value using a CC message with controller number (2nd byte) set to 0, and send the fine value using a CC message with a controller number set to 32.

Processing 14 bit CCs needs to be done one way or another, even if you only ever want to use 7 bit CCs. Therefore to correctly process CC messages you must always store the controller number of the previous CC you received, and if the new CC number is between 32 and 63 where the previous CC number is equal to the new CC number minus 32, flag that you have received a 14 bit CC value. It's then up to you whether you want to process this as a 14 bit CC or not. In my project/code I don't want to process 14 bit CCs so I just ignore the second CC (else my application thinks I've received two separate CCs and could do odd things based on this), however if you want to use 14 bit values it would be at this point that you combine the coarse and fine values to create a 14 bit number.

For more info on 14 bit MIDI CCs, including how they are encoded and decoded, see here.

4. RPNs and NRPNs

Another way that 14 bit resolution values can be sent using MIDI is using Registered Parameter Numbers (RPNs) and Non-Registered Parameter Numbers (NRPNs), which are sent as a succession of 3 or 4 specific CCs. RPN/NRPNs not only allow greater resolution of values, but also allow for a greater number of controller/parameter numbers. First an RPN/NRPN parameter number is sent using a pair of CCs (101 and 100 for RPNs, 99 and 98 for NRPNs), and then the parameter value is sent using either one or two further CCs (6 - course value, and 38 - fine value) depending on whether it is a 7-bit or 14-bit number. These CC numbers are specified for sending RPNs and NRPNs, and ideally should not be used for any other purpose in order to have full compatibility with other MIDI hardware and software.

To process RPNs/NRPNs an RPN/NRPN-specific message buffer needs to be used. If CC 101 or 99 is fully received you need to store the value of the CC and wait for the next parameter CC (CC 100 or 98), and once you have that combine the two received parameter numbers into a single parameter number, much like how 14 bit CC coarse and fine values are combined. Once you get the following CC 6 message you now have the full NRPN message to use as desired, though if the RPN/NRPN is followed by CC 38 you then need to adjust the value or this NRPN to include this fine value.

RPN/NRPN processing is something I have yet to add to my MIDI processing code. You can read more about RPNs and NRPNs here.

5. Intwined MIDI Messages

Most MIDI equipment won't start sending a new MIDI message until it has finished sending the previous one, however this isn't the case for all MIDI gear. You may find that some MIDI devices will send Realtime Category messages (e.g clock, active sensing) intwined within Voice Category messages, which is valid and needs to be processed correctly.

For example, the following is an example of a series of MIDI messages, with the bytes received in the order you'd expect:

MIDI_message_stream

This example shows four CC messages (status byte in red, data bytes in orange) with several Clock Timing messages (one byte MIDI messages) (in black) in between.

However here is an example of the same set of messages but with the bytes sent in a different order:

midi_message_stream

All the same messages are sent, but the clock messages are sent in the middle of CC messages. Unfortunately this is a valid stream of MIDI messages, and without proper processing only the last CC message in the previous example would have been processed correctly.

When I was working on an algorithm to process intwined MIDI messages correctly, this webpage proved to be very helpful. Even though it is mainly talking about running status, it offers an answer on how to handle intwined messages:

A recommended approach for a receiving device is to maintain its "running status buffer" as so:
  1. Buffer stores the status when a Voice Category Status (ie, 0x80 to 0xEF) is received.
  2. Nothing is done to the buffer when a RealTime Category message is received.

As can be seen in my example code below, only when a voice category message is received do I update my running status variable; if a clock message is received I just store this in my message buffer and flag that I've received a clock message. If the clock message was received within a voice message and I then start receiving the rest of the message, as the running status variable is still equal to the last received voice message I will keep processing this message.

Example MIDI Processing Code

This is the C code I'm currently using to process MIDI messages coming from the MIDI serial port on my BBB. It is a function that is called each time I receive a byte, which the byte is passed into, and returns a non-zero number when a full message has been received indicating the message type. I'm not suggesting the is the best or definitive way of processing MIDI messages, and it is currently only processing MIDI messages I care about (note, CC, aftertouch, pitch bend, program change, clock, and SysEx), however it is a good working example of how it can be done.

#define MIDI_NOTEOFF 0x80
#define MIDI_NOTEON 0x90
#define MIDI_PAT 0xA0
#define MIDI_CC 0xB0
#define MIDI_PROGRAM_CHANGE 0xC0
#define MIDI_CAT 0xD0
#define MIDI_PITCH_BEND 0xE0

#define MIDI_NOTEOFF_MAX 0x8F
#define MIDI_NOTEON_MAX 0x9F
#define MIDI_PAT_MAX 0xAF
#define MIDI_CC_MAX 0xBF
#define MIDI_PROGRAM_CHANGE_MAX 0xCF
#define MIDI_CAT_MAX 0xDF
#define MIDI_PITCH_BEND_MAX 0xEF

#define MIDI_CLOCK 0xF8
#define MIDI_CLOCK_START 0xFA
#define MIDI_CLOCK_CONTINUE 0xFB
#define MIDI_CLOCK_STOP 0xFC

#define MIDI_SYSEX_START 0xF0
#define MIDI_SYSEX_END 0xF7

uint8_t ProcInputByte (uint8_t input_byte, uint8_t message_buffer[], uint8_t *byte_counter, uint8_t *running_status_value, uint8_t *prev_cc_num)
{
    /*
     A recommended approach for a receiving device is to maintain its "running status buffer" as so:
     Buffer is cleared (ie, set to 0) at power up.
     Buffer stores the status when a Voice Category Status (ie, 0x80 to 0xEF) is received.
     Buffer is cleared when a System Common Category Status (ie, 0xF0 to 0xF7) is received - need to implement this fully!?
     Nothing is done to the buffer when a RealTime Category message (ie, 0xF8 to 0xFF, which includes clock messages) is received.
     Any data bytes are ignored when the buffer is 0.
     
     (http://www.blitter.com/~russtopia/MIDI/~jglatt/tech/midispec/run.htm)
     
     */
    
    //static uint8_t running_status_value = 0;
    //static uint8_t prev_cc_num = 127; //don't init this to 0, incase the first CC we get is 32, causing it to be ignored!
    uint8_t result = 0;
    
    //=====================================================================
    //If we've received the start of a new MIDI message (a status byte)...
    
    if (input_byte >= MIDI_NOTEOFF)
    {
        //If it's a Voice Category message
        if (input_byte >= MIDI_NOTEOFF && input_byte <= MIDI_PITCH_BEND_MAX)
        {
            message_buffer[0] = input_byte;
            *byte_counter = 1;
            result = 0;
            
            *running_status_value = message_buffer[0];
        }
        
        //If it's a clock message
        else if (input_byte >= MIDI_CLOCK && input_byte <= MIDI_CLOCK_STOP)
        {
            //Don't do anything with MidiInCount or *running_status_value here,
            //so that running status works correctly.
            
            message_buffer[0] = input_byte;
            result = input_byte;
        }
        
        //If it's the start of a sysex message
        else if (input_byte == MIDI_SYSEX_START)
        {
            message_buffer[0] = input_byte;
            *byte_counter = 1;
        }
        
        //If it's the end of a sysex
        else if (input_byte == MIDI_SYSEX_END)
        {
            message_buffer[*byte_counter] = input_byte;
            *byte_counter = 0;
            
            result = MIDI_SYSEX_START;
        }
        
        // If any other status byte, don't do anything
        
    } //if (input_byte >= MIDI_NOTEOFF)
    
    //=====================================================================
    //If we're received a data byte of a non-sysex MIDI message...
    //FIXME: do I actually need to check *byte_counter here?
    
    else if (input_byte < MIDI_NOTEOFF && message_buffer[0] != MIDI_SYSEX_START && *byte_counter != 0)
    {
        switch (*byte_counter)
        {
            case 1:
            {
                //Process the second byte...
                
                //Check *running_status_value here instead of message_buffer[0], as it could be possible
                //that we are receiving running status messages entwined with clock messages, where
                //message_buffer[0] will actually be equal to MIDI_CLOCK.
                
                //TODO: process NRPNs, correctly (e.g. process 9 byte NRPN, and then as 12 bytes if the following CC is 0x26)
                
                //if it's a channel aftertouch message
                if (*running_status_value >= MIDI_CAT && *running_status_value <= MIDI_CAT_MAX)
                {
                    message_buffer[1] = input_byte;
                    result = MIDI_CAT;
                    
                    //set the correct status value
                    message_buffer[0] = *running_status_value;
                    
                    //wait for next data byte if running status
                    *byte_counter = 1;
                }
                
                //if it's a program change message
                else if (*running_status_value >= MIDI_PROGRAM_CHANGE && *running_status_value <= MIDI_PROGRAM_CHANGE_MAX)
                {
                    message_buffer[1] = input_byte;
                    result = MIDI_PROGRAM_CHANGE;
                    
                    //set the correct status value
                    message_buffer[0] = *running_status_value;
                    
                    //wait for next data byte if running status
                    *byte_counter = 1;
                }
                
                //else it's a 3+ byte MIDI message
                else
                {
                    message_buffer[1] = input_byte;
                    *byte_counter = 2;
                    
                    //set the correct status value
                    message_buffer[0] = *running_status_value;
                }
                
                break;
            }
                
            case 2:
            {
                //Process the third byte...
                
                result = 0;
                
                //TODO: process NRPNs, correctly
                
                //if it's not zero it's a note on
                if (input_byte && (*running_status_value >= MIDI_NOTEON && *running_status_value <= MIDI_NOTEON_MAX))
                {
                    //3rd byte is velocity
                    message_buffer[2] = input_byte;
                    result = MIDI_NOTEON;
                    
                    //set the correct status value
                    message_buffer[0] = *running_status_value;
                }
                
                //if it's a note off
                else if ((*running_status_value >= MIDI_NOTEOFF && *running_status_value <= MIDI_NOTEOFF_MAX) ||
                         (!input_byte && (*running_status_value >= MIDI_NOTEON && *running_status_value <= MIDI_NOTEON_MAX)))
                {
                    //3rd byte should be zero
                    message_buffer[2] = 0;
                    result = MIDI_NOTEOFF;
                    
                    //set the correct status value
                    message_buffer[0] = *running_status_value;
                }
                
                //if it's a CC
                else if (*running_status_value >= MIDI_CC && *running_status_value <= MIDI_CC_MAX)
                {
                    //if we have got a 32-63 CC (0-31 LSB/fine CC),
                    //and the last CC we received was the MSB/coarse CC pair
                    if (message_buffer[1] >= 32 && message_buffer[1] <= 63 && (*prev_cc_num == (message_buffer[1] - 32)))
                    {
                        //Don't do anything. Right now if this is the case we just want to ignore it.
                        //However in the future we may want to process coarse/fine CC pairs to
                        //control parameters at a higher resolution.
                        printf ("[VB] Received CC num %d directly after CC num %d, so ignoring it\r\n", message_buffer[1], *prev_cc_num);
                    }
                    
                    else
                    {
                        message_buffer[2] = input_byte;
                        result = MIDI_CC;
                        
                        //set the correct status value
                        message_buffer[0] = *running_status_value;
                        
                    } //else
                    
                    //store this CC num as the previously received CC
                    *prev_cc_num = message_buffer[1];
                }
                
                //if it's a poly aftertouch message
                else if (*running_status_value >= MIDI_PAT && *running_status_value <= MIDI_PAT_MAX)
                {
                    message_buffer[2] = input_byte;
                    result = MIDI_PAT;
                    
                    //set the correct status value
                    message_buffer[0] = *running_status_value;
                }
                
                //if it's a pitch bend message
                else if (*running_status_value >= MIDI_PITCH_BEND && *running_status_value <= MIDI_PITCH_BEND_MAX)
                {
                    message_buffer[2] = input_byte;
                    result = MIDI_PITCH_BEND;
                    
                    //set the correct status value
                    message_buffer[0] = *running_status_value;
                }
                
                // wait for next data byte (if running status)
                *byte_counter = 1;
                
                break;
            }
                
            default:
            {
                break;
            }
                
        } //switch (*byte_counter)
        
    } //else if (input_byte < MIDI_NOTEOFF && message_buffer[0] != MIDI_SYSEX_START && *byte_counter != 0)
    
    //if we're currently receiving a sysex message
    else if (message_buffer[0] == MIDI_SYSEX_START)
    {
        //add data to the sysex buffer
        message_buffer[*byte_counter] = input_byte;
        *byte_counter++;
    }
    
    return result;
}

Discussions