blob: 3e8588b500c644343916c0fec5cc5eff7a9230fd [file] [log] [blame] [raw]
/*
A simple MIDI parsing library
Copyright (c) 2014, Mateusz Viste
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdlib.h> /* malloc */
#include <string.h> /* strcmp() */
#include "mem.h"
#include "midi.h" /* include self for control */
/* PRIVATE ROUTINES USED FOR INTERNAL PROCESSING ONLY */
/* fetch a variable length quantity value from a given offset. returns number of bytes read */
static int midi_fetch_variablelen_fromfile(FILE *fd, unsigned long *result) {
int bytebuff;
int offset = 0;
*result = 0;
for (;;) {
bytebuff = fgetc(fd);
*result <<= 7;
*result |= (bytebuff & 127);
if ((bytebuff & 128) == 0) break;
}
return(offset);
}
/* reads a MIDI file and computes a map of chunks (ie a list of offsets) */
static int midi_getchunkmap(FILE *fd, struct midi_chunkmap_t *chunklist, int maxchunks) {
int chunkid, i;
unsigned char hdr[8];
unsigned long readres;
/*unsigned long chunklen;*/
for (chunkid = 0; chunkid < maxchunks; chunkid++) {
readres = fread(hdr, 1, 8, fd);
if (readres != 8) break;
chunklist[chunkid].offset = ftell(fd);
/* read chunk's id */
for (i = 0; i < 4; i++) chunklist[chunkid].id[i] = hdr[i];
chunklist[chunkid].id[4] = 0; /* string terminator */
/* compute the length */
chunklist[chunkid].len = 0;
for (i = 4; i < 8; i++) {
chunklist[chunkid].len <<= 8;
chunklist[chunkid].len |= hdr[i];
}
/* skip to next chunk */
fseek(fd, chunklist[chunkid].len, SEEK_CUR);
}
return(chunkid);
}
/* PUBLIC INTERFACE */
struct midi_chunk_t *midi_readchunk(FILE *fd) {
unsigned char hdr[8];
unsigned long readres;
int i;
unsigned long len = 0;
struct midi_chunk_t *res;
readres = fread(hdr, 1, 8, fd);
if (readres != 8) return(NULL);
/* compute the length */
for (i = 4; i < 8; i++) {
len <<= 8;
len |= hdr[i];
}
res = malloc(sizeof(struct midi_chunk_t) + (unsigned int)len);
if (res == NULL) return(NULL);
/* copy the id string */
res->id[0] = hdr[0];
res->id[1] = hdr[1];
res->id[2] = hdr[2];
res->id[3] = hdr[3];
res->id[4] = 0;
/* assign data length */
res->datalen = len;
/* copy actual data */
readres = fread(res->data, 1, (unsigned int)res->datalen, fd);
if (readres != res->datalen) {
free(res);
return(NULL);
}
return(res);
}
int midi_readhdr(FILE *fd, int *format, int *tracks, unsigned int *timeunitdiv, struct midi_chunkmap_t *chunklist, int maxchunks) {
struct midi_chunk_t *chunk;
unsigned char rmidbuff[12];
/* test for RMID format and rewind if not found */
/* a RMID file starts with RIFFxxxxRMID (xxxx being the data size) */
/* read first 12 bytes - if unable, return an error */
if (fread(rmidbuff, 1, 12, fd) != 12) return(-5);
/* if no RMID header, then rewind the file assuming it's normal MIDI */
if ((rmidbuff[0] != 'R') || (rmidbuff[1] != 'I') || (rmidbuff[2] != 'F') || (rmidbuff[3] != 'F')
|| (rmidbuff[8] != 'R') || (rmidbuff[9] != 'M') || (rmidbuff[10] != 'I') || (rmidbuff[11] != 'D')) {
rewind(fd);
}
/* Read the first chunk of data (should be the MIDI header) */
chunk = midi_readchunk(fd);
/* check id */
if (strcmp(chunk->id, "MThd") != 0) {
free(chunk);
return(-4);
}
/* check len - must be at least 6 bytes */
if (chunk->datalen < 6) {
free(chunk);
return(-3);
}
*format = (chunk->data[0] << 8) | chunk->data[1];
*tracks = (chunk->data[2] << 8) | chunk->data[3];
*timeunitdiv = chunk->data[4];
*timeunitdiv <<= 8;
*timeunitdiv |= chunk->data[5];
/* default tempo -> quarter note (1 beat) == 500'000 microseconds (0.5s), ie 120 bpm.
a delta time unit is therefore (0.5s / DIV) long. */
/* check the timeunit */
if (chunk->data[4] & 128) {
free(chunk);
return(-2);
}
/* check format - must be 0, 1 or 2 */
if ((*format < 0) || (*format > 2)) {
free(chunk);
return(-1);
}
/* read the chunk map */
midi_getchunkmap(fd, chunklist, maxchunks);
return(0);
}
/* parse a track object and returns the id of the first events in the linked
* list. channelsusage contains 16 flags indicating what channels are used. */
long midi_track2events(FILE *fd, char **title, int titlenodes, int titlemaxlen, char *copyright, int copyrightmaxlen, unsigned short *channelsusage, FILE *logfd) {
unsigned long deltatime;
unsigned char statusbyte = 0, tmp;
struct midi_event_t event;
long result = -1, lastnode = -1, newnode;
unsigned long tracklen = 0;
unsigned long ignoreddeltas = 0;
for (;;) {
/* read the delta time first - variable length */
midi_fetch_variablelen_fromfile(fd, &deltatime);
tracklen += deltatime;
/* check the type of the event */
/* if it's a byte with MSB set, we are dealing with running status (so it's same status as last time */
tmp = fgetc(fd);
if ((tmp & 128) != 0) {
statusbyte = tmp;
} else { /* get back one byte */
fseek(fd, -1, SEEK_CUR);
}
event.data.raw[0] = statusbyte;
event.type = EVENT_NONE;
event.deltatime = deltatime;
event.next = -1;
if (statusbyte == 0xFF) { /* META event */
unsigned long metalen;
unsigned long i;
int subtype, endoftrack = 0;
subtype = fgetc(fd); /* fetch the META subtype fields */
midi_fetch_variablelen_fromfile(fd, &metalen);
/* printf("got META[0x%02X] of %ld bytes\n", subtype, metalen); */
switch (subtype) {
case 1: /* text */
fseek(fd, metalen, SEEK_CUR);
break;
case 2: /* copyright notice */
for (i = 0; i < metalen; i++) {
if (i == copyrightmaxlen) break; /* avoid overflow */
copyright[i] = fgetc(fd);
}
copyright[i] = 0;
for (; i < metalen; i++) fgetc(fd); /* skip the rest, if we had to truncate the string */
break;
case 3: /* track name */
/* find the first empty track */
{
int titid = -1, t;
for (t = 0; t < titlenodes; t++) {
if (title[t][0] == 0) {
titid = t;
break;
}
}
if (titid >= 0) {
for (i = 0; i < metalen; i++) {
if (i == titlemaxlen) break; /* avoid overflow */
title[titid][i] = fgetc(fd);
}
title[titid][i] = 0;
} else {
i = 0;
}
}
for (; i < metalen; i++) fgetc(fd); /* skip the rest, if we had to truncate the string */
break;
case 4: /* instrument name */
case 5: /* lyric */
fseek(fd, metalen, SEEK_CUR);
if (logfd != NULL) fprintf(logfd, "%lu: LYRIC EVENT (ignored)\n", tracklen);
break;
case 0x21: /* MIDI port -- no support for multi-MIDI files, I just ignore it */
fseek(fd, metalen, SEEK_CUR);
if (logfd != NULL) fprintf(logfd, "%lu: MIDI PORT EVENT (ignored)\n", tracklen);
break;
case 0x2F: /* end of track */
if (logfd != NULL) fprintf(logfd, "%lu: END OF TRACK\n", tracklen);
endoftrack = 1;
break;
case 0x51: /* set tempo */
if (metalen != 3) {
if (logfd != NULL) fprintf(logfd, "%lu: TEMPO ERROR\n", tracklen);
} else {
event.type = EVENT_TEMPO;
event.data.tempoval = fgetc(fd);
event.data.tempoval <<= 8;
event.data.tempoval |= fgetc(fd);
event.data.tempoval <<= 8;
event.data.tempoval |= fgetc(fd);
if (logfd != NULL) fprintf(logfd, "%lu: TEMPO -> %lu\n", tracklen, event.data.tempoval);
}
break;
case 0x54: /* SMPTE offset -> since I expect only format 0/1 files, I ignore this because I want to start playing asap anyway */
fseek(fd, metalen, SEEK_CUR);
if (logfd != NULL) fprintf(logfd, "%lu: SMPTE OFFSET (ignored)\n", tracklen);
break;
case 0x58: /* Time signature */
if (metalen != 4) {
if (logfd != NULL) fprintf(logfd, "%lu: INVALID TIME SIGNATURE!\n", tracklen);
} else {
fseek(fd, metalen, SEEK_CUR);
if (logfd != NULL) fprintf(logfd, "%lu: TIME SIGNATURE (ignored)\n", tracklen);
}
break;
case 0x59: /* key signature */
if (metalen != 2) {
if (logfd != NULL) fprintf(logfd, "%lu: INVALID KEY SIGNATURE!\n", tracklen);
} else {
/* event.type = 128 | 3; */ /* 128 to mark it as a 'raw event', then its length */
/* event.data.raw[1] = fgetc(fd);
event.data.raw[2] = fgetc(fd);*/
fseek(fd, metalen, SEEK_CUR);
if (logfd != NULL) fprintf(logfd, "%lu: KEY SIGNATURE (ignored)\n", tracklen);
}
break;
case 0x7F: /* proprietary event -> this is non-standard stuff, I ignore it */
fseek(fd, metalen, SEEK_CUR);
if (logfd != NULL) fprintf(logfd, "%lu: PROPRIETARY EVENT (ignored)\n", tracklen);
break;
default:
fseek(fd, metalen, SEEK_CUR); /* skip the meta data */
if (logfd != NULL) fprintf(logfd, "%lu: UNHANDLED META EVENT [0x%02X] (ignored)\n", tracklen, subtype);
break;
}
if (endoftrack) break;
} else if ((statusbyte >= 0xF0) && (statusbyte <= 0xF7)) { /* SYSEX event */
unsigned long sysexlen;
midi_fetch_variablelen_fromfile(fd, &sysexlen); /* get length */
if (logfd != NULL) fprintf(logfd, "%lu: SYSEX EVENT OF %ld BYTES (ignored)\n", tracklen, sysexlen);
fseek(fd, sysexlen, SEEK_CUR);
} else if ((statusbyte >= 0x80) && (statusbyte <= 0xEF)) { /* else it's a note-related command */
event.data.note.chan = statusbyte & 0x0F;
switch (statusbyte & 0xF0) { /* I care only about NoteOn/NoteOff events */
case 0x80: /* Note OFF */
event.type = EVENT_NOTEOFF;
event.data.note.note = fgetc(fd) & 127; /* a note must be in range 0..127 */
event.data.note.velocity = fgetc(fd);
break;
case 0x90: /* Note ON */
*channelsusage |= (1 << event.data.note.chan); /* update the channel usage flags */
event.type = EVENT_NOTEON;
event.data.note.note = fgetc(fd) & 127;
event.data.note.velocity = fgetc(fd);
if (event.data.note.velocity == 0) event.type = EVENT_NOTEOFF; /* if no velocity, it's in fact a note OFF */
break;
case 0xA0: /* key after-touch */
/* puts("KEY AFTER-TOUCH"); */
event.type = 128 | 3; /* 128 to mark it as a 'raw event', then its length */
event.data.raw[1] = fgetc(fd);
event.data.raw[2] = fgetc(fd);
break;
case 0xB0: /* control change */
event.type = 128 | 3; /* 128 to mark it as a 'raw event', then its length */
/* puts("CONTROL CHANGE"); */
event.data.raw[1] = fgetc(fd);
event.data.raw[2] = fgetc(fd);
break;
case 0xC0: /* program (patch) change */
event.type = EVENT_PROGCHAN;
event.data.prog.chan = statusbyte & 0x0F;
event.data.prog.prog = fgetc(fd);
break;
case 0xD0: /* channel after-touch */
event.type = 128 | 2; /* 128 to mark it as a 'raw event', then its length */
event.data.raw[1] = fgetc(fd);
break;
case 0xE0: /* pitch wheel change */
event.type = 128 | 3; /* 128 to mark it as a 'raw event', then its length */
event.data.raw[1] = fgetc(fd);
event.data.raw[2] = fgetc(fd);
break;
default:
puts("unknown note data");
break;
}
} else { /* else it's an error - free memory we allocated and return NULL */
printf("Err. at offset %04lX (bytebuff = 0x%02X)\n", ftell(fd), statusbyte);
return(-1);
}
/* add the event to the queue */
if (event.type == EVENT_NONE) {
ignoreddeltas += event.deltatime;
} else {
int pullpushres;
struct midi_event_t far *eventptr;
event.deltatime += ignoreddeltas; /* add any previous ignored delta times */
ignoreddeltas = 0;
eventptr = &event;
/* add the event to the queue */
newnode = newevent();
if (newnode < 0) {
puts("OUT OF MEMORY");
break;
}
pullpushres = pushevent(eventptr, newnode);
if (pullpushres != 0) {
printf("PUSHEVENT ERROR %02X\n", pullpushres);
break;
}
if (result < 0) {
result = newnode;
lastnode = newnode;
} else {
if (pullevent(lastnode, eventptr) != 0) {
puts("PULLEVENT ERROR");
break;
}
event.next = newnode;
pushevent(&event, lastnode);
lastnode = newnode;
}
}
}
return(result);
}
/* merge two MIDI tracks into a single (serialized) one. returns a "pointer"
* to the unique track. I take care here to not allocate/free memory here.
* All notes are already in RAM after all. totlen is filled with the total
* time of the merged tracks (in miliseconds). */
long midi_mergetrack(long t0, long t1, unsigned long *totlen, unsigned int timeunitdiv) {
long res = -1, lasteventid = -1, selectedid;
int selected;
unsigned long curtempo = 500000l, utotlen = 0;
struct midi_event_t event[2], tmpevent;
if (totlen != NULL) *totlen = 0;
/* fetch first events for both tracks */
if (t0 >= 0) pullevent(t0, &event[0]);
if (t1 >= 0) pullevent(t1, &event[1]);
/* start looping */
while ((t0 >= 0) || (t1 >= 0)) {
/* compare both tracks, and select the soonest one */
if (t0 >= 0) {
if ((t1 >= 0) && (event[1].deltatime < event[0].deltatime)) {
selected = 1;
selectedid = t1;
} else {
selected = 0;
selectedid = t0;
}
} else {
selected = 1;
selectedid = t1;
}
/* if no result yet, assign it */
if (res < 0) {
res = selectedid;
lasteventid = selectedid;
} else { /* otherwise attach the selected track to the last note, and update last pointer */
pullevent(lasteventid, &tmpevent);
tmpevent.next = selectedid;
pushevent(&tmpevent, lasteventid);
lasteventid = selectedid;
}
/* increment timer */
if (totlen != NULL) {
utotlen += event[selected].deltatime * curtempo / timeunitdiv;
while (utotlen >= 1000000lu) {
utotlen -= 1000000lu;
*totlen += 1;
}
if (event[selected].type == EVENT_TEMPO) curtempo = event[selected].data.tempoval;
}
/* decrement timer on the non-selected track, and synch it to xms */
/* move along on the selected track, and fetch it from xms */
if (selected == 0) {
if (t1 >= 0) {
event[1].deltatime -= event[0].deltatime;
pushevent(&event[1], t1);
}
t0 = event[0].next;
if (t0 >= 0) pullevent(t0, &event[0]);
} else { /* selected == 1 */
if (t0 >= 0) {
event[0].deltatime -= event[1].deltatime;
pushevent(&event[0], t0);
}
t1 = event[1].next;
if (t1 >= 0) pullevent(t1, &event[1]);
}
}
return(res);
}