MIDI Tutorial

Pages
Contributors: Byron J.
Favorited Favorite 24

Implementing MIDI

Working with MIDI messages directly is much easier if you have a strong grasp on using binary operators to manipulate bytes. You can use bitwise AND and OR operations to mask off the status bit, and extract the channel nybble value. Bitwise-shifts are useful to manipulate the 14-bit bender data.

The code snippets below were written for Arduino. You'll notice that bytes are transmitted using the Serial.write() method, which transmits the raw binary data, rather than converting it into a string like Serial.print() does. We're also using the Arduino-defined byte data type, which is defined as an unsigned 8-bit value. This allows us to properly access the status bit, whereas the char type is signed -- doing binary operations on signed values sometimes leads to subtle errors in the manipulating the MSB.

The software examples below are intentionally incomplete, as we'll introduce a more complete and user-friendly option below.

Message Generation

To send a MIDI message, simply send the appropriate bytes in the right order.

System Realtime messages are the easiest to send, just feed the byte to the output.

language:c
void send_sys_rt(byte rt_byte)
{
    // We are assuming that the input is in a valid range:
    //  System RT bytes are betwen 0xF8 and 0xFF.
    //
    // More realistic code might validate each input and return an error if out of range

    Serial.write(rt_byte);
}

Channel messages are a little more complex. To form the first byte, you'd set the MS nybble to the status value, then subtract one from the channel, and put that value in the channel nybble. Send the first byte, then follow it with the appropriate number of data bytes. This snippet also includes running status -- the sender keeps track of the last status byte sent in a global varible.

language:c
static byte last_status = 0;

void send_channel_message(byte command,
                          byte channel,
                          int num_data_bytes,
                          byte * data)
{
    // We are assuming that all of the inputs are in a valid range:
    //  Acceptable commands are 0x80 to 0xF0.
    // Channels must be between 1 and 16.
    // num_data_bytes should be either 1 or 2
    // data should be an array of 1 or 2 bytes, with each element constrained 
    // to the 0x0 to 0x7f range.
    //
    // More realistic code might validate each input and return an error if out of range

    byte first;

    // Combine MS-nybble of command with channel nybble.
    first = (command & 0xf0) | ((channel - 1) & 0x0f);

    if(first != last_status)
    {
        Serial.write(first);
        last_status = first;
    }

    // Then send the right number of data bytes
    for(int i = 0; i < num_data_bytes; i++)
    {
        Serial.write(data[i]);
    }
}

Finally, sending system exclusive messages requires sending an identifier and data payload that are both of variable length. It must begin and also end with the proper status bytes.

language:c
void send_system_expusive(int num_id_bytes,
                          byte * id,
                          int num_data_bytes,
                          byte * data)
{
    // We are assuming that all of the inputs are in a valid range:
    // num_id_bytes should be either 1 or 3
    // num_data_bytes is not constrained, but a practical limitation might be 1000 bytes.
    // Id and data values should be constrained to the 0x0 to 0x7f range.
    //
    // More realistic code might validate each input and return an error if out of range



    // send start-of-exclusive
    Serial.write(0xF0);

    // Then send the identifier bytes
    for(int i = 0; i < num_data_bytes; i++)
    {
        Serial.write(data[i]);
    }
    // Then send the right number of data bytes
    for(int i = 0; i < num_data_bytes; i++)
    {
        Serial.write(data[i]);
    }

    // send end-of-exclusive
    Serial.write(0xF7);

}

Message Parsing

Receiving and parsing the incoming MIDI stream requires more sophistication. While features like running status, implicit note off and active sense are optional from the sender, the receiver needs to be prepared to deal with them.

A simplified finite state machine for parsing MIDI is shown below.

MIDI Bubble Diagram

There are a few things to note

  • The system initializes and starts receiving bytes. Until it sees a status byte, the input is meaningless and is discarded. Realistically, status bytes occur fairly frequently, so this state is a temporary condition.
  • The "Dispatch Status" state decides how to handle messages based on how long they are.
    • Most status messages have one or two bytes of data.
    • System Exclusive messages can be any length, with the end marked by a "end of exclusive" byte.
  • Running status is handled by the Dispatch Status state, in conjunction with the status handling branches.
    • Dispatch Status stores the status byte, so it can be recalled when running status is used.
    • When the one-byte and two-byte branches of the FSM see repeated data bytes, they repeatedly apply the stored status.
  • At any point that the FSM is expecting a data byte, a new status could arrive instead. The previous status is discarded, and the new status dispatched instead.

You'll notice that all of the "receive bytes" states are illustrated with a double-circle. This is shorthand for more sophisticated behavior. As we discussed previously, system realtime bytes can occur at any point. We could be expecting the velocity byte of a Note On message and get a clock byte instead. The clock is allowed to non-disruptively interrupt the parsing. The details of the receive states are shown below.

Realtime Handling Within Receive States

From the perspective of the parsing FSM, system realtime bytes don't count as data or status bytes, they just get handled as they arrive.

The diagram shown above illustrates how to decode the bytes in the MIDI stream, but it doesn't make any implication about how the receiver will respond to them. The verb "handle" is intentionally vague, as each type of device might respond differently.

Arduino Library

The FSM above is pretty complicated. Writing and debugging it means developing a pretty serious piece of software.

But there's an easier way. Github user FortySevenEffects has written an Arduino MIDI library that makes reading and writing MIDI messages much easier. The library is flexible, and it can be configured to fit different applications.

  • It can use hardware or software serial ports (leaving the hardware serial port for printing debug messages!).
  • Incoming messages can be received by polling, or callbacks for specific messages can be installed.
  • The library can filter for messages on a specific midi channel, or receive on all channels.
  • It implements an optional "soft thru" port that can be configured to echo the input back to the output port.

It also has detailed documentation in doxygen format.

For some practical examples that demonstrate this library, take a look at the hookup guide for the SparkFun MIDI Shield.

Devising Your Own Implementation Chart

In the messages section, we discussed the MIDI implementation chart. If you're implementing a MIDI device, you should consider writing your own chart, so you know what messages to handle. Most devices won't implement every message, and for messages it does implement, it may still choose to ignore some. For instance, a tone generator will handle note on and off messages, but only those on its MIDI channel. For statuses that are not needed, you can simply discard the messages.

If you elect to discard messages, it's advisable that you still handle the System Reset message (0xff) - it's used as a "panic switch" in situations that you need things to be silent, ASAP!