/* | |
* MUS loader for DOSMid | |
* | |
* Copyright (c) 2014, 2015 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 <stdio.h> | |
#include <string.h> /* memcpy() */ | |
#include "mem.h" | |
#include "midi.h" | |
#include "mus.h" /* include self for control */ | |
#define TICKLEN 500000l /* length of a single MUS tick, in us */ | |
#define TIMEUNITDIV 70 /* the MIDI time unit divisor */ | |
/* loads a MUS file into memory, returns the id of the first event on success, | |
* or -1 on error. channelsusage contains 16 flags indicating what channels | |
* are used. */ | |
long mus_load(FILE *fd, unsigned long *totlen, unsigned short *timeunitdiv, unsigned short *channelsusage) { | |
unsigned char remapchannel[16] = {0,1,2,3,4,5,6,7,8,15,10,11,12,13,14,9}; | |
unsigned char hdr_or_chanvol[16]; | |
unsigned short scorestart; | |
int bytebuff, bytebuff2, loadflag = 0; | |
unsigned long event_dtime, nextwait = 0; | |
unsigned short event_type; | |
unsigned short event_channel; | |
long res = -1; | |
int tickduration = 0; | |
long mslen = 0; | |
struct midi_event_t midievent; | |
rewind(fd); | |
/* read the 16 bytes header first, and populate hdr data */ | |
if (fread(hdr_or_chanvol, 1, 16, fd) != 16) return(-8); | |
if ((hdr_or_chanvol[0] != 'M') || (hdr_or_chanvol[1] != 'U') || (hdr_or_chanvol[2] != 'S') || (hdr_or_chanvol[3] != 0x1A)) { | |
return(-9); | |
} | |
scorestart = hdr_or_chanvol[6] | (hdr_or_chanvol[7] << 8); | |
/* position the next reading position to first event */ | |
fseek(fd, scorestart, SEEK_SET); | |
/* set tempo to 140 bpm (428571 us per quarter note) */ | |
memset(&midievent, 0, sizeof(struct midi_event_t)); | |
midievent.type = EVENT_TEMPO; | |
*timeunitdiv = TIMEUNITDIV; | |
midievent.data.tempoval = TICKLEN; | |
if (pusheventqueue(&midievent, &res) != 0) return(MUS_OUTOFMEM); | |
/* since now on, hdr_or_chanvol is used to store volume of channels */ | |
memset(hdr_or_chanvol, 0, 16); | |
*channelsusage = 0; /* zero out the used instruments map */ | |
/* read events from the MUS file and translate them into midi events */ | |
for (;;) { | |
bytebuff = fgetc(fd); | |
if (bytebuff < 0) return(-5); /* if EOF, abort with error */ | |
event_channel = bytebuff & 0x0F; | |
bytebuff >>= 4; | |
event_type = bytebuff & 7; | |
bytebuff >>= 3; | |
event_dtime = bytebuff; /* if the 'last' bit is set, remember to read time after the event later */ | |
/* if channel is 15, it is percussion, and must be remapped to MIDI #9 */ | |
event_channel = remapchannel[event_channel]; | |
/* clear out midievent to make room for the incoming MIDI message */ | |
memset(&midievent, 0, sizeof(struct midi_event_t)); | |
/* read complementary data, if any */ | |
switch (event_type) { | |
case 0: /* release note (1 byte follows) */ | |
bytebuff = fgetc(fd); | |
if ((bytebuff & 128) != 0) return(-13); /* MSB should always be zero */ | |
midievent.type = EVENT_NOTEOFF; | |
midievent.data.note.note = bytebuff; | |
midievent.data.note.chan = event_channel; | |
midievent.data.note.velocity = 0; | |
break; | |
case 1: /* play note (1 or 2 bytes follow) */ | |
*channelsusage |= (1 << event_channel); /* update the channel usage flags */ | |
bytebuff = fgetc(fd); | |
midievent.type = EVENT_NOTEON; | |
midievent.data.note.note = bytebuff & 127; | |
midievent.data.note.chan = event_channel; | |
if ((bytebuff & 128) != 0) { | |
midievent.data.note.velocity = fgetc(fd); | |
hdr_or_chanvol[event_channel] = midievent.data.note.velocity; /* remember the last velocity, might be needed later */ | |
} else { | |
midievent.data.note.velocity = hdr_or_chanvol[event_channel]; | |
} | |
break; | |
case 2: /* pitch wheel (1 byte follows) */ | |
/* MIDI says that pitch wheel is a 14bit value with the center being | |
* at 0x2000. MUS on the other hand provides only 8bits, so some | |
* adjustement must be made as to fit into this scheme: | |
* 0 = two half-tones down | |
* 64 = one half-tone down | |
* 128 = normal (default) | |
* 192 = one half-tone up | |
* 255 = two half-tones up */ | |
bytebuff = fgetc(fd); | |
midievent.type = EVENT_PITCH; | |
midievent.data.pitch.wheel = bytebuff << 6; /* convert wheel value to 14 bits as expected by MIDI */ | |
midievent.data.pitch.chan = event_channel; | |
break; | |
case 3: /* sysex (1 byte follows) - I ignore SYSEX messages */ | |
bytebuff = fgetc(fd); | |
if ((bytebuff & 128) != 0) return(-11); /* MSB should always be zero */ | |
break; | |
case 4: /* control (2 bytes follow) */ | |
bytebuff = fgetc(fd); | |
bytebuff2 = fgetc(fd); | |
if ((bytebuff == 0) && (bytebuff2 < 128)) { /* change program */ | |
midievent.type = EVENT_PROGCHAN; | |
midievent.data.prog.prog = bytebuff2; | |
midievent.data.note.chan = event_channel; | |
} else if ((bytebuff >= 1) && (bytebuff <= 9)) { /* else it maps directly to a MIDI controller message */ | |
int tmpmap[10] = {0,0,1,7,10,11,91,93,64,67}; /* map MUS controllers to MIDI controller IDs */ | |
midievent.type = EVENT_CONTROL; | |
midievent.data.control.chan = event_channel; | |
midievent.data.control.id = tmpmap[bytebuff]; | |
midievent.data.control.val = bytebuff2; | |
} else { /* else it's an illegate byte pattern - abort mission, captain! */ | |
return(-91); | |
} | |
break; | |
case 6: /* end of song (no byte follow) */ | |
if (pusheventqueue(NULL, NULL) != 0) return(MUS_OUTOFMEM); | |
loadflag = 1; | |
break; | |
default: /* unknown event type - abort */ | |
return(-3); | |
break; | |
} | |
/* if file loaded fine, break out of the loop now */ | |
if (loadflag != 0) break; | |
/* fill in the delta time */ | |
midievent.deltatime = nextwait; | |
/* if dtime is non-zero, read the number of ticks to wait before next note */ | |
nextwait = 0; | |
while (event_dtime != 0) { | |
bytebuff = fgetc(fd); | |
if (bytebuff < 0) return(-6); | |
event_dtime = bytebuff & 128; | |
nextwait <<= 7; | |
nextwait |= (bytebuff & 127); | |
} | |
/* push the event into memory */ | |
if (pusheventqueue(&midievent, NULL) != 0) return(MUS_OUTOFMEM); | |
/* recompute total song's length */ | |
tickduration += nextwait; | |
while (tickduration >= TIMEUNITDIV) { | |
mslen += TICKLEN / 1000; /* mslen is in miliseconds, while TICKLEN is in us */ | |
tickduration -= TIMEUNITDIV; | |
} | |
} | |
mslen += (tickduration / TIMEUNITDIV * TICKLEN) / 1000; | |
*totlen = mslen / 1000; /* totlen is in seconds */ | |
/* */ | |
return(res); | |
} |