Skip to content

Instantly share code, notes, and snippets.

@piquan
Created August 18, 2018 23:03
Show Gist options
  • Save piquan/8aaebbf4b22e7fbb5fdccdf248ed2f19 to your computer and use it in GitHub Desktop.
Save piquan/8aaebbf4b22e7fbb5fdccdf248ed2f19 to your computer and use it in GitHub Desktop.
Quick and dirty MIDI note reader
// Useful link:
// http://www.personal.kent.edu/~sbirch/Music_Production/MP-II/MIDI/an_introduction_to_midi_contents.htm
#include <assert.h>
#include <err.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// BEGIN PUBLIC API
typedef enum {
kNoteOn, // Velocity will be non-zero
kNoteOff, // Possibly a "note on" with velocity 0, which is a note off
kError, // Possibly premature EOF; use feof and ferror
kEOF // MIDI "track end" marker found
} EventType;
typedef struct {
EventType event_type;
double delta_time; // Seconds since previous returned event
int channel; // MIDI channel, i.e., instrument; 0-15; ok to ignore
int note_value; // 0x3C == middle C; cf http://bit.ly/2ONtijc
int note_velocity; // Always 0 for note off
} MidiEvent;
typedef struct MidiFile MidiFile; // Private internal structure
// Initializes a caller-owned MidiFile structure, based on a stdio FILE*.
// The caller is expected to keep a reference to the FILE*, so it can
// close it later. Returns true on success.
bool MidiOpen(MidiFile* midi_file, FILE* stdio_file);
// Get the next note.
MidiEvent MidiRead(MidiFile* midi_file);
// END OF PUBLIC API
struct MidiFile {
FILE* fh;
double tick_length;
int accumulated_ticks;
int running_status;
};
static bool MidiReadBytes(MidiFile *midi_file, void *ptr, size_t len) {
size_t len_read = fread(ptr, 1, len, midi_file->fh);
return len_read == len;
}
static bool MidiSkipBytes(MidiFile *midi_file, size_t len) {
int rv = fseek(midi_file->fh, len, SEEK_CUR);
return rv != -1;
}
// Per the MIDI spec, this will return at most 0x0FFFFFFF. It will
// return -1 on error.
static int MidiReadVarint(MidiFile *midi_file) {
int rv = 0;
uint8_t varint_byte;
do {
if (!MidiReadBytes(midi_file, &varint_byte, 1))
return -1;
rv = (rv << 7) | (varint_byte & 0x7f);
} while (varint_byte & 0x80);
if (rv < 0 || rv > 0x0FFFFFFF)
return -1;
return rv;
}
static ssize_t MidiSeekToChunk(MidiFile *midi_file, const char* name) {
char chunk_type[4];
uint32_t chunk_len_be;
if (!MidiReadBytes(midi_file, &chunk_type, sizeof(chunk_type)) ||
!MidiReadBytes(midi_file, &chunk_len_be, sizeof(chunk_len_be)))
return -1;
uint32_t chunk_len = be32toh(chunk_len_be);
if (0 == strncmp(chunk_type, name, sizeof(chunk_type)))
return chunk_len;
if (!MidiSkipBytes(midi_file, chunk_len))
return -1;
return MidiSeekToChunk(midi_file, name);
}
bool MidiOpen(MidiFile* midi_file, FILE* stdio_file) {
midi_file->fh = stdio_file;
#ifndef RAW_MIDI_DEVICE
ssize_t mthd_len = MidiSeekToChunk(midi_file, "MThd");
if (mthd_len == -1)
return false;
if (mthd_len < 6) // Not allowed in MIDI
return false;
uint16_t format_be;
uint16_t tracks_be;
uint16_t division_be;
if (!MidiReadBytes(midi_file, &format_be, sizeof(format_be)) ||
!MidiReadBytes(midi_file, &tracks_be, sizeof(tracks_be)) ||
!MidiReadBytes(midi_file, &division_be, sizeof(division_be)))
return false;
int format = be16toh(format_be);
//int tracks = be16toh(tracks_be); // We don't use this
uint16_t division = be16toh(division_be);
if (!MidiSkipBytes(midi_file, mthd_len - 6))
return false;
// The "division" is how MIDI files implement temporal resolution.
// The entire MIDI time system is designed for very precise timing
// across multiple systems that implement different timing
// mechanisms, including things like SMPTE time and sequencer note
// position. We use a simplified version.
//
// For a reasonable overview, see also https://mido.readthedocs.io/en/latest/midi_files.html#about-the-time-attribute
if (division & 0x8000) {
assert(0 && "SMPTE-based timing not implemented");
} else {
// FIXME We're assuming 120 BPM here (the default at the start of
// the file); we don't process tempo change events.
midi_file->tick_length = 2.0 / division;
}
switch (format) {
case 0: { // Single track; play the first track.
break;
}
case 1: {
// Multitrack, with tempo map; we'll just play the first after the map.
// FIXME We should read the tempo map.
ssize_t tempo_map_len = MidiSeekToChunk(midi_file, "MTrk");
if (tempo_map_len == -1)
return false;
if (!MidiSkipBytes(midi_file, tempo_map_len))
return false;
}
case 2: { // Multitrack, indepedent; we'll play the first track.
break;
}
default: { // Unknown format
return false;
}
}
#endif
// Seek to the first track.
ssize_t mtrk_len = MidiSeekToChunk(midi_file, "MTrk");
if (mtrk_len == -1)
return false;
midi_file->accumulated_ticks = 0;
return true;
}
MidiEvent MidiRead(MidiFile* midi_file) {
int delta_ticks = MidiReadVarint(midi_file);
if (delta_ticks == -1)
return (MidiEvent){.event_type = kError};
midi_file->accumulated_ticks += delta_ticks;
uint8_t first_byte;
if (!MidiReadBytes(midi_file, &first_byte, 1))
return (MidiEvent){.event_type = kError};
if (first_byte == 0xf0 || first_byte == 0xf7) {
// SysEx event.
// Note that in an SMF (.mid) file, the term "SysEx" has a
// different meaning than in a physical MIDI stream. In a SMF
// file, it's an embedding frame that holds MIDI System messages
// (Real-Time, Universal, and Exclusive), or really any other
// messages. We don't care about any of them.
midi_file->running_status = 0;
int event_len = MidiReadVarint(midi_file);
if (event_len == -1)
return (MidiEvent){.event_type = kError};
if (!MidiSkipBytes(midi_file, event_len))
return (MidiEvent){.event_type = kError};
// Go on to the next event
return MidiRead(midi_file);
}
if (first_byte == 0xff) {
// Meta event
uint8_t meta_event_type;
if (!MidiReadBytes(midi_file, &meta_event_type, 1))
return (MidiEvent){.event_type = kError};
int event_len = MidiReadVarint(midi_file);
if (event_len == -1)
return (MidiEvent){.event_type = kError};
if (meta_event_type == 0x2f)
// End of track; the only meta event we care about.
return (MidiEvent){.event_type = kEOF};
if (!MidiSkipBytes(midi_file, event_len))
return (MidiEvent){.event_type = kError};
// Go on to the next event
return MidiRead(midi_file);
}
uint8_t event_type;
uint8_t first_data_byte;
// Determine whether we're using the running status or have a new
// event type.
if (first_byte & 0x80) {
// New event type; update running status and read the first data byte.
event_type = midi_file->running_status = first_byte;
if (!MidiReadBytes(midi_file, &first_data_byte, 1))
return (MidiEvent){.event_type = kError};
} else {
// Using running status
event_type = midi_file->running_status;
first_data_byte = first_byte;
}
switch (event_type & 0xf0) {
// We get the easy ones out of the way first.
case 0xa0: // Polyphonic key pressure (aftertouch)
case 0xb0: // Controller change
case 0xe0: // Pitch bend
// These take two data bytes, and we've read one.
// Skip the remaining one.
if (!MidiSkipBytes(midi_file, 1))
return (MidiEvent){.event_type = kError};
// FALLTHRU
case 0xc0: // Program change
case 0xd0: // Channel key pressure (aftertouch)
// These only take one data byte, which we've read, so we're done.
// Proceed to the next message.
return MidiRead(midi_file);
case 0xf0:
// System real-time events can't be in MIDI files, or rather,
// they have to be embedded in a SMF SysEx event (0xf0 or 0xf7),
// which we handled above.
return (MidiEvent){.event_type = kError};
case 0x80: // Note off
case 0x90: { // Note on
// Similar enough that most of the code is shared.
uint8_t velocity;
if (!MidiReadBytes(midi_file, &velocity, 1))
return (MidiEvent){.event_type = kError};
MidiEvent rv;
rv.event_type = kNoteOff;
rv.delta_time = midi_file->accumulated_ticks * midi_file->tick_length;
midi_file->accumulated_ticks = 0;
rv.channel = (event_type & 0x0f) + 1;
rv.note_value = first_data_byte;
if ((((event_type & 0xf0) == 0x90) && (velocity == 0)) ||
((event_type & 0xf0) == 0x80)) {
// Note on message with velocity 0 is interpreted as a note
// off. With a note off message, we report back a velocity 0
// anyway, since the synthesizer we're feeding doesn't do
// anything with release velocities, and reporting note off with
// velocity 0 may make it easier.
rv.event_type = kNoteOff;
rv.note_velocity = 0;
} else {
rv.event_type = kNoteOn;
rv.note_velocity = velocity;
}
return rv;
}
}
// Unreachable
abort();
}
int main(int argc, char *argv[]) {
if (argc != 2)
errx(1, "usage: nickmid foo.mid");
FILE* fh = fopen(argv[1], "rb");
if (fh == NULL)
err(1, "%s", argv[1]);
MidiFile mf;
bool open_ok = MidiOpen(&mf, fh);
if (!open_ok)
errx(1, "%s: open error", argv[1]);
double total_time = 0;
for (;;) {
MidiEvent ev = MidiRead(&mf);
switch (ev.event_type) {
case kNoteOn:
total_time += ev.delta_time;
printf("%7.3f[%02i] Note %#02x ON : velocity %#02x\n",
total_time, ev.channel, ev.note_value, ev.note_velocity);
break;
case kNoteOff:
total_time += ev.delta_time;
printf("%7.3f[%02i] Note %#02x OFF\n",
total_time, ev.channel, ev.note_value);
break;
case kError:
if (feof(fh)) {
errx(1, "%s: premature end-of-file", argv[1]);
} else if (ferror(fh)) {
errx(1, "%s: read error", argv[1]);
} else {
errx(1, "%s: invalid MIDI data", argv[1]);
}
case kEOF:
return 0;
default:
abort(); // canthappen
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment