/*

	                    Encode CSV file into MIDI

	The CSV file must be in the format generated by midicsv--while
	you can add and delete events and change their contents, the
	overall organisation of the file must be the same.

	Designed and implemented in October of 1998 by John Walker.
	Revised and updated in February of 2004 by the same perp.

		       http://www.fourmilab.ch/
		       
                This program is in the public domain.

*/

#define PROG	"csvmidi"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#ifdef _WIN32
#include <fcntl.h>
#include <io.h>
#define strcasecmp _stricmp
#endif

#include "version.h"
#include "types.h"
#include "midifile.h"
#include "midio.h"
#include "csv.h"
#include "getopt.h"

/*  List of comment delimiters recognised in CSV files.
    These are whole-line comments which must be marked
    by one of the following characters as the first
    nonblank character of a record.  Rest-of-line comments
    are not implemented.  Since the track number always
    begins a data record, any non-numeric character may
    be used as a comment delimiter.  */

#define COMMENT_DELIMITERS  "#;"

#define FALSE	0
#define TRUE	1

#define ELEMENTS(array) (sizeof(array)/sizeof((array)[0]))

/*  Codes for control items.  */

typedef enum {
    Header,
    Start_track,
    End_of_file
} controlMessages;

/*  The following table lists all possible item codes which may
    appear in a CSV-encoded MIDI file.	These should be listed
    in order of frequency of occurrence since the list is
    searched linearly.	*/

struct mitem {
    char *name;
    int icode;
};

#define EVENT	0
#define META	0x100
#define MARKER	0x200

#define Event(x)    ((x) | EVENT)
#define Meta(x)     ((x) | META)
#define Marker(x)   ((x) | MARKER)

static struct mitem mitems[] = {
    { "Note_on_c", Event(NoteOn) },
    { "Note_off_c", Event(NoteOff) },
    { "Pitch_bend_c", Event(PitchBend) },
    { "Control_c", Event(ControlChange) },
    { "Program_c", Event(ProgramChange) },
    { "Poly_aftertouch_c", Event(PolyphonicKeyPressure) },
    { "Channel_aftertouch_c", Event(ChannelPressure) },

    { "System_exclusive", Event(SystemExclusive) },
    { "System_exclusive_packet", Event(SystemExclusivePacket) },

    { "Sequence_number", Meta(SequenceNumberMetaEvent) },
    { "Text_t", Meta(TextMetaEvent) },
    { "Copyright_t", Meta(CopyrightMetaEvent) },
    { "Title_t", Meta(TrackTitleMetaEvent) },
    { "Instrument_name_t", Meta(TrackInstrumentNameMetaEvent) },
    { "Lyric_t", Meta(LyricMetaEvent) },
    { "Marker_t", Meta(MarkerMetaEvent) },
    { "Cue_point_t", Meta(CuePointMetaEvent) },
    { "Channel_prefix", Meta(ChannelPrefixMetaEvent) },
    { "MIDI_port", Meta(PortMetaEvent) } ,
    { "End_track", Meta(EndTrackMetaEvent) },
    { "Tempo", Meta(SetTempoMetaEvent) },
    { "SMPTE_offset", Meta(SMPTEOffsetMetaEvent) },
    { "Time_signature", Meta(TimeSignatureMetaEvent) },
    { "Key_signature", Meta(KeySignatureMetaEvent) },
    { "Sequencer_specific", Meta(SequencerSpecificMetaEvent) },
    { "Unknown_meta_event", Meta(0xFF) },

    { "Header", Marker(Header) },
    { "Start_track", Marker(Start_track) },
    { "End_of_file", Marker(End_of_file) },
};

static char *progname;		      /* Program name string */
static int verbose = FALSE; 	      /* Debug output */
static int zerotol = FALSE;    	      /* Any warning terminates processing */
static int errors = 0;	    	      /* Errors and warnings detected */
static char *s = NULL;	    	      /* Dynamically expandable CSV input buffer */

/*  OUTBYTE  --  Store byte in track buffer.  */

static byte of[10];
static byte *trackbuf = NULL, *trackbufp;
static int trackbufl;
static char *f = NULL;
static int flen = 0;

#define Warn(msg) { errors++; fprintf(stderr, "%s: Error on line %d:\n    %s\n  %s.\n", PROG, lineno, s, msg); if (zerotol) { exit(1); } }

