/* Program to read a MIDI file and write out a new MIDI file that has
 * specified tracks delayed by a specified number of ticks.
 * This may be useful if the instruments on some tracks have a much
 * slower attack than those of other tracks,
 * which cause them to sound as if they are behind, even though they aren't.
 * To keep things very simple, this program reads stdin and writes to stdout,
 * so you will almost certainly want to redirect them to and from files or
 * use this program as a filter in a pipeline.
 * Each command line argument is expected to be a pair of numbers,
 * where the first number is the track number, and the second is how
 * many MIDI clock ticks to delay that track. Any track not mentioned
 * in any argument will be copied to the output unchanged. So, for example,
 *	mididelay 2,10 4,5 < origfile > newfile
 * will read 'origfile' and create 'newfile', where newfile will have track 2
 * delayed by 10 ticks from the original, and track 4 delayed by 5 ticks.
 */

#include <stdio.h>

int midi_header(void);
int * process_command_line_arguments(int argc, char **argv, int number_of_tracks);
int do_a_track(int delay);
void copy(int length);

int
main(int argc, char **argv)
{
	int number_of_tracks;
	int *delays;	/* pointer to malloc-ed array containing
			 * list of delays, 1 element for each track */
	int track;


	/* Read the MIDI header, make sure it's okay, and find out how many
	 * tracks there are in the rest of the file */
	number_of_tracks = midi_header();

	/* Validate the command line arguments and save away the information
	 * from them about which tracks to delay. */
	delays = process_command_line_arguments(argc, argv, number_of_tracks);

	/* Process each track */
	for (track = 0; track < number_of_tracks; track++) {
		do_a_track(delays[track]);
	}

	return(0);
}

/* Read and echo the MIDI header. Return number of tracks.
 * Exit if something goes wrong. */

int
midi_header(void)
{
	unsigned char buff[14];

	if (read(0, buff, 14) != 14) {
		(void) fprintf(stderr, "failed to read MIDI header\n");
		exit(1);
	}

	if ( strncmp(buff, "MThd", 4) != 0 || buff[4] != '\0' ||
			buff[5] != '\0' || buff[6] != '\0' || buff[7] != '\6') {
		(void) fprintf(stderr, "Not a MIDI file\n");
		exit(1);
	}

	/* Echo out the header */
	(void) write(1, buff, 14);

	/* The number of tracks is in bytes 10 and 11 */
	return ( buff[10] * 255 + buff[11]);
}

/* Go through command line arguments, building a list of delay values.
 * Return array of ints that contain the delay values for each track.
 */

int *
process_command_line_arguments(int argc, char **argv, int number_of_tracks)
{
	int *delays;	/* list of delays */
	int a;		/* argument index */
	int track;
	char *p;	/* pointer into argv string where there
			 * should be a comma */

	/* Get enough space to store a delay value for each track */
	if ((delays = (int *) calloc(number_of_tracks, sizeof(int))) == (int *) 0) { 
		(void) fprintf(stderr, "malloc failed\n");
		exit(1);
	}

	/* Arguments should be pairs of numbers. The first is a track, the
	 * second the delay for that track.
 	 */
	for (a = 1; a < argc; a++) {
		track = strtol(argv[a], &p, 0);
		if (*p != ',') {
			(void) fprintf(stderr, "missing comma in argument #%d\n", a);
			exit(1);
		}
		if (track < 0 || track >= number_of_tracks) {
			(void) fprintf(stderr, "track %d out of range (0-%d)\n",
				track, number_of_tracks - 1);
			exit(1);
		}

		/* Save the delay value for the specified track number */
		delays[track] = strtol(p+1, &p, 0);
	}

	return(delays);
}

/* Process one track, adding delay if needed. Copy track, but add delay
 * to first delta value, adjusting track length if necessary.
 */

int
do_a_track(int delay)
{
	unsigned char buff[8];
	unsigned char delta_buff[4];
	long length;
	long delta_length;
	long new_delta_length;	/* length of delta after adjustment */
	long delta;


	if (read(0, buff, 8) != 8) {
		(void) fprintf(stderr, "unable to read track header\n");
		exit(1);
	}
	if (strncmp(buff, "MTrk", 4) != 0) {
		(void) fprintf(stderr, "invalid track header\n");
		exit(1);
	}

	/* get the track length */
	length = ((buff[4] << 24) & 0xff000000)
		| ((buff[5] << 16) & 0xff0000)
		| ((buff[6] << 8) & 0xff00)
		| (buff[7] & 0xff);

	/* if the delay is zero, our job is easy, just copy the track as is */
	if (delay == 0) {
		(void) write(1, buff, 8);
		copy(length);
		return;
	}

	/* extract the first delta time. Read bytes till we get one without
	 * its high order bit set. */
	for (delta_length = delta = 0;   ; ) {
		read(0, &(delta_buff[delta_length]), 1);
		delta = (delta << 7) | (delta_buff[delta_length] & 0x7f);
		delta_length++;
		if ( (delta_buff[0] & 0x80) == 0) {
			break;
		}
	}

	/* add on the adjustment for this track */
	delta += delay;

	if (delta < 0) {
		(void) fprintf(stderr, "adjustment resulted in negative delta value\n");
		exit(1);
	};

	/* create new delta value as variable-length string */
	new_delta_length = 0;
	if (delta > 0x1fffff) {
		delta_buff[new_delta_length++] = 0x80 | ((delta >> 21) & 0x7f);
		delta = delta & 0x1fffff;
	}
	if (delta > 0x3fff) {
		delta_buff[new_delta_length++] = 0x80 | ((delta >> 14) & 0x7f);
		delta = delta & 0x3fff;
	}
	if (delta > 0x7f) {
		delta_buff[new_delta_length++] = 0x80 | ((delta >> 7) & 0x7f);
		delta = delta & 0x7f;
	}
	delta_buff[new_delta_length++] = delta;
	
	/* adjust the track length if necessary to account for different delta
	 * length */
	if (new_delta_length != delta_length) {
		length = length - delta_length + new_delta_length;
		buff[4] = (length >> 24) & 0xff;
		buff[5] = (length >> 16) & 0xff;
		buff[6] = (length >> 8) & 0xff;
		buff[7] = length & 0xff;
	}

	/* echo back the track header, with possibly adjusted length */
	(void) write(1, buff, 8);

	/* write out the new delta */
	(void) write(1, delta_buff, new_delta_length);

	/* copy the rest of the track as is */
	copy(length - new_delta_length);
}

/* Copy length bytes from stdin to stdout */

void
copy(int length)
{
	int chunk_length;
	unsigned char buff[BUFSIZ];

	for ( ; ; ) {
		/* Do BUFSIZ at a time, unless less than that is left */
		chunk_length = (length > BUFSIZ ? BUFSIZ : length);
		if (chunk_length == 0) {
			return;
		}

		/* Copy from stdin to stdout */
		if (read(0, buff, chunk_length) != chunk_length) {
			(void) fprintf(stderr, "read failed\n");
			exit(1);
		}
		if (write(1, buff, chunk_length) != chunk_length) {
			(void) fprintf(stderr, "write failed\n");
			exit(1);
		}

		/* Figure out how much we have left to copy */
		length -= chunk_length;
	}
}
