/*
 * 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
 */

#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 */
static double	track_delta[MAX_TRACKS], division, time_per_beat;
static BYTE	*track_pos[MAX_TRACKS];
static int	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	i;

/* 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;
unsigned int	len;
{   BYTE	*pos=buffer,*next;
    BYTE	running_status[MAX_TRACKS],last_running_status;
    WORD	w;
    LONG	dw,delta;
    double	clock_delta;
    unsigned int 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=(double)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 = (double)(CLK_TCK * 60.0)/120.0; /* default 120bpm */
#ifdef DEBUG
    (void)printf("time per beat (1/200 sec) %f\n",time_per_beat);
#endif

    /* do some initialisation, track finding etc */
    finished_tracks = 0;
    for(i=0;i<tracks;i++)
    {	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+i) = pos;
	next = pos+dw;
	*(track_left+i) = (int)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+i),*(track_left+i));
	*(track_delta+i)=(double)dw;
	*(track_finished+i)=FALSE;
	*(running_status+i) = 0xfe;
	last_running_status = 0xfe;
    };

    /* let the user know what's happening */
    (void)printf("playing '%s' with %d tracks\n",file,tracks);

    /* get start time */
    clock_orig=clock();

    /* dispatcher */
    while (finished_tracks != tracks)
    {	BYTE	event;
	clock_delta = (double)(clock()-clock_orig)*division/time_per_beat;
	for(i=0;i<tracks;i++)
	{   if(!*(track_finished+i) & (clock_delta>*(track_delta+i)))
	    {	GET8BITS(event,*(track_pos+i),*(track_left+i));

		/* 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+i),*(track_left+i));
				SEND(c);
				GET8BITS(c,*(track_pos+i),*(track_left+i));
				SEND(c);
				*(running_status+i) = event;
				last_running_status = event;
				break;
			    };
			    /* program change */
			    case 0xc0:
			    {	BYTE	c;
				GET8BITS(c,*(track_pos+i),*(track_left+i));
				if (f_Program)
				{   SEND(event);
				    SEND(c);
				    *(running_status+i) = event;
				    last_running_status = event;
				};
				break;
			    };
			    /* channel pressure */
			    case 0xd0:
			    {	BYTE	c;
				GET8BITS(c,*(track_pos+i),*(track_left+i));
				if (f_Channel_pressure)
				{   SEND(event);
				    SEND(c);
				    *(running_status+i) = event;
				    last_running_status = event;
				};
				break;
			    };
			    default:
			    {	/* running status */
				if(*(running_status+i)!=last_running_status)
				{   SEND(*(running_status+i));
				    last_running_status=*(running_status+i);
				};
				if((event&0x80)==0)
				{   SEND(event);
#ifdef DEBUG
				    (void)printf("running stat (%02x)  %02x\n",
					*(running_status+i),event);
#endif
				    switch(*(running_status+i) & 0xf0)
				    {   /* 3 byte events */
					case 0x90:
					case 0x80:
					case 0xa0:
					case 0xb0:
					case 0xe0:
					{   BYTE c;
					    GET8BITS(c,*(track_pos+i),
						*(track_left+i));
					    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+i))
		{   GETVARLEN(delta,*(track_pos+i),*(track_left+i));
		    *(track_delta+i) += (double)delta;
		};
	    };
	};
	/* 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);
		};
	    };
	};
    };
    return(FALSE);
};

/* system exclusive events */
int	sysex_event(event)
BYTE	event;
{   LONG	length,l;
    GETVARLEN(length,*(track_pos+i),*(track_left+i));
    if(f_Sysex)
    {   BYTE	c;
	if(event==0xf0)
		SEND(0xf0);
	for(l=0;l<length;l++)
	{  GET8BITS(c,*(track_pos+i),*(track_left+i));
	   SEND(c);
	};
    }
    else
    {   CHECKLEFT(*(track_left+i),(int)length);
        *(track_pos+i) += length;
        *(track_left+i) -= length;
    };
    return(TRUE);
};

/* meta-events */
int meta_event()
{   BYTE	type;
    LONG	length;

    /* get type of meta event */
    GET8BITS(type,*(track_pos+i),*(track_left+i));
#ifdef DEBUG
    (void)printf("meta-event %02x ",type);
#endif

    /* get length */
    GETVARLEN(length,*(track_pos+i),*(track_left+i));
#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+i),*(track_left+i));
		    putchar(c);
		};
		/* lyrics should be on same line */
		if(type!=0x05)
		    printf("\"[Tr %d]\n",i);
	    }
	    else
	    {	CHECKLEFT(*(track_left+i),(int)length);
		*(track_pos+i) += length;
		*(track_left+i) -= length;
	    };
	    break;
	};
	/* u-sec tempo set */
	case 0x51:
	{   LONG	t;
	    BYTE	c;
	    GET8BITS(c,*(track_pos+i),*(track_left+i));
	    t=(LONG)c<<16;
	    GET8BITS(c,*(track_pos+i),*(track_left+i));
	    t += (LONG)c<<8;
	    GET8BITS(c,*(track_pos+i),*(track_left+i));
	    t += (LONG)c;
#ifdef DEBUG
	    (void)printf("usec %ld ",t);
#endif
	    t=(t*(LONG)CLK_TCK)/1000000L;
	    time_per_beat = (double)t;
#ifdef DEBUG
	    (void)printf("our %f\n",time_per_beat);
#endif
	    break;
	};

	/* end of track - can't be bothered to check left == 0 too */
	case 0x2f:
	    *(track_finished+i)=TRUE;
	    finished_tracks++;
	    break;

	    /* ignore rest */
	default:
	    CHECKLEFT(*(track_left+i),(int)length);
	    *(track_pos+i) += length;
	    *(track_left+i) -= length;
	    break;
    };
    return(TRUE);
};

static int	system_common(event)
BYTE	event;
{   switch(event)
    {	/* 3 byte instructions */
	case 0xf2:
	{   CHECKLEFT(*(track_left+i),2);
	    *(track_pos+i) += 2;
	    *(track_left+i) -= 2;
	    break;
	};
	/* 2 byte instructions */
	case 0xf1:
	case 0xf3:
	{   CHECKLEFT(*(track_left+i),1);
	    *(track_pos+i) += 1;
	    *(track_left+i) -= 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;
    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.
 *
 */