static void outbyte(const byte c)
{
    long l = trackbufp - trackbuf;

    if (l >= trackbufl) {
	trackbuf = realloc(trackbuf, trackbufl += 16384);
	if (trackbuf == NULL) {
	    fprintf(stderr, "%s: Unable to allocate memory to expand track buffer to %d bytes.\n", PROG, trackbufl);
	    exit(2);
    	}
	trackbufp = trackbuf + l;
    }
    *trackbufp++ = c;
}

/*  OUTEVENT  --  Output event, optimising repeats.  */

static long abstime, tabstime = 0;
static void outVarLen(const vlint value);
static int optimiseStatus = TRUE, lastStatus = -1;

static void outevent(const byte c)
{
    outVarLen(abstime - tabstime);
    tabstime = abstime;
    if (!optimiseStatus || (c != lastStatus)) {
	outbyte(c);
	lastStatus = c;
    }
}

/*  OUTMETA  --  Output file meta-event.  */

static void outmeta(const byte c)
{
    /* Running status may not be used by file meta-events,
       and meta-events cancel any running status. */
    lastStatus = -1;
    outevent(FileMetaEvent);
    outbyte(c);
}

/*  OUTSHORT  --  Output two-byte value to track buffer.  */

static void outshort(const short v)
{
    outbyte((byte) ((v >> 8) & 0xFF));
    outbyte((byte) (v & 0xFF));
}

/*  OUTBYTES  --  Output a linear array of bytes.  */

static void outbytes(const byte *s, const int n)
{
    int i;

    outVarLen(n);
    for (i = 0; i < n; i++) {
	outbyte(*s++);
    }
}

/*  OUTVARLEN  --  Output variable length number.  */

static void outVarLen(const vlint v)
{
    vlint value = v;
    long buffer;

    buffer = value & 0x7F;
    while ((value >>= 7) > 0) {
	buffer <<= 8;
	buffer |= 0x80;
	buffer += (value & 0x7F);
    }

    while (TRUE) {
	outbyte((byte) (buffer & 0xFF));
	if (buffer & 0x80) {
	    buffer >>= 8;
	} else {
	    break;
	}
    }
}

/*  XFIELDS  --  Parse one or more numeric fields.  Returns FALSE if
    	    	 all fields were scanned successfully, TRUE if an
		 error occurred.  The fbias argument gives the absolute
		 field number of the first field to be parsed; it is used
		 solely to identify fields in error messages.  */

#define MAX_NFIELDS  10

static int lineno = 0;
static long nfld[MAX_NFIELDS];
static char *csvline;

static int xfields(const int n, const int fbias)
{
    int i;

#ifndef ndebug
    if (n > MAX_NFIELDS) {
        fprintf(stderr, "%s: Internal error: nfields(%d) exceeds max of %d.\n",
	    PROG, n, MAX_NFIELDS);
	abort();
    }
#endif
    for (i = 0; i < n; i++) {
	if (!CSVscanField(&f, &flen)) {
	    errors++;
            fprintf(stderr, "%s: Error on line %d:\n    %s\n  Missing field %d.\n",
		    PROG, lineno, s, fbias + i);
	    return TRUE;
	}
        if (sscanf(f, "%ld", &nfld[i]) != 1) {
	    errors++;
            fprintf(stderr, "%s: Error on line %d:\n    %s\n  Invalid field %d.\n",
		    PROG, lineno, s, fbias + i);
	    return TRUE;
	}
    }
    return FALSE;
}

/*  XFIELDS  --  Parse one or more numeric fields.  This is a
    	    	 wrapper for xfields which specifies the default
		 starting field of 4 used by most events.  */

static int nfields(const int n)
{
    return xfields(n, 4);
}

/*  CHECKBYTES  --  Pre-parse a sequence of arguments representing
    	    	    a byte vector.  If an error is detected, return
		    TRUE.  Otherwise, reset the CSV parser to the
		    first byte of the sequence and return FALSE.  This
		    permits code which handles arbitrary length byte
		    vectors to recover from syntax errors and ignore
		    the line prior to the irreversible step of emitting
		    the event type and length. */

static int checkBytes(const int fieldno, const int length)
{
    int i;
    
    for (i = 0; i < length; i++) {
	if (xfields(1, i + fieldno)) {
	    if (zerotol) {
	    	exit(1);
	    }
    	    return TRUE;
	}
    }
    
    /* The preliminary parsing pass passed.  (Got that?)  Now restart
       the CSV parser at the start of the line and ignore fields prior
       to the first byte of the data, leaving them ready to be processed
       by the actual translation code. */
    
    CSVscanInit(s);
    for (i = 0; i < (fieldno - 1); i++) {
    	CSVscanField(&f, &flen);
    }
    return FALSE;
}

