Created
August 18, 2018 23:03
-
-
Save piquan/8aaebbf4b22e7fbb5fdccdf248ed2f19 to your computer and use it in GitHub Desktop.
Quick and dirty MIDI note reader
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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