Jacob Vosmaer's blog

A small MIDI parser

2024-01-12

In this post I will talk through the code of a pet project of mine, a small MIDI parser library written in C.

MIDI

MIDI stands for "Musical Instrument Digital Interface". It is a standard from 1983 that describes both an electrical interface between musical instruments and the digital protocol used to communicate over that interface.

The electrical part is a UART current loop through an opto-isolator on the receiving end. The opto-isolator provides galvanic isolation which prevents ground loops.

The digital protocol consists of variable length messages of 1 up to 3 bytes. The first byte is the "status" which indicates the type of the message. The optional second and third bytes are the "data" bytes. The standard defines how many data bytes follow each status byte. For example, status 0x90 is always followed by 2 data bytes, status 0xf8 is followed by 0 data bytes, etc.

Complications

The MIDI protocol has several complications of which I will mention three. Firstly, status byte 0xf0 ("start of system exclusive") is followed by arbitrarily many data bytes and terminated by 0xf7. You will see below how I chose to unify this with the idea that "MIDI messages are 1-3 bytes long".

Secondly, the MIDI protocol has a rudimentary form of compression called "running status". The sender can sometimes omit the status byte if it sends a sequence of messages which all have the same status. For example 0xc0 0x01 0x02 0x03 is a legal compressed form of 0xc0 0x01 0xc0 0x02 0xc0 0x03.

Thirdly, multi-byte messages may get fragmented by some of the single-byte messages. A good example is the clock tick message 0xf8. To reduce clock jitter, the sender is allowed to send 0xf8 in the middle of another message. A MIDI parser must not get confused by this.

Why write a MIDI parser

My first embedded programming project was to rewrite the code of the Yocto that translates MIDI messages into drum triggers. Because it seemed like fun at the time I decided to write my own MIDI parser in C.

I don't have a formal education in computer science so this was probably one of the first times I wrote a parser. It also is one of the earliest things I wrote in C.

Over time this parser has proven to be a fun piece of code to come back to and refactor. Below I will talk you through the current version.

The header file

The header contains three definitions.


typedef struct midi_message {
    uint8_t status;
    uint8_t data[2];
} midi_message;

The parser emits midi_message structs. A MIDI message consists of 1 "status" byte (which has its highest bit set to 1) followed by 0, 1 or 2 "data" bytes (with their highest bit set to 0).


typedef struct midi_parser {
    uint8_t status;
    uint8_t previous;
} midi_parser;

The midi_parser struct contains the internal state of the parser. Because MIDI messages are spread over multiple bytes, and the bytes come in one at a time, the parser must "remember" what it has seen before. It needs 2 bytes of memory for this.

Users of the library do not need to know the internals of midi_parser but we will look at it in detail below.


midi_message midi_read(midi_parser *p, uint8_t b);

The final piece of the interface is the midi_read() function which consumes a byte b of the incoming MIDI stream, updates the start of parser p, and returns a midi_message struct. Not every call to midi_read() yields a message. If there is no message, the .status field of the returned struct is 0.

Example code

This is straight from the README.


int main(void) {
  int c;
  midi_parser parser = {0};

  puts("status data0 data1");
  while ((c = getchar()) != EOF) {
    midi_message msg = midi_read(&parser, c);
    if (msg.status)
      printf("  0x%02x  0x%02x  0x%02x\n", msg.status,
        msg.data[0], msg.data[1]);
  }
}

Output:


$ printf '\x90\x11\x22\x33\x44' | ./example
status data0 data1
  0x90  0x11  0x22
  0x90  0x33  0x44

Note how the example feeds 5 bytes into the parser but the parser only returns 2 messages. Part of the time, the return msg has its .status set to zero. This example also showcases support for MIDI "running status": in the second message, the status byte is not sent on the wire because it is remembered from the first message.

The implementation

Here is the entire implementation.


midi_message midi_read(midi_parser *p, uint8_t b) {
    enum { DATA0_PRESENT = 0x80 };
    midi_message msg = {0};

    if (b >= 0xf8) {
        msg.status = b;
    } else if (b >= 0xf4) {
        msg.status = b;
        p->status = 0;
    } else if (b >= 0x80) {
        p->status = b;
        p->previous = b == 0xf0 ? b : 0;
    } else if ((p->status >= 0xc0 && p->status < 0xe0) ||
           (p->status >= 0xf0 && p->status != 0xf2)) {
        msg.status = p->status;
        msg.data[0] = b;
        msg.data[1] = p->previous;
        p->previous = 0;
    } else if (p->status && p->previous & DATA0_PRESENT) {
        msg.status = p->status;
        msg.data[0] = p->previous ^ DATA0_PRESENT;
        msg.data[1] = b;
        p->previous = 0;
    } else {
        p->previous = b | DATA0_PRESENT;
    }

    return msg;
}

I will talk through it step by step below.


    if (b >= 0xf8) {
        msg.status = b;

Some MIDI messages have "real time" priority and no data bytes. These get returned immediately by the parser. One example is 0xf8 which is a MIDI clock tick. These real time messages do not change the state of the parser.


    } else if (b >= 0xf4) {
        msg.status = b;
        p->status = 0;

Next we have some messages again without data but which do reset the parser. An example is 0xf7 which is "end of system exclusive data". These messages get emitted immediately.


    } else if (b >= 0x80) {
        p->status = b;
        p->previous = b == 0xf0 ? b : 0;

Next we come to the class of messages which have 1 or 2 data bytes. For these, we must remember the status byte and we cannot yet emit a message, so msg.status remains unset.

There is a special case for status 0xf0 which is the start of a system exclusive message. System exclusive messages are a stream of arbitrarily many 7-bit values, starting with 0xf0 and ending with 0xf7. For the API of midi_read() I chose to return these as follows.

Suppose we have a system exclusive ("sysex") message 0xf0 0x01 0x02 0x03 0xf7. When you feed this to the parser it will return 4 message structs: {0xf0, 0x01, 0xf0}, {0xf0, 0x02, 0x00}, {0xf0, 0x03, 0x00} and {0xf7, 0x00, 0x00}. The first message has 0xf0 as its second data byte. This is to signal the start of a sysex stream.

To implement this special behavior for 0xf0, we store a value in p->previous above.


    } else if ((p->status >= 0xc0 && p->status < 0xe0) ||
           (p->status >= 0xf0 && p->status != 0xf2)) {
        msg.status = p->status;
        msg.data[0] = b;
        msg.data[1] = p->previous;
        p->previous = 0;

When we reach this branch, the parser p->status field is set, the message type has 1 data byte, and the data byte just came in in b. We know that b is not a status byte because it is less than 0x80. In this case we emit a message. Note how we set p->previous to 0 so that in the special status 0xf0 case, only the first message has 0xf0 as its second data byte.


    } else if (p->status && p->previous & DATA0_PRESENT) {
        msg.status = p->status;
        msg.data[0] = p->previous ^ DATA0_PRESENT;
        msg.data[1] = b;
        p->previous = 0;

In this branch, the parser has a p->status value which carries 2 data bytes, and b holds the second data byte. We can tell it is the second one because the DATA0_PRESENT flag was set when the first data byte came in. In this case we emit a message and reset p->previous.


    } else {
        p->previous = b | DATA0_PRESENT;
    }

In this final branch, we know that p->status indicates we need 2 data bytes, and b holds the first data byte. In case b is zero, we would not be able to detect that b is set during the next call to midi_read(). The DATA0_PRESENT flag avoids this problem by making sure p->previous is not zero.

Why no callbacks

Many examples of MIDI parsers I see use callbacks. This is indeed a natural solution for dealing with the problem that when you feed a byte to the parser, sometimes you get a message, and sometimes you do not.

However, I feel that a callback API would have been bigger, and clunky in the case when you don't need it. Callbacks in C are clunky because you have to define them as top-level functions. It is not hard to embed this library in a program that does use callbacks but now you have the choice not to use callbacks.

Conclusion

Thank you for reading this far. This was all very nerdy.

To my knowledge this parser is correct. Let me know if you think differently. The library is MIT licensed in the hope that others can benefit from using it.

Tags: music c

IndexContact