/*  GETCSVLINE  --  Get next line from CSV file.  Reads into a dynamically
    	    	    allocated buffer s which is expanded as required to
		    accommodate longer lines.  All standard end of line
		    sequences are handled transparently, and the line
		    is returned with the end line sequence stripped
		    with a C string terminator (zero byte) appended.  */

static int getCSVline(FILE *fp)
{
    static int sl = 0;
    int c, ll = -1;
    
    if (s == NULL) {
    	sl = 1024;
	s = (char *) malloc(sl);
	if (s == NULL) {
	    fprintf(stderr, "%s: Unable to allocate %d byte CSV input line buffer.\n",
	    	    PROG, sl);
	    exit(2);
	}
    }
    
    while ((c = getc(fp)) >= 0) {
    
    	/*  Test for end of line sequence.  We accept either a carriage return or
	    line feed as an end of line delimiter, optionally followed by no more
	    than one of the other delimiter.  */
	    
    	if (c == '\n') {
	    c = getc(fp);
	    if (c != '\r') {
	    	ungetc(c, fp);
	    }
	    break;
    	}
    	if (c == '\r') {
	    c = getc(fp);
	    if (c != '\n') {
	    	ungetc(c, fp);
	    }
	    break;
    	}
	
	/*  Increment line length, expand buffer if necessary, and store character
	    in buffer.  */
	    
    	ll++;
	if (ll >= (sl - 1)) {
    	    sl += 1024;
	    s = (char *) realloc(s, sl);
	    if (s == NULL) {
		fprintf(stderr, "%s: Unable to expand CSV input line buffer to %d bytes.\n",
		    	PROG, sl);
		exit(2);
	    }
	}
	s[ll] = c;
    }
    
    /*	If we got a line, append a C string terminator to it.  */
    
    if (ll >= 0) {
    	s[ll + 1] = 0;
    }
    
    return (ll >= 0);
}

/*  CLAMP  --  Constrain a numeric value to be within a specified
    	       inclusive range.  If the value is outside the range
	       an error message is issued and the value is forced to
	       the closest limit of the range.  */
	       
static void clamp(long *value, const long minval, const long maxval, const char *fieldname)
{
    if (((*value) < minval) || ((*value) > maxval)) {
    	char errm[256];
	
	sprintf(errm, "%s out of range.  Value (%ld) outside limits of %ld to %ld",
	    fieldname, *value, minval, maxval);
	Warn(errm);
	
	if ((*value) < minval) {
    	    *value = minval;
	} else {
    	    *value = maxval;
	}
    }
}

/*  Main program  */

