kobodl/sound/a_midifile.c
Ville Lindholm dbc223eb84
Initial commit
Import existing source tree; original VCS history is no longer available.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-28 16:35:31 +03:00

807 lines
17 KiB
C
Raw Permalink Blame History

/*(LGPL)
------------------------------------------------------------
a_midifile.c - MIDI file loader and player
------------------------------------------------------------
* Stolen from the AdPlug player, stripped down,
* translated into C, and adapted for the Audio Engine
* by David Olofson, 2002.
*
* Below is the original copyright. Of course, the LGPL
* license still applies.
------------------------------------------------------------
* Adplug - Replayer for many OPL2/OPL3 audio file formats.
* Copyright (C) 1999, 2000, 2001 Simon Peter, <dn.tlp@gmx.net>, et al.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*
* MIDI & MIDI-like file player - Last Update: 8/16/2000
* by Phil Hassey - www.imitationpickles.org
* philhassey@hotmail.com
*
* Can play the following
* .LAA - a raw save of a Lucas Arts Adlib music
* or
* a raw save of a LucasFilm Adlib music
* .MID - a "midi" save of a Lucas Arts Adlib music
* - or general MIDI files
* .CMF - Creative Music Format
* .SCI - the sierra "midi" format.
* Files must be in the form
* xxxNAME.sci
* So that the loader can load the right patch file:
* xxxPATCH.003 (patch.003 must be saved from the
* sierra resource from each game.)
*
* 6/2/2000: v1.0 relased by phil hassey
* Status: LAA is almost perfect
* - some volumes are a bit off (intrument too quiet)
* MID is fine (who wants to listen to MIDI vid adlib anyway)
* CMF is okay (still needs the adlib rythm mode implemented
* for real)
* 6/6/2000:
* Status: SCI: there are two SCI formats, orginal and advanced.
* original: (Found in SCI/EGA Sierra Adventures)
* played almost perfectly, I believe
* there is one mistake in the instrument
* loader that causes some sounds to
* not be quite right. Most sounds are fine.
* advanced: (Found in SCI/VGA Sierra Adventures)
* These are multi-track files. (Thus the
* player had to be modified to work with
* them.) This works fine.
* There are also multiple tunes in each file.
* I think some of them are supposed to be
* played at the same time, but I'm not sure
* when.
* 8/16/200:
* Status: LAA: now EGA and VGA lucas games work pretty well
*
* Other acknowledgements:
* Allegro - for the midi instruments and the midi volume table
* SCUMM Revisited - for getting the .LAA / .MIDs out of those
* LucasArts files.
* FreeSCI - for some information on the sci music files
* SD - the SCI Decoder (to get all .sci out of the Sierra files)
*/
/*
TODO:
* Number of subsongs and other stuff should be checked
in mf_open(), rather than in mp_rewind().
*/
#undef TESTING
#undef TESTING2
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <string.h>
#include "kobolog.h"
#include "a_midifile.h"
#ifdef TESTING
#define midiprintf log_printf
#else
static inline void midiprintf(int level, const char *format, ...)
{
}
#endif
#ifdef TESTING2
#define midiprintf2 log_printf
#else
static inline void midiprintf2(int level, const char *format, ...)
{
}
#endif
static inline unsigned char datalook(midi_player_t *mp, unsigned pos)
{
if(pos < 0 || pos > mp->mf->flen)
return 0;
return mp->mf->data[pos];
}
static inline unsigned getnext(midi_player_t *mp, unsigned num)
{
unsigned v = 0;
unsigned i;
for(i = 0; i < num; i++)
{
v <<= 8;
v += datalook(mp, mp->pos);
mp->pos++;
}
return v;
}
static inline unsigned getval(midi_player_t *mp)
{
unsigned v = 0;
unsigned char b;
b = (unsigned char)getnext(mp, 1);
v = b & 0x7f;
while((b & 0x80) != 0)
{
b = (unsigned char)getnext(mp, 1);
v = (v << 7) + (b & 0x7F);
}
return v;
}
static inline void set_tempo(midi_player_t *mp, unsigned ppqn, unsigned usqtr)
{
mp->c.ppqn = ppqn;
mp->c.usqtr = usqtr;
mp->c.spulse = (float)usqtr / (float)ppqn / 1000000.0;
midiprintf(D2LOG, "ppqn = %u ", ppqn);
midiprintf(D2LOG, "<EFBFBD>sqtr = %u ", usqtr);
midiprintf(D2LOG, "spulse = %f ", mp->c.spulse);
}
static int matchnext(midi_player_t *mp, const char *s, unsigned len)
{
while(len--)
if(getnext(mp, 1) != (unsigned)(((int)*s++) & 0xff))
return 0;
return 1;
}
static int readheader(midi_player_t *mp, const char *s)
{
if(!matchnext(mp, s, 4))
return -1;
return (int)getnext(mp, 4);
}
static int printnext(midi_player_t *mp, unsigned len)
{
#ifdef TESTING
midiprintf(D2LOG, "\"");
while(len--)
{
unsigned int ch = getnext(mp, 1);
if(ch < 32)
midiprintf(D2LOG, ".");
else
midiprintf(D2LOG, "%c", (char)ch);
}
midiprintf(D2LOG, "\"");
#else
while(len--)
getnext(mp, 1);
#endif
return 1;
}
/* FIXME: This won't work if there are signature changes... */
static void mp_print_time(midi_player_t *mp, unsigned pulses)
{
#ifdef TESTING
unsigned beatval, beat, tick, bar;
if(mp->c.timesig.value)
beatval = mp->c.ppqn * 4 / mp->c.timesig.value;
else
beatval = 120;
beat = pulses / beatval;
tick = pulses % beatval;
if(mp->c.timesig.beats)
{
bar = beat / mp->c.timesig.beats;
beat = beat % mp->c.timesig.beats;
}
else
{
bar = beat / 4;
beat = beat % 4;
}
midiprintf(D2LOG, "(%u:%u:%u)", bar+1, beat+1, tick);
#endif
}
static long filelength(FILE *f)
{
long buf, size;
buf = ftell(f);
if(buf < 0)
return buf;
size = fseek(f, 0, SEEK_END);
if(size < 0)
return size;
size = ftell(f);
if(size < 0)
return size;
fseek(f, buf, SEEK_SET);
return size;
}
midi_file_t *mf_open(const char *name)
{
FILE *f;
unsigned char s[6];
midi_file_t *mf = calloc(1, sizeof(midi_file_t));
if(!mf)
return NULL;
mf->title = mf->author = "<Unknown>";
mf->remarks = "<None>";
f = fopen(name, "rb");
if(!f)
{
free(mf);
return NULL;
}
if(fread(s, 6, 1, f) != 1)
{
fclose(f);
free(mf);
return NULL;
}
if(!memcmp(s, "MThd", 4))
mf->subsongs = 1;
else
{
mf->subsongs = 0;
fclose(f);
free(mf);
return NULL;
}
fseek(f, 0, SEEK_SET);
mf->flen = (unsigned)filelength(f);
mf->data = malloc(mf->flen);
if(!mf->data)
{
fclose(f);
free(mf);
return NULL;
}
if(fread(mf->data, mf->flen, 1, f) != 1)
{
fclose(f);
free(mf->data);
free(mf);
return NULL;
}
fclose(f);
return mf;
}
void mf_close(midi_file_t *mf)
{
if(!mf)
return;
free(mf->data);
free(mf);
}
int mp_select(midi_player_t *mp, midi_file_t *midifile)
{
mp_stop(mp);
mp->mf = midifile;
mp_rewind(mp, 0);
return 0;
}
static void mp_mark_loop(midi_player_t *mp)
{
int i;
for(i = 0; i < 16; i++)
if(mp->track[i].enabled)
mp->track[i].loop_start = mp->track[i].c;
mp->loop_start = mp->c;
mp_print_time(mp, mp->c.time);
midiprintf(D2LOG, ": mp_mark_loop\n");
}
static void mp_loop(midi_player_t *mp)
{
int i;
for(i = 0; i < 16; i++)
if(mp->track[i].enabled)
mp->track[i].c = mp->track[i].loop_start;
mp_print_time(mp, mp->c.time);
midiprintf(D2LOG, ": mp_loop\n");
mp->c = mp->loop_start;
}
static int mp_parse_command(midi_player_t *mp, unsigned len)
{
char buf[128];
unsigned i;
for(i = 0; i < len; i++)
{
if(i < 127)
buf[i] = (char)datalook(mp, mp->pos);
mp->pos++;
}
buf[len < 127 ? len : 127] = 0;
if('#' != buf[0])
{
midiprintf(D2LOG, "Marker: %s", buf);
return 0;
}
if(!memcmp(buf + 1, "LOOP", 4))
{
if(!memcmp(buf + 5, "_START", 6))
mp_mark_loop(mp);
else if(!memcmp(buf + 5, "_END", 4))
{
mp_loop(mp);
return 1;
}
}
return 0;
}
static void mp_read_time_sig(midi_player_t *mp)
{
mp_timesig_t *ts = &mp->c.timesig;
ts->beats = getnext(mp, 1);
ts->value = 1 << getnext(mp, 1);
ts->clock = getnext(mp, 1);
ts->qn = getnext(mp, 1);
midiprintf(D2LOG, "Time Sig: %d/%d;", ts->beats, ts->value);
midiprintf(D2LOG, " %d clock %d qn", ts->clock, ts->qn);
}
#define FOR_TRACKS_ON for(trk = 0; trk < 16; trk++) \
if(mp->track[trk].c.on)
int mp_update(midi_player_t *mp)
{
unsigned note, vel, ctrl, x, l;
unsigned i, j, c;
int trk, ret;
if(mp->doing == 1)
{
/* just get the first wait and ignore it :> */
midiprintf(D2LOG, "Initial wait: ");
FOR_TRACKS_ON
{
mp_track_t *mt = mp->track + trk;
mp->pos = mt->c.pos;
mt->c.iwait += getval(mp);
midiprintf(D2LOG, "%d:%u ", trk, mt->c.iwait);
mt->c.pos = mp->pos;
}
mp->doing = 0;
midiprintf(D2LOG, "\n\n");
}
do
{
ret = 0;
FOR_TRACKS_ON
{
mp_track_t *mt = mp->track + trk;
if(mt->c.iwait)
continue;
if(mt->c.pos >= mt->tend)
{
midiprintf(D2LOG, "(%d) End Of Track\n", trk);
mt->c.on = 0;
continue;
}
ret = 1;
midiprintf2(D2LOG, "<%u>: ", mt->c.pos);
mp->pos = mt->c.pos;
x = getnext(mp, 1);
/* This is for MIDI "running status" */
if(x < 0x80)
{
x = mt->c.pv;
mp->pos--;
}
mt->c.pv = (unsigned char)x;
c = x & 0x0f;
midiprintf2(D2LOG, "(%d)[%2X]", trk, x);
switch (x & 0xf0)
{
case 0x80: /* note off */
note = getnext(mp, 1) + (mp->pitch >> 16);
if(note > 127)
note = 127;
else if(note < 0)
note = 0;
vel = getnext(mp, 1);
if(mp->sock->note_off)
mp->sock->note_off(c, note, vel);
break;
case 0x90: /* note on */
/* doing=0; */
note = getnext(mp, 1) + (mp->pitch >> 16);
if(note > 127)
note = 127;
else if(note < 0)
note = 0;
vel = getnext(mp, 1);
if(mp->sock->note_on)
mp->sock->note_on(c, note, vel);
break;
case 0xa0: /* key aftertouch */
note = getnext(mp, 1) + (mp->pitch >> 16);
if(note > 127)
note = 127;
else if(note < 0)
note = 0;
vel = getnext(mp, 1);
if(mp->sock->poly_pressure)
mp->sock->poly_pressure(c, note,
vel);
break;
case 0xb0: /* control change */
ctrl = getnext(mp, 1);
vel = getnext(mp, 1);
if(mp->sock->control_change)
mp->sock->control_change(c, ctrl,
vel);
break;
case 0xc0: /* patch change */
x = getnext(mp, 1);
if(mp->sock->program_change)
mp->sock->program_change(c, x);
break;
case 0xd0: /* channel aftertouch */
x = getnext(mp, 1);
if(mp->sock->channel_pressure)
mp->sock->channel_pressure(c, x);
break;
case 0xe0: /* pitch wheel */
x = getnext(mp, 1);
x |= getnext(mp, 1) << 7;
if(mp->sock->pitch_bend)
mp->sock->pitch_bend(c, (int)x - 8192);
break;
case 0xf0:
switch (x)
{
case 0xf0:
case 0xf7: /* sysex */
l = getval(mp);
i = (datalook(mp, mp->pos + l) ==
0xf7);
midiprintf2(D2LOG, "{%ld}", l);
midiprintf2(D2LOG, "\n");
for(j = 0; j < l; j++)
midiprintf2(D2LOG, "%2lX ", getnext(mp, 1));
midiprintf2(D2LOG, "\n");
if(i)
getnext(mp, 1);
break;
case 0xf1:
break;
case 0xf2:
getnext(mp, 2);
break;
case 0xf3:
getnext(mp, 1);
break;
case 0xf4:
break;
case 0xf5:
break;
case 0xf6: /* something */
case 0xf8:
case 0xfa:
case 0xfb:
case 0xfc:
break;
case 0xfe:
break;
case 0xfd:
break;
case 0xff:
x = getnext(mp, 1);
l = getval(mp);
midiprintf(D2LOG, " {%X (%X): ", x, l);
switch(x)
{
case 1:
midiprintf(D2LOG, "Text: ");
printnext(mp, l);
break;
case 2:
midiprintf(D2LOG, "Copyright: ");
printnext(mp, l);
break;
case 3:
midiprintf(D2LOG, "Track Name: ");
printnext(mp, l);
break;
case 4:
midiprintf(D2LOG, "Instrument: ");
printnext(mp, l);
break;
case 5:
midiprintf(D2LOG, "Lyric: ");
printnext(mp, l);
break;
case 6:
if(mp_parse_command(mp, l))
{
trk = -1;
continue;
}
break;
case 7:
midiprintf(D2LOG, "Cue Point: ");
printnext(mp, l);
break;
case 8:
case 9:
case 10:
case 11:
case 12:
case 13:
case 14:
case 15:
midiprintf(D2LOG, "Text event: ");
printnext(mp, l);
break;
case 0x21: /* MIDI Port # */
x = getnext(mp, 1);
midiprintf(D2LOG, "Port %u", x);
break;
case 0x2f: /* End Of Track */
mt->c.on = 0;
midiprintf(D2LOG, "End Of Track");
break;
case 0x51: /* Set Tempo */
x = getnext(mp, l);
set_tempo(mp, mp->c.ppqn, x);
midiprintf(D2LOG, "(<28>sqtr:%u ==> BPM:%f)",
mp->c.usqtr,
60.0*1000000.0 /
(float)mp->c.usqtr);
break;
case 0x58: /* Time signature */
mp_read_time_sig(mp);
break;
default:
for(i = 0; i < l; i++)
midiprintf2(D2LOG, "%2lX ", getnext(mp, 1));
break;
}
midiprintf(D2LOG, "}\n");
break;
}
break;
default:
/* if we get down here, an error occurred */
log_printf(ELOG, "a_midifile.c: INTERNAL ERROR;"
" illegal status byte!\n");
break;
}
if(mt->c.on)
mt->c.iwait = getval(mp);
mt->c.pos = mp->pos;
midiprintf2(D2LOG, "\n");
}
} while(ret);
/* Find first track to play */
mp->c.iwait = 1000000000; /* Bigger than any wait can be! */
FOR_TRACKS_ON
if(mp->track[trk].c.iwait < mp->c.iwait)
mp->c.iwait = mp->track[trk].c.iwait;
if(1000000000 == mp->c.iwait)
{
/* No tracks playing! */
mp->c.iwait = 0;
mp->c.fwait = 1000;
return 0;
}
/* Calculate time to next event */
mp->c.fwait = mp->c.iwait * mp->c.spulse;
midiprintf2(D2LOG, "fwait = %f\n", mp->c.fwait);
/* Update "track timers" and check if any tracks are playing */
ret = 0;
midiprintf2(D2LOG, "Wait: ");
FOR_TRACKS_ON
{
ret = 1;
mp->track[trk].c.iwait -= mp->c.iwait;
midiprintf2(D2LOG, "%d:%ld ", trk, mp->track[trk].c.iwait);
}
midiprintf2(D2LOG, "\n");
/* Update song time */
mp->c.time += mp->c.iwait;
midiprintf2(D2LOG, "----------------------------\n");
return ret;
}
#undef FOR_TRACKS_ON
int mp_play(midi_player_t *mp, float dt)
{
if(!mp->mf)
return 0;
while(dt > 0.0)
{
if(dt < mp->c.fwait)
{
mp->c.fwait -= dt;
return 1;
}
dt -= mp->c.fwait;
if(!mp_update(mp))
return 0;
}
return 1;
}
void mp_stop(midi_player_t *mp)
{
unsigned c;
if(!mp)
return;
if(!mp->sock->control_change)
return;
for(c = 0; c < 16; ++c)
{
mp->sock->control_change(c, 120, 1);
mp->sock->control_change(c, 121, 1);
mp->sock->control_change(c, 123, 1);
}
mp->mf = NULL;
}
void mp_rewind(midi_player_t *mp, unsigned subsong)
{
unsigned i;
int len;
if(!mp->mf)
return;
mp->pos = 0;
set_tempo(mp, 250, (unsigned)(1000.0 / (120.0/60.0)));
mp->c.time = 0;
mp->c.fwait = 0;
mp->c.iwait = 0;
mp->mf->subsongs = 1;
for(i = 0; i < 16; i++)
{
mp->track[i].tend = 0;
mp->track[i].spos = 0;
mp->track[i].c.pos = 0;
mp->track[i].c.iwait = 0;
mp->track[i].c.on = 0;
mp->track[i].c.pv = 0;
}
len = readheader(mp, "MThd");
if(len < 0)
{
log_printf(ELOG, "mp_rewind(): Is this a MIDI file...?\n");
mp->mf = NULL;
return;
}
midiprintf(D2LOG, "header length:%d\n", len);
if(len >= 6)
{
mp->mf->format = getnext(mp, 2);
mp->mf->tracks = getnext(mp, 2);
set_tempo(mp, getnext(mp, 2), mp->c.usqtr);
midiprintf(D2LOG, "format:%u\n", mp->mf->format);
midiprintf(D2LOG, "tracks:%u\n", mp->mf->tracks);
midiprintf(D2LOG, " ppqn:%u\n", mp->c.ppqn);
if(0 == mp->mf->format)
mp->mf->tracks = 1;
}
else
{
log_printf(ELOG, "mp_rewind(): WARNING: Short header!\n");
mp->mf->format = 0;
mp->mf->tracks = 1;
}
if(len > 6)
getnext(mp, (unsigned)(len - 6)); /* Skip rest of header */
if(mp->mf->tracks > 16)
{
log_printf(ELOG, "mp_rewind(): WARNING: Too many tracks!\n");
mp->mf->tracks = 16;
}
for(i = 0; i < mp->mf->tracks; ++i)
{
len = readheader(mp, "MTrk");
if(len < 0)
{
log_printf(ELOG, "mp_rewind(): Bad MIDI file!\n");
mp->mf = NULL;
return;
}
mp->track[i].enabled = 1;
mp->track[i].c.on = 1;
mp->track[i].spos = mp->pos;
mp->track[i].tend = mp->pos + len;
midiprintf(D2LOG, "track %u; start:%u length:%d\n",
i, mp->track[i].spos, len);
getnext(mp, (unsigned)len);
}
for(i = 0; i < 16; i++)
if(mp->track[i].enabled)
{
mp->track[i].c.pos = mp->track[i].spos;
mp->track[i].c.pv = 0;
mp->track[i].c.iwait = 0;
mp->track[i].loop_start = mp->track[i].c;
}
mp->doing = 1;
}
midi_player_t *mp_open(midisock_t *ms)
{
midi_player_t *mp = calloc(1, sizeof(midi_player_t));
if(!mp)
return NULL;
mp->sock = ms;
mp->mf = NULL;
return mp;
}
void mp_close(midi_player_t *mp)
{
mp_stop(mp);
free(mp);
}