/*
 * File: mp_intp.c
 * SGoldthorpe	20-Jul-91
 */

/*
 * mp_intp - the midi file interpreter for midiplay
 * This software is (C) 1991 Stephen Goldthorpe but it's FREE!  Usual
 * disclaimers and notices about this software not being sold for profit.
 * But you may take all you want from the code though!  If you have any
 * suggestions/bug fixes please get in contact with me.  I don't want to
 * maintain code i've never even seen before (life's hard enough without all
 * of that)!
 *						-Steve Goldthorpe
 * Phone (DAYTIME UK):	+44 707 382350
 * Internet E-Mail:	SGoldthorpe.wgc-e@rx.xerox.com
 *			goldthor@arisia.xerox.com
 *
 * Version 0.5 by Piet van Oostrum <piet@cs.ruu.nl>
 * November 1991.
 * I made the following changes:
 * 1. Files > 32767 wouldn't play. I have changed a couple of ints to
 *    LONG. Also malloc'ed the buffer with a variable rather than a fixed size.
 * 2. replaced array referenced with a[i] rather than *(a+i). I find this
 *    more readable but it has the same meaning.
 * 3. Midifiles of more than about 3 minutes didn't play right. I think
 *    this is a bug in ltod (long to double) in the floating point lib. I
 *    changed the timing from using floating point arithmetic to LONG
 *    arithmetic.
 *    Anyway the timing was not robust in the presence of tempo changes
 *    because the new time per beat would be applied to the time from the
 *    beginning of the piece rather than from the time of the tempo change.
 * 4. I introduced the -T option to send timing commands. This can be
 *    used to trigger a drum computer or an arranger.
 *
 * NOTE: I distribute this version with the consent of Steve
 * Goldthorpe. I think he should not be bothered with bugs in my version!
 */

#include	<stdio.h>
#include	<types.h>
#include	<time.h>
#include	<string.h>

/* and for the atari OS stuff */
#include	<osbind.h>

/* #include	"midiplay.h" included by mp_gbls.h */
#include	"mp_gbls.h"

/* GLOBAL VARIABLES */
/* The track_delta's are in units of 1/division clockticks */
/* time_per_beat is the length of a quarter note in clock ticks */
   
static long	track_delta[MAX_TRACKS], division, time_per_beat;
static BYTE	*track_pos[MAX_TRACKS];
static long	track_left[MAX_TRACKS];
static int	track_finished[MAX_TRACKS];
static clock_t	clock_orig;
static char	*gFile;
static WORD	format, tracks;
static int	finished_tracks;
static int	trnr;
static long	clock_delta, clock_time;

/* FUNCTION DECLS */
BOOL		interp();
static void	truncated(), all_notes_off(), parse_error();

/* MACRO FUNCTIONS */
/* the error checking may be a bit OTT but I'm gonna do it anyway  (helps
   catch those naughty bugs - and bad files) */
#define GET32BITS(dw,p,l)	dw=(((LONG)(*p)<<24)+			\
					((LONG)(*(p+1))<<16)+		\
					(((LONG)*(p+2))<<8)+		\
					(LONG)(*(p+3)));		\
				if(l<4)					\
				{	truncated();			\
					return(FALSE);			\
				};					\
				p += 4; l -= 4

#define	GET16BITS(w,p,l)	w=(((WORD)(*p)<<8)+(WORD)*(p+1));	\
				if(l<2)					\
				{	truncated();			\
					return(FALSE);			\
				}					\
				p += 2; l -= 2

#define	GET8BITS(b,p,l)		b = *(p)++;				\
				if(--l<0)				\
				{	truncated();			\
					return(FALSE);			\
				}

#define	GETVARLEN(dw,p,l)	for(dw=(LONG)(*p)&0x7f;(*(p)++)&0x80;)	\
				{	if(--l<0)			\
					{	truncated();		\
						return(FALSE);		\
					};				\
					dw <<=7;			\
					dw |= (LONG)(*p)&0x7f;		\
				};					\
				if(--l<0)				\
				{	truncated();			\
					return(FALSE);			\
				}

#define	CHECKLEFT(l,v)		if(l<v)					\
				{	truncated();			\
					return(FALSE);			\
				}

#define	SEND(b)			Bconout(3,b)

/* FUNCTION DEFS */
BOOL		interp(buffer, file, len)
char		*file;
BYTE		*buffer;
LONG		len;
{   BYTE	*pos=buffer,*next;
    BYTE	running_status[MAX_TRACKS],last_running_status;
    WORD	w;
    LONG	dw,delta;
    LONG	left=len;

    gFile=file;

    /* check header */
    if((left<4)||(strncmp("MThd",(char*)pos,4)!=0))
    {	(void)fprintf(stderr,"%s: %s is not a midi file\n",app_name,file);
	return(FALSE);
    };
    pos += 4; left -= 4;

    /* find address of next chunk */
    GET32BITS(dw,pos,left);
    next=pos+dw;
#ifdef DEBUG
    (void)printf("pos is %lx len is %ld next would be %lx\n",pos-buffer,dw,
	next-buffer);
#endif

    /* get file format */
    GET16BITS(format,pos,left);
    switch (format)
    {	/* OK we accept formats 0 and 1 */
	case 0:
	case 1:
     	    break;
	/* but we don't do any others */
    default:
	(void)fprintf(stderr,"%s: can't play %s, midi file type %d\n",app_name,
	    file,format);
	return(FALSE);
    };

    /* get number of tracks */
    GET16BITS(tracks,pos,left);

    /* check tracks in range */
    if(tracks > MAX_TRACKS)
    {	(void)fprintf(stderr,"%s: %s has too many tracks (%d allowed).\n",
	    app_name,file,tracks,MAX_TRACKS);
	return(FALSE);
    };

    /* get division - this is the division of a quarter note or if negative is
       frame based. */
    GET16BITS(w,pos,left);
    division=(LONG)w;

    /* I don't suport the frame stuff yet! */
    if(division < 0)
    {	(void)fprintf(stderr,
	    "%s: file %s - don't support framed based files yet!", app_name,
	    file);
	return(FALSE);
    };
#ifdef DEBUG
    (void)printf("division = %f\n",division);
#endif

    time_per_beat = (LONG)(CLK_TCK/2); /* default 120bpm = 2bps */
#ifdef DEBUG
    (void)printf("time per beat (1/%d sec) %ld\n", CLK_TCK, time_per_beat);
#endif

    /* do some initialisation, track finding etc */
    finished_tracks = 0;
    for(trnr=0;trnr<tracks;trnr++)
    {	CHECKLEFT(left,(next-pos));
	left -= next-pos; pos = next;

	/* track start  */
	if((left<4)||(strncmp("MTrk",(char*)pos,4)!=0))
	{   (void)fprintf(stderr,"%s: %s parse error, track expected\n",
		app_name,file);
#ifdef DEBUG
	    (void)printf("posn %lx, bytes around (-3..3) %02x %02x %02x %02x \
%02x %02x %02x\n",pos-buffer,*(pos-3),*(pos-2),*(pos-1),*pos,*(pos+1),*(pos+2),
		*(pos+3));
#endif
	    return(FALSE);
	};
	pos += 4; left -= 4;

	/* find address of next chunk */
	GET32BITS(dw,pos,left);
	track_pos[trnr] = pos;
	next = pos+dw;
	track_left[trnr] = dw;
#ifdef DEBUG
	(void)printf("pos is %lx len is %ld next would be %lx\n",pos-buffer,dw,
	    next-buffer);
#endif

	/* get initial delta time */
	GETVARLEN(dw,track_pos[trnr],track_left[trnr]);
	track_delta[trnr]=dw * time_per_beat;
	track_finished[trnr]=FALSE;
	running_status[trnr] = 0xfe;
	last_running_status = 0xfe;
    };

    /* let the user know what's happening */
    (void)printf("playing '%s' with %d %s\n",file,tracks,
    						tracks==1?"track":"tracks");

    /* get start time */
    clock_orig=clock();
    clock_time=0;
    if (f_Timing) SEND (0xFA);
        
    /* dispatcher */
    while (finished_tracks != tracks)
    {	BYTE	event;
	clock_delta = (clock()-clock_orig)*division;
	if (f_Timing && clock_delta >= clock_time) {
	    SEND(0xF8);
	    clock_time += (time_per_beat * division / 24);
	}
	for(trnr=0;trnr<tracks;trnr++)
	{   if (!track_finished[trnr] & (clock_delta >= track_delta[trnr]))
	    {	GET8BITS(event,track_pos[trnr],track_left[trnr]);

		/* parse event */
		switch (event)
		{   /* meta-events */
		    case 0xff:
		    {	if(!meta_event())
			    return(FALSE);
			break;
		    };
		    /* sysex events */
		    case 0xf0:
		    case 0xf7:
		    {	if(!sysex_event(event))
			    return(FALSE);
			break;
		    };
		    case 0xf1:
		    case 0xf2:
		    case 0xf3:
		    case 0xf4:
		    case 0xf5:
		    case 0xf6:
		    {	if(!system_common(event))
			    return(FALSE);
			break;
		    };
		    case 0xf8:
		    case 0xf9:
		    case 0xfa:
		    case 0xfb:
		    case 0xfc:
		    case 0xfd:
		    case 0xfe:
		    {	if(!system_real_time())
			    return(FALSE);
			break;
		    };
		    /* midi events */
		    default:
		    {	switch(event & 0xf0)
			{   /* 3 byte events */
			    case 0x90:
			    case 0x80:
			    case 0xa0:
			    case 0xb0:
			    case 0xe0:
			    {	BYTE	c;
				SEND(event);
				GET8BITS(c,track_pos[trnr],track_left[trnr]);
				SEND(c);
				GET8BITS(c,track_pos[trnr],track_left[trnr]);
				SEND(c);
				running_status[trnr] = event;
				last_running_status = event;
				break;
			    };
			    /* program change */
			    case 0xc0:
			    {	BYTE	c;
				GET8BITS(c,track_pos[trnr],track_left[trnr]);
				if (f_Program)
				{   SEND(event);
				    SEND(c);
				    running_status[trnr] = event;
				    last_running_status = event;
				};
				break;
			    };
			    /* channel pressure */
			    case 0xd0:
			    {	BYTE	c;
				GET8BITS(c,track_pos[trnr],track_left[trnr]);
				if (f_Channel_pressure)
				{   SEND(event);
				    SEND(c);
				    running_status[trnr] = event;
				    last_running_status = event;
				};
				break;
			    };
			    default:
			    {	/* running status */
				if(running_status[trnr]!=last_running_status)
				{   SEND(running_status[trnr]);
				    last_running_status=running_status[trnr];
				};
				if((event&0x80)==0)
				{   SEND(event);
#ifdef DEBUG
				    (void)printf("running stat (%02x)  %02x\n",
					running_status[trnr],event);
#endif
				    switch(running_status[trnr] & 0xf0)
				    {   /* 3 byte events */
					case 0x90:
					case 0x80:
					case 0xa0:
					case 0xb0:
					case 0xe0:
					{   BYTE c;
					    GET8BITS(c,track_pos[trnr],
						track_left[trnr]);
					    SEND(c);
					    break;
					};
					/* ignoring other 0xf? events - naughty
					    aren't I */
				    };
				};
			    };
			};
		    };
		};

		/* get delta time  - but only if we're still going on this
		   track */
		if(!track_finished[trnr])
		{   GETVARLEN(delta,track_pos[trnr],track_left[trnr]);
		    track_delta[trnr] += delta * time_per_beat;
		};
	    };
	};
	/* CHECK TO SEE IF CTRL C/S PRESSED */
	if(Bconstat(2))
	{   BYTE	c;
	    c=(Bconin(2)&0xff);
	    switch(c)
	    {	case 3: /* CTRL C */
		{   printf("\n** Exit by CTRL C **\n");
		    all_notes_off();
		    return(TRUE);
		};
		case 19: /* CTRL S */
		{   printf("\n** Skip track by CTRL S **\n");
		    all_notes_off();
		    return(FALSE);
		};
	    };
	};
    };
    if (f_Timing) SEND (0xFC);

    return(FALSE);
};

/* system exclusive events */
int	sysex_event(event)
BYTE	event;
{   LONG	length,l;
    GETVARLEN(length,track_pos[trnr],track_left[trnr]);
    if(f_Sysex)
    {   BYTE	c;
	if(event==0xf0)
		SEND(0xf0);
	for(l=0;l<length;l++)
	{  GET8BITS(c,track_pos[trnr],track_left[trnr]);
	   SEND(c);
	};
    }
    else
    {   CHECKLEFT(track_left[trnr],length);
        track_pos[trnr] += length;
        track_left[trnr] -= length;
    };
    return(TRUE);
};

/* meta-events */
int meta_event()
{   BYTE	type;
    LONG	length;

    /* get type of meta event */
    GET8BITS(type,track_pos[trnr],track_left[trnr]);
#ifdef DEBUG
    (void)printf("meta-event %02x ",type);
#endif

    /* get length */
    GETVARLEN(length,track_pos[trnr],track_left[trnr]);
#ifdef DEBUG
    (void)printf("length %ld\n",length);
#endif

    switch(type)
    {	/* textual meta-events */
	case 0x01:
	case 0x02:
	case 0x03:
	case 0x04:
	case 0x05:
	case 0x06:
	case 0x07:
	{   int		flag;
	    char	*msg;
	    switch(type)
	    {	case 0x01:
		    flag=f_text;
		    msg="Text: \"";
		    break;
	 	case 0x02:
		    flag=f_copyright;
		    msg="Copyright: \"";
		    break;
		case 0x03:
		    flag=f_track_name;
		    msg="Track\Sequence: \"";
		    break;
		case 0x04:
		    flag=f_instrument;
		    msg="Instrument: \"";
		    break;
		case 0x05:
		    flag=f_lyric;
		    break;
		case 0x06:
		    flag=f_marker;
		    msg="Marker: \"";
		    break;
		case 0x07:
		    flag=f_prompt;
		    msg="Prompt: \"";
		    break;
	    };
	    if (flag)
	    {	LONG	l;
		BYTE	c;
		/* lyrics should have no header */
	        if(type!=0x05)
		    printf(msg);
		for(l=0;l<length;l++)
		{   GET8BITS(c,track_pos[trnr],track_left[trnr]);
		    putchar(c);
		};
		/* lyrics should be on same line */
		if(type!=0x05)
		    printf("\"[Tr %d]\n",trnr);
	    }
	    else
	    {	CHECKLEFT(track_left[trnr],length);
		track_pos[trnr] += length;
		track_left[trnr] -= length;
	    };
	    break;
	};
	/* u-sec tempo set */
	case 0x51:
	{   LONG	t;
	    BYTE	c;
	    int		i;
	    long	delta;
	    GET8BITS(c,track_pos[trnr],track_left[trnr]);
	    t=(LONG)c<<16;
	    GET8BITS(c,track_pos[trnr],track_left[trnr]);
	    t += (LONG)c<<8;
	    GET8BITS(c,track_pos[trnr],track_left[trnr]);
	    t += (LONG)c;
#ifdef DEBUG
	    (void)printf("usec %ld ",t);
#endif
	    t=(t*(LONG)CLK_TCK)/1000000L;

	    /* adjust all time values to the new unit system */
	    /* Note: the calculations must be in long, not LONG as some of
	       the intermediate values may become negative */

/* printf("tpb= %ld t= %ld delta=%ld clock_time=%ld\n",
	time_per_beat, t, delta, clock_time);
*/	
	    delta = track_delta[trnr];
	    clock_time = (clock_time-delta)*(long)t/time_per_beat + delta;
	    for (i=0;i<tracks;i++) {
/* printf("trnr= %d delta= %ld ", i, track_delta[i]);	    */
	        if (!track_finished[i])
	    	    track_delta[i] =
		        (track_delta[i]-delta)*(long)t/time_per_beat + delta;
/* printf("=> %ld\n", track_delta[i]);	    */
	    }
	    time_per_beat = t;
#ifdef DEBUG
	    (void)printf("our %ld\n",time_per_beat);
#endif
	    break;
	};

	/* end of track - can't be bothered to check left == 0 too */
	case 0x2f:
	    track_finished[trnr]=TRUE;
/* printf("track %d finished\n", trnr); */
	    finished_tracks++;
	    break;

	    /* ignore rest */
	default:
	    CHECKLEFT(track_left[trnr],length);
	    track_pos[trnr] += length;
	    track_left[trnr] -= length;
	    break;
    };
    return(TRUE);
};

static int	system_common(event)
BYTE	event;
{   switch(event)
    {	/* 3 byte instructions */
	case 0xf2:
	{   CHECKLEFT(track_left[trnr],2);
	    track_pos[trnr] += 2;
	    track_left[trnr] -= 2;
	    break;
	};
	/* 2 byte instructions */
	case 0xf1:
	case 0xf3:
	{   CHECKLEFT(track_left[trnr],1);
	    track_pos[trnr] += 1;
	    track_left[trnr] -= 1;
	    break;
	};
	/* rest are single byte */
    };
    return(TRUE);
};

static int	system_real_time()
{	/* all 1 byte so ignore */
	return(TRUE);
};

static void	all_notes_off()
{   /* MANUALLY TURN ALL NOTES OFF */
    /* time= (2+8)x16x(1+128x2)/31250 ~= 1.5 seconds */
    int	i,j;
    if (f_Timing) SEND (0xFC);
    for(i=0;i<16;i++)
    {	SEND((BYTE)(0x90+i));
	for(j=0;j<128;j++)
	{   SEND((BYTE)j);
	    SEND(0);
	};
    };
};

/* general error messages - I got fed up typing these over and over again */
static void	truncated()
{   (void)fprintf(stderr,"%s: %s truncated\n",app_name,gFile);
};

/*
 * REVISION LOG
 * ============
 * 0.1	SGoldthorpe	20-Mar-91	Created for Atari ST / Sozobon C.  It's
 *					a bit atari specific in places but i've
 *					tried to make it UNIX(tm) looking for
 *					easier porting (if anyone feels brave
 *					enough to try.
 * 0.2	SGoldthorpe	 7-Apr-91	Messed up the code in mp_intp to
 *					allow type 1 midi files.  Timing is
 *					still a bit hairy but it plays 80%
 *					of the files I have OK.
 * 0.3	SGoldthorpe	27-May-91	Reformatted & tidied up, sorted out
 *					running status and added flags.
 * 0.4	SGoldthorpe	20-Jul-91	Generally restructed and tidied up and
 *					added sysex events.
 *
 */