int main(int argc, char *argv[])
{
    struct mhead mh;
    struct mtrack mt;
    FILE *fp = stdin, *fo = stdout;
    int i, n, track, rtype, etype,
	headerseen = FALSE, eofseen = FALSE, intrack = 0, ntrack = 0;
    char errm[256];
    long tl;

    /* Parse command line arguments. */

    progname = argv[0];
    csvline = s;
    while ((n = getopt(argc, argv, "uvxz")) != -1) {
	switch (n) {
            case 'u':
                fprintf(stderr, "Usage: %s [ options ] [ csv_file ] [ midi_file ]\n", progname);
                fprintf(stderr, "       Options:\n");
                fprintf(stderr, "           -u            Print this message\n");
                fprintf(stderr, "           -v            Verbose: dump header and track information\n");
                fprintf(stderr, "           -x            Expand running status\n");
                fprintf(stderr, "           -z            Abort on any warning message\n");
		fprintf(stderr, "Version %s\n", VERSION);
		return 0;

            case 'v':
		verbose = TRUE;
		break;

            case 'x':
		optimiseStatus = FALSE;
		break;

            case 'z':
		zerotol = TRUE;
		break;

            case '?':
                fprintf(stderr, "%s: undefined option -%c specified.\n",
		    PROG, n);
		return 2;
	}
    }

    /* Open input and output files, if supplied.  Otherwise
       standard input and output are used.  A file name of
       "-" is equivalent to no specification. */

    i = 0;
    while (optind < argc) {
	switch (i++) {
	    case 0:
                if (strcmp(argv[optind], "-") != 0) {
                    fp = fopen(argv[optind], "r");
		    if (fp == NULL) {
                        fprintf(stderr, "%s: Unable to to open CSV input file %s\n",
				PROG, argv[optind]);
			return 2;
		    }
		}
		break;

	    case 1:
                if (strcmp(argv[optind], "-") != 0) {
                    fo = fopen(argv[optind], "wb");
		    if (fo == NULL) {
                        fprintf(stderr, "%s: Unable to to create MIDI output file %s\n",
				PROG, argv[optind]);
			return 2;
		    }
		}
		break;
	}
	optind++;
    }
    
#ifdef _WIN32

    /*  If output is to standard output, set the output
    	file mode to binary.  */

    if (fo == stdout) {
	_setmode(_fileno(fo), _O_BINARY);
    }
#endif

    /* Allocate initial track assembly buffer.	The buffer is
       expanded as necessary. */

    trackbufp = trackbuf = (byte *) malloc(trackbufl = 16384);
    memcpy(mt.chunktype, MIDI_Track_Sentinel, sizeof mt.chunktype);

#define Nfields(n) { if (nfields(n)) { if (zerotol) { exit(1); } else { continue; } } }
#define Clamp(fld, low, high, what) clamp(&nfld[fld - 4], low, high, "Field "  # fld " (" what ")")

    while (getCSVline(fp)) {
	char *p = s + strlen(s);

	lineno++;

	/* Trim any white space from the end of the record. */

	while (p >= s && isspace(*(p - 1))) {
	    *(--p) = 0;
	}

	/* Test for and ignore blank lines and comments.  A comment
	   is any line whose first character (ignoring leading white
	   space) is one of the COMMENT_DELIMITERS).  Note that we
	   do not permit in-line comments, although the user is free
	   to supply extra CSV fields which we ignore for that
	   purpose. */

    	p = s;
	while ((*p != 0) && isspace(*p)) {
	    p++;
	}
	if ((*p == 0) || (strchr(COMMENT_DELIMITERS, *p) != NULL)) {
	    continue;
	}

	CSVscanInit(s);

	/* Scan track, absolute time, and record type.	These are
	   present in all records. */

	if (CSVscanField(&f, &flen)) {
	    track = atoi(f);
	} else {
            Warn("Missing track number (field 1)");
	    continue;
	}
	tl = track;
	clamp(&tl, 0, 65535, "Field 1 (Track)");
	track = (int) tl; 

	if (CSVscanField(&f, &flen)) {
	    abstime = atol(f);
	} else {
            Warn("Missing absolute time (field 2)");
	    continue;
	}
	clamp(&abstime, 0, 0x7FFFFFFF, "Field 2 (Time)"); 

	if (!CSVscanField(&f, &flen)) {
            Warn("Missing record type (field 3)");
	    continue;
	}

	/* Look up record type and dispatch to appropriate handler. */

	rtype = -1;
	for (i = 0; i < ELEMENTS(mitems); i++) {
	    if (strcasecmp(f, mitems[i].name) == 0) {
		rtype = mitems[i].icode;
		break;
	    }
	}

	if (rtype < 0) {
            sprintf(errm, "Unknown record type: \"%s\"", f);
	    Warn(errm);
	    continue;
	}

	etype = rtype & 0xFF;

	/* File structure pseudo-events.  These records do
	   not correspond to items in the MIDI file. */

	switch (rtype) {

	    case Marker(Header):
		if (headerseen) {
                    Warn("Duplicate header record");
		    continue;
		}
		Nfields(3);
		memcpy(mh.chunktype, MIDI_Header_Sentinel, sizeof mh.chunktype);
		mh.length = 6;
		clamp(&(nfld[0]), 0, 2, "Field 4 (Format)");
		mh.format = (short) nfld[0];
		clamp(&(nfld[1]), 0, 65535, "Field 5 (Number of tracks)");
		mh.ntrks = (short) nfld[1];
		clamp(&(nfld[2]), 0, 65535, "Field 6 (Pulses per quarter note)");
		mh.division = (short) nfld[2];
		writeMidiFileHeader(fo, &mh);
		if (verbose) {
        	    fprintf(stderr, "Format %d MIDI file.  %d tracks, %d ticks per quarter note.\n",
			  mh.format, mh.ntrks, mh.division);
		}
		headerseen = TRUE;
		continue;

	    case Marker(Start_track):
		if (intrack != 0) {
                    Warn("Previous track end missing");
		    continue;
		}
		intrack = track;
		tabstime = 0;
		continue;

	    case Marker(End_of_file):
		eofseen = TRUE;
		if (intrack != 0) {
                    Warn("Last track end missing");
		}
		continue;
	}

	/* Verify events occur within Start_track / End_track
	   brackets and that track number is correct. */

	if (track != intrack) {
	    if (intrack == 0) {
                Warn("Event not within track");
		continue;
	    } else {
                Warn("Incorrect track number in event");
		continue;
	    }
	}

        /* Make sure absolute time isn't less than that
	   in previous event in this track. */

	if (abstime < tabstime) {
            Warn("Events out of order; this event is before the previous.");
	    continue;
	}

	switch (rtype) {

#define ClampChannel	Clamp(4, 0, 15, "Channel")
	    case Event(NoteOff):
	    case Event(NoteOn):
	    case Event(PolyphonicKeyPressure):
	    case Event(ControlChange):
		Nfields(3);
		ClampChannel;
    	    	Clamp(5, 0, 127, "Note number");
		Clamp(6, 0, 127, "Value");
		outevent((byte) (etype | nfld[0]));
		outbyte((byte) nfld[1]);
		outbyte((byte) nfld[2]);
		break;

	    case Event(ProgramChange):
	    case Event(ChannelPressure):
		Nfields(2);
		ClampChannel;
		Clamp(5, 0, 127, "Value");
		outevent((byte) (etype | nfld[0]));
		outbyte((byte) nfld[1]);
		break;

	    case Event(PitchBend):
		Nfields(2);
		ClampChannel;
		Clamp(5, 0, (1 << 14) - 1, "Value");
		outevent((byte) (etype | nfld[0]));
		/* Note that the pitch bend event consists of two
		   bytes each containing 7 bits of data with a
		   zero MSB.  Consequently, we cannot use outshort()
		   for the pitch bend data and must generate and emit
		   the two 7 bit values here.  The values are output
		   with the least significant 7 bits first, followed
		   by the most significant 7 bits. */
		outbyte((byte) (nfld[1] & 0x7F));
		outbyte((byte) ((nfld[1] >> 7) & 0x7F));
		break;

	    /* System-exclusive messages */

	    case Event(SystemExclusive):
	    case Event(SystemExclusivePacket):
		Nfields(1);
		Clamp(4, 0, (1 << 28) - 1, "Length");
		lastStatus = -1;	    /* Running status not used for sysex,
				    	       and sysex cancels any running
				    	       status in effect. */
		n = nfld[0];
		if (checkBytes(5, n)) {
		    continue;
		}
		outevent((byte) etype);
		outVarLen(n);   	    /* Length of following data */
		for (i = 0; i < n; i++) {
		    /* We must call xfields() inside the loop for
		       each individual system exclusive field since
		       the number of fields is unlimited and may
		       exceed MAX_NFIELDS. */
		    if (xfields(1, i + 5)) {
		    	abort();    	    /* Can't happen, thanks to checkBytes() above */
		    }
		    clamp(&nfld[0], 0, 255, "Sysex data byte");
		    outbyte((byte) nfld[0]);
		}
		break;

	    /* File meta-events */

	    case Meta(SequenceNumberMetaEvent):
		Nfields(1);
		Clamp(4, 0, (1 << 16) - 1, "Sequence number");
		outmeta((byte) etype);
		outbyte(2);
		outshort((short) nfld[0]);
		break;

	    case Meta(TextMetaEvent):
	    case Meta(CopyrightMetaEvent):
	    case Meta(TrackTitleMetaEvent):
	    case Meta(TrackInstrumentNameMetaEvent):
	    case Meta(LyricMetaEvent):
	    case Meta(MarkerMetaEvent):
	    case Meta(CuePointMetaEvent):
		if (!CSVscanField(&f, &flen)) {
		    Warn("Missing field 4.");
		}
		outmeta((byte) etype);
		outbytes((byte *) f, CSVfieldLength);
		break;

	    case Meta(ChannelPrefixMetaEvent):
	    case Meta(PortMetaEvent):
		Nfields(1);
		Clamp(4, 0, 255, "Number");
		outmeta((byte) etype);
		of[0] = (byte) nfld[0];
		outbytes(of, 1);
		break;

	    case Meta(EndTrackMetaEvent):
		outmeta((byte) etype);
		outbyte(0);	      /* All meta events must include length */
		mt.length = trackbufp - trackbuf;
		writeMidiTrackHeader(fo, &mt);
		if (verbose) {
        	    fprintf(stderr, "Track %d: length %ld.\n", ntrack + 1, mt.length);
		}
		fwrite(trackbuf, trackbufp - trackbuf, 1, fo);
		trackbufp = trackbuf;
		tabstime = 0;
		lastStatus = -1;
		intrack = 0;
		ntrack++;
		break;

	    case Meta(SetTempoMetaEvent):
		Nfields(1);
		Clamp(4, 0, (1 << 24) - 1, "Value");
		outmeta((byte) etype);
		of[0] = (byte) ((nfld[0] >> 16) & 0xFF);
		of[1] = (byte) ((nfld[0] >> 8) & 0xFF);
		of[2] = (byte) (nfld[0] & 0xFF);
		outbytes(of, 3);
		break;

	    case Meta(SMPTEOffsetMetaEvent):
		Nfields(5);
		Clamp(4, 0, 255, "Hour");
		Clamp(5, 0, 255, "Minute");
		Clamp(6, 0, 255, "Second");
		Clamp(7, 0, 255, "Frame");
		Clamp(8, 0, 255, "FrameFraction");
		outmeta((byte) etype);
		for (i = 0; i < 5; i++) {
		    of[i] = (byte) nfld[i];
		}
		outbytes(of, 5);
		break;

	    case Meta(TimeSignatureMetaEvent):
		Nfields(4);
		Clamp(4, 0, 255, "Numerator");
		Clamp(5, 0, 255, "Denominator");
		Clamp(6, 0, 255, "Click");
		Clamp(7, 0, 255, "NotesPerQuarter");
		outmeta((byte) etype);
		for (i = 0; i < 4; i++) {
		    of[i] = (byte) nfld[i];
		}
		outbytes(of, 4);
		break;

	    case Meta(KeySignatureMetaEvent):
		Nfields(1);
		if (!CSVscanField(&f, &flen)) {
		    Warn("Missing field 5");
		}
		outmeta((byte) etype);
		Clamp(4, -7, 7, "Key");
		of[0] = (byte) nfld[0];
    	    	if (strcasecmp(f, "major") == 0) {
		    of[1] = 0;
    	    	} else if (strcasecmp(f, "minor") == 0) {
		    of[1] = 1;
    	    	} else {
                    sprintf(errm, "Field 5 has invalid major/minor indicator \"%s\"", f);
		    Warn(errm);
		    of[1] = 0;
		}
		outbytes(of, 2);
		break;

	    case Meta(SequencerSpecificMetaEvent):
		Nfields(1);
		Clamp(4, 0, (1 << 28) - 1, "Length");
		n = nfld[0];
		if (checkBytes(5, n)) {
		    continue;
		}
		outmeta((byte) etype);
		outVarLen(n);	    	/* Length of following data */
		for (i = 0; i < n; i++) {
		    if (xfields(1, i + 5)) {	/* Scan data field */
		    	abort();    	    /* Can't happen, thanks to checkBytes() above */
		    }
		    clamp(&nfld[0], 0, 255, "Sequencer specific data byte");
		    outbyte((byte) nfld[0]);
		}
		break;

	    /* The code 0xFF indicates an unknown file meta
	       event.  Since meta events include a length field,
	       such events can be preserved in a CSV file by
	       including the unknown event code in the record. */

	    case Meta(0xFF):
		Nfields(2);	      /* Get type and length */
		Clamp(4, 0, 255, "UnknownMetaType");
		Clamp(5, 0, (1 << 28) - 1, "UnknownMetaLength");
		etype = nfld[0];
		n = nfld[1];
		if (checkBytes(6, n)) {
		    continue;
		}
		/* We output the length as a variable-length quantity
		   on the assumption that any meta event which can
		   have data longer than 127 bytes will use a variable
		   length len field. */
		outmeta((byte) etype);
		outVarLen(n);
		for (i = 0; i < n; i++) {
		    if (xfields(1, i + 6)) {	/* Scan data field */
		    	abort();    	    /* Can't happen, thanks to checkBytes() above */
		    }
		    clamp(&nfld[0], 0, 255, "Unknown meta data byte");
		    outbyte((byte) nfld[0]);
		}
		break;
	}
    }
    fclose(fp);

    if (!eofseen) {
        fprintf(stderr, "%s: Missing End_of_file record.\n", PROG);
	return 1;
    }

    return ((errors > 0) ? 1 : 0);
}
