view at/ofai/music/util/EventList.java @ 5:bcb4c9697967 tip

Add README and CITATION files
author Chris Cannam
date Tue, 03 Dec 2013 12:58:05 +0000
parents 4c3f5bc01c97
children
line wrap: on
line source
/*
	Copyright (C) 2001, 2006 by Simon Dixon

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; either version 2 of the License, or
	(at your option) any later version.

	This program 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 General Public License for more details.

	You should have received a copy of the GNU General Public License along
	with this program (the file gpl.txt); if not, download it from
	http://www.gnu.org/licenses/gpl.txt or write to the
	Free Software Foundation, Inc.,
	51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

package at.ofai.music.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.io.Serializable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.Sequence;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Track;

import at.ofai.music.worm.Worm;
import at.ofai.music.worm.WormFile;
import at.ofai.music.worm.WormParameters;

// Adapted from eventList::readMatchFile in beatroot/src/eventMidi.cpp

// Reads in a Prolog score+performance (.match) file; returns it as an eventList
// Lines in the match file can be of the form:
//		hammer_bounce-PlayedNote.
//		info(Attribute, Value).
//		insertion-PlayedNote.
//		ornament(Anchor)-PlayedNote.
//		ScoreNote-deletion.
//		ScoreNote-PlayedNote.
//		ScoreNote-trailing_score_note.
//		trailing_played_note-PlayedNote.
//		trill(Anchor)-PlayedNote.
// where ScoreNote is of the form
//		snote(Anchor,[NoteName,Modifier],Octave,Bar:Beat,Offset,Duration,
//				BeatNumber,DurationInBeats,ScoreAttributesList)
//		e.g. snote(n1,[b,b],5,1:1,0,3/16,0,0.75,[s])
// and PlayedNote is of the form
//		note(Number,[NoteName,Modifier],Octave,Onset,Offset,AdjOffset,Velocity)
//		e.g. note(1,[a,#],5,5054,6362,6768,53)

class WormFileParseException extends RuntimeException {

	static final long serialVersionUID = 0;
	public WormFileParseException(String s) {
		super(s);
	} // constructor

} // class WormFileParseException

class MatchFileParseException extends RuntimeException {

	static final long serialVersionUID = 0;
	public MatchFileParseException(String s) {
		super(s);
	} // constructor

} // class MatchFileParseException

class BTFileParseException extends RuntimeException {

	static final long serialVersionUID = 0;
	public BTFileParseException(String s) {
		super(s);
	} // constructor

} // class BTFileParseException


// Process the strings which label extra features of notes in match files.
// We assume no more than 32 distinct labels in a file.
class Flags {

	String[] labels = new String[32];
	int size = 0;
	
	int getFlag(String s) {
		if ((s == null) || s.equals(""))
			return 0;
		//int val = 1;
		for (int i = 0; i < size; i++)
			if (s.equals(labels[i]))
				return 1 << i;
		if (size == 32)	{
			System.err.println("Overflow: Too many flags: " + s);
			size--;
		}
		labels[size] = s;
		return 1 << size++;
	} // getFlag()

	String getLabel(int i) {
		if (i >= size)
			return "ERROR: Unknown flag";
		return labels[i];
	} // getLabel()

} // class Flags


// A score/match/midi file is represented as an EventList object,
//  which contains pointers to the head and tail links, and some
//  class-wide parameters. Parameters are class-wide, as it is
//  assumed that the Worm has only one input file at a time.
public class EventList implements Serializable {

	public LinkedList<Event> l;

	protected static boolean timingCorrection = false;
	protected static double timingDisplacement = 0;
	protected static int clockUnits = 480;
	protected static int clockRate = 500000;
	protected static double metricalLevel = 0;
	public static final double UNKNOWN = Double.NaN;
	protected static boolean noMelody = false;
	protected static boolean onlyMelody = false;
	protected static Flags flags = new Flags();

	public EventList() {
		l = new LinkedList<Event>();
	} // constructor

	public EventList(EventList e) {
		this();
		ListIterator<Event> it = e.listIterator();
		while (it.hasNext())
			add(it.next());
	} // constructor

	public EventList(Event[] e) {
		this();
		for (int i=0; i < e.length; i++)
			add(e[i]);
	} // constructor

	public void add(Event e) {
		l.add(e);
	} // add()

	public void add(EventList ev) {
		l.addAll(ev.l);
	} // add()

	public void insert(Event newEvent, boolean uniqueTimes) {
		ListIterator<Event> li = l.listIterator();
		while (li.hasNext()) {
			int sgn = newEvent.compareTo(li.next());
			if (sgn < 0) {
				li.previous();
				break;
			} else if (uniqueTimes && (sgn == 0)) {
				li.remove();
				break;
			}
		}
		li.add(newEvent);
	} // insert()

	public ListIterator<Event> listIterator() {
		return l.listIterator();
	} // listIterator()

	public Iterator<Event> iterator() {
		return l.iterator();
	} // iterator()

	public int size() {
		return l.size();
	} // size()

	public Event[] toArray() {
		return toArray(0);
	} // toArray()

	public double[] toOnsetArray() {
		double[] d = new double[l.size()];
		int i = 0;
		for (Iterator<Event> it = l.iterator(); it.hasNext(); i++)
			d[i] = it.next().keyDown;
		return d;
	} // toOnsetArray()

	public Event[] toArray(int match) {
		int count = 0;
		for (Event e : l)
			if ((match == 0) || (e.midiCommand == match))
				count++;
		Event[] a = new Event[count];
		int i = 0;
		for (Event e : l)
			if ((match == 0) || (e.midiCommand == match))
				a[i++] = e;
		return a;
	} // toArray()

	public void writeBinary(String fileName) {
		try {
			ObjectOutputStream oos = new ObjectOutputStream(
										new FileOutputStream(fileName));
			oos.writeObject(this);
			oos.close();
		} catch (IOException e) {
			System.err.println(e);
		}
	} // writeBinary()

	public static EventList readBinary(String fileName) {
		try {
			ObjectInputStream ois = new ObjectInputStream(
										new FileInputStream(fileName));
			EventList e = (EventList) ois.readObject();
			ois.close();
			return e;
		} catch (IOException e) {
			System.err.println(e);
			return null;
		} catch (ClassNotFoundException e) {
			System.err.println(e);
			return null;
		}
	} // readBinary()

	public void writeMIDI(String fileName) {
		writeMIDI(fileName, null);
	} // writeMIDI()

	public void writeMIDI(String fileName, EventList pedal) {
		try {
			MidiSystem.write(toMIDI(pedal), 1, new File(fileName));
		} catch (Exception e) {
			System.err.println("Error: Unable to write MIDI file " + fileName);
			e.printStackTrace();
		}
	} // writeMIDI()

	public Sequence toMIDI(EventList pedal) throws InvalidMidiDataException {
		final int midiTempo = 1000000;
		Sequence s = new Sequence(Sequence.PPQ, 1000);
		Track[] tr = new Track[16];
		tr[0] = s.createTrack();
		MetaMessage mm = new MetaMessage();
		byte[] b = new byte[3];
		b[0] = (byte)((midiTempo >> 16) & 0xFF);
		b[1] = (byte)((midiTempo >> 8) & 0xFF);
		b[2] = (byte)(midiTempo & 0xFF);
		mm.setMessage(0x51, b, 3);
		tr[0].add(new MidiEvent(mm, 0L));
		for (Event e : l) {		// from match or beatTrack file
			if (e.midiCommand == 0)	// skip beatTrack file
				break;
			if (tr[e.midiTrack] == null)
				tr[e.midiTrack] = s.createTrack();
			//switch (e.midiCommand) 
			//case ShortMessage.NOTE_ON:
			//case ShortMessage.POLY_PRESSURE:
			//case ShortMessage.CONTROL_CHANGE:
			//case ShortMessage.PROGRAM_CHANGE:
			//case ShortMessage.CHANNEL_PRESSURE:
			//case ShortMessage.PITCH_BEND:
			ShortMessage sm = new ShortMessage();
			sm.setMessage(e.midiCommand, e.midiChannel,
							e.midiPitch, e.midiVelocity);
			tr[e.midiTrack].add(new MidiEvent(sm,
						(long)Math.round(1000 * e.keyDown)));
			if (e.midiCommand == ShortMessage.NOTE_ON) {
				sm = new ShortMessage();
				sm.setMessage(ShortMessage.NOTE_OFF, e.midiChannel, e.midiPitch, 0);
				tr[e.midiTrack].add(new MidiEvent(sm, (long)Math.round(1000 * e.keyUp)));
			}
		}
		if (pedal != null) {	// from MIDI file
	//		if (t.size() > 0)	// otherwise beatTrack files leave an empty trk
	//			t = s.createTrack();
			for (Event e : pedal.l) {
				if (tr[e.midiTrack] == null)
					tr[e.midiTrack] = s.createTrack();
				ShortMessage sm = new ShortMessage();
				sm.setMessage(e.midiCommand, e.midiChannel, 
								e.midiPitch, e.midiVelocity);
				tr[e.midiTrack].add(new MidiEvent(sm,
						(long)Math.round(1000 * e.keyDown)));
				if (e.midiCommand == ShortMessage.NOTE_ON) {
					sm = new ShortMessage();
					sm.setMessage(ShortMessage.NOTE_OFF, e.midiChannel,
									e.midiPitch,e.midiVelocity);
					tr[e.midiTrack].add(new MidiEvent(sm,
							(long)Math.round(1000 * e.keyUp)));
				}
				//catch (InvalidMidiDataException exception) {}
			}
		}
		return s;
	} // toMIDI()

	public static EventList readMidiFile(String fileName) {
		return readMidiFile(fileName, 0);
	} // readMidiFile()

	public static EventList readMidiFile(String fileName, int skipTrackFlag) {
		EventList list = new EventList();
		Sequence s;
		try {
			s = MidiSystem.getSequence(new File(fileName));
		} catch (Exception e) {
			e.printStackTrace();
			return list;
		}
		double midiTempo = 500000;
		double tempoFactor = midiTempo / s.getResolution() / 1000000.0;
		// System.err.println(tempoFactor);
		Event[][] noteOns = new Event[128][16];
		Track[] tracks = s.getTracks();
		for (int t = 0; t < tracks.length; t++, skipTrackFlag >>= 1) {
			if ((skipTrackFlag & 1) == 1)
				continue;
			for (int e = 0; e < tracks[t].size(); e++) {
				MidiEvent me = tracks[t].get(e);
				MidiMessage mm = me.getMessage();
				double time = me.getTick() * tempoFactor;
				byte[] mesg = mm.getMessage();
				int channel = mesg[0] & 0x0F;
				int command = mesg[0] & 0xF0;
				if (command == ShortMessage.NOTE_ON) {
					int pitch = mesg[1] & 0x7F;
					int velocity = mesg[2] & 0x7F;
					if (noteOns[pitch][channel] != null) {
						if (velocity == 0) {	// NOTE_OFF in disguise :(
							noteOns[pitch][channel].keyUp = time;
							noteOns[pitch][channel].pedalUp = time;
							noteOns[pitch][channel] = null;
						} else
 							System.err.println("Double note on: n=" + pitch +
									" c=" + channel +
									" t1=" + noteOns[pitch][channel] +
									" t2=" + time);
					} else {
						Event n = new Event(time, 0, 0, pitch, velocity, -1, -1,
										0, ShortMessage.NOTE_ON, channel, t);
						noteOns[pitch][channel] = n;
						list.add(n);
					}
				} else if (command == ShortMessage.NOTE_OFF) {
					int pitch = mesg[1] & 0x7F;
					noteOns[pitch][channel].keyUp = time;
					noteOns[pitch][channel].pedalUp = time;
					noteOns[pitch][channel] = null;
				} else if (command == 0xF0) {
					if ((channel == 0x0F) && (mesg[1] == 0x51)) {
						midiTempo = (mesg[5] & 0xFF) |
									((mesg[4] & 0xFF) << 8) |
									((mesg[3] & 0xFF) << 16);
						tempoFactor = midiTempo / s.getResolution() / 1000000.0;
					//	System.err.println("Info: Tempo change: " + midiTempo +
					//						"  tf=" + tempoFactor);
					}
				} else if (mesg.length > 3) {
					System.err.println("midi message too long: " + mesg.length);
					System.err.println("\tFirst byte: " + mesg[0]);
				} else {
					int b0 = mesg[0] & 0xFF;
					int b1 = -1;
					int b2 = -1;
					if (mesg.length > 1)
						b1 = mesg[1] & 0xFF;
					if (mesg.length > 2)
						b2 = mesg[2] & 0xFF;
					list.add(new Event(time, time, -1, b1, b2, -1, -1, 0,
										b0 & 0xF0, b0 & 0x0F, t));
				}
			}
		}
		for (int pitch = 0; pitch < 128; pitch++)
			for (int channel = 0; channel < 16; channel++)
				if (noteOns[pitch][channel] != null)
					System.err.println("Missing note off: n=" + 
							noteOns[pitch][channel].midiPitch + " t=" +
							noteOns[pitch][channel].keyDown);
		return list;
	} // readMidiFile()

	public void print() {
		for (Iterator<Event> i = l.iterator(); i.hasNext(); )
			i.next().print(flags);
	} // print()

	public static void setTimingCorrection(double corr) {
		timingCorrection = corr >= 0;
		timingDisplacement = corr;
	} // setTimingCorrection()

	public static EventList readBeatsAsText(String fileName) throws Exception {
		EventList list = new EventList();
		BufferedReader inputFile = new BufferedReader(new FileReader(fileName));
		String s = inputFile.readLine();
		if (s.startsWith("###"))
			return readLabelFile(fileName);
		int beats = 0;
		int pitch = 56;
		int vol = 80;
		int ch = 10;
		int track = 0;
		int fl = 1;
		while (s != null) {
			int ind = s.indexOf(',');
			if (ind < 0)
				ind = s.indexOf(' ');
			double time = 0;
			if (ind >= 0) {
				String tmp = s.substring(0,ind).trim();
				if (tmp.length() == 0) {
					s = inputFile.readLine();
					continue;
				}
				time = Double.parseDouble(tmp);
				s = s.substring(ind+1);
			} else {
				String tmp = s.trim();
				if (tmp.length() > 0)
					time = Double.parseDouble(tmp);
				s = inputFile.readLine();
			}
			list.add(new Event(time, time, time, pitch, vol, ++beats,
				1.0, fl, ShortMessage.NOTE_ON, ch, track));
		}
		return list;
	} // readBeatsAsText()
	
	public static EventList readBeatTrackFile(String fileName) throws Exception{
		if (!fileName.endsWith(".tmf")) // || fileName.endsWith(".csv"))
			return readBeatsAsText(fileName);
		else {
			EventList list = new EventList();
			BufferedReader inputFile = new BufferedReader(new FileReader(fileName));
			Matcher s = new Matcher(inputFile.readLine());
			if (!s.matchString("MFile"))
				throw new BTFileParseException("Header not found");
			s.getInt();	// skip fileType
			int tracks = s.getInt();
			int div = s.getInt();
			int tempo = 500000;	// default tempo
			double tf = 1e6 / tempo * div;
			int lineCount = 1;
			int beats = 0;
			for (int track = 0; track < tracks; track++) {
				s.set(inputFile.readLine());
				lineCount++;
				if (!s.matchString("MTrk"))
					throw new BTFileParseException("MTrk not found");
				s.set(inputFile.readLine());
				lineCount++;
				while (!s.matchString("TrkEnd")) {
					double time = s.getInt() / tf;
					s.trimSpace();
					if (s.matchString("Tempo")) {
						tempo = s.getInt();
						tf = 1e6 / tempo * div;
					} else if (s.matchString("On")) {
						s.trimSpace();
						s.matchString("ch=");
						int ch = s.getInt();
						s.trimSpace();
						if (!s.matchString("n="))
							s.matchString("note=");
						int pitch = s.getInt();
						s.trimSpace();
						if (!s.matchString("v="))
							s.matchString("vol=");
						int vol = s.getInt();
						s.set(inputFile.readLine());
						lineCount++;
						s.getInt();
						s.trimSpace();
						s.matchString("Off");
						s.skip('v');
						s.matchString("ol");
						s.matchString("=");
						int flags = s.getInt();
						list.add(new Event(time, time, time, pitch, vol, ++beats,
								1.0, flags, ShortMessage.NOTE_ON, ch, track));
					} else if (!s.matchString("Meta TrkEnd")) {
						System.err.println("Unmatched text on line " + lineCount +
								": " + s.get());
					}
					s.set(inputFile.readLine());
					lineCount++;
				}
			}
			return list;
		}
	} // readBeatTrackFile()

	public void writeBeatsAsText(String fileName) throws Exception {
		PrintStream out = new PrintStream(new File(fileName));
		char separator = '\n';
		if (fileName.endsWith(".csv"))
			separator = ',';
		for (Iterator<Event> it = iterator(); it.hasNext(); ) {
			Event e = it.next();
			out.printf("%5.3f%c", e.keyDown, it.hasNext()? separator: '\n');
		}
		out.close();
	} // writeBeatsAsText()

	public void writeBeatTrackFile(String fileName) throws Exception {
		if (fileName.endsWith(".txt") || fileName.endsWith(".csv"))
			writeBeatsAsText(fileName);
		else {
			PrintStream out = new PrintStream(new File(fileName));
			out.println("MFile 0 1 500");
			out.println("MTrk");
			out.println("     0 Tempo 500000");
			int time = 0;
			for (Iterator<Event> it = iterator(); it.hasNext(); ) {
				Event e = it.next();
				time = (int) Math.round(1000 * e.keyDown);
				out.printf("%6d On   ch=%3d n=%3d v=%3d\n",
							time, e.midiChannel, e.midiPitch, e.midiVelocity);
				out.printf("%6d Off  ch=%3d n=%3d v=%3d\n",
							time, e.midiChannel, e.midiPitch, e.flags);
			}
			out.printf("%6d Meta TrkEnd\nTrkEnd\n", time);
			out.close();
		}
	} // writeBeatTrackFile()

	/** Reads a file containing time,String pairs into an EventList. */
	public static EventList readLabelFile(String fileName) throws Exception {
		EventList list = new EventList();
		BufferedReader inputFile = new BufferedReader(new FileReader(fileName));
		Matcher s = new Matcher(inputFile.readLine());
		int prevBar = 0;
		int beats = 0;
		int pitch = 56;
		int vol = 80;
		int ch = 10;
		int track = 0;
		while (s.hasData()) {
			if (!s.matchString("#")) {
				double time = s.getDouble();
				String label = s.get().trim();
				int colon = label.indexOf(':');
				int beat = 0;
				if (colon < 0)
					colon = label.length();
				else
					beat = Integer.parseInt(label.substring(colon+1));
				int bar = Integer.parseInt(label.substring(0, colon));
				int flags = WormFile.BEAT;
				if (bar != prevBar) {
					flags |= WormFile.BAR;
					prevBar = bar;
				}
				WormEvent ev = new WormEvent(time, time, time, pitch, vol,
						++beats,1.0,flags, ShortMessage.NOTE_ON, ch, track);
				ev.label = label;
				list.add(ev);
//				System.out.println(time + " " + label);
			}
			s.set(inputFile.readLine());
		}
		return list;
	} // readLabelFile()

	public void writeLabelFile(String fileName) throws Exception {
		PrintStream out = new PrintStream(new File(fileName));
		out.printf("###Created automatically\n");
		for (Event ev : l)
			out.printf("%5.3f\t%s\n", ev.keyDown, ((WormEvent)ev).label);
		out.close();
	} // writeLabelFile()

	public static EventList readWormFile(String fileName) throws Exception {
		EventList list = new EventList();
		BufferedReader inputFile = new BufferedReader(new FileReader(fileName));
		Matcher s = new Matcher(inputFile.readLine());
		int lineCount = 1;
		if (!s.matchString("WORM Version:"))
			throw new WormFileParseException("WORM format: header not found");
		if (s.getDouble() < 1.01)
			throw new WormFileParseException("WORM format: v1.0 not supported");
		int dataCountDown = -1;
		int beat = 0;
		while (true) {
			s.set(inputFile.readLine());
			lineCount++;
			if (dataCountDown == 0) {
				if (s.hasData())
					System.err.println("Ignoring trailing data past line " +
										lineCount);
				return list;
			} else if (!s.hasData())
				throw new WormFileParseException("Unexpected EOF");
			if (dataCountDown < 0) {
				if (s.matchString("Length:"))
					dataCountDown = s.getInt();
			} else {
				double time = s.getDouble();
				double tempo = s.getDouble();
				double loudness = s.getDouble();
				int flags = s.getInt();
				if ((flags & WormFile.TRACK) != 0)
					beat++;		// i.e. always, as index for comparing files 
				list.add(new WormEvent(time, tempo, loudness, beat, flags));
				dataCountDown--;
			}
		}
	} // readWormFile()

	public static String getAudioFileFromWormFile(String wormFile) {
		return getWormFileAttribute(wormFile, "AudioFile");
	} // getAudioFileFromWormFile()

	public static double getTrackLevelFromWormFile(String wormFile) {
		String level = getWormFileAttribute(wormFile,WormParameters.TRACKLEVEL);
		try {
			int i = level.indexOf("/");
			if (i >= 0)
				return Double.parseDouble(level.substring(0,i)) /
						Double.parseDouble(level.substring(i+1));
			else
				return Double.parseDouble(level);
		} catch (Exception e) {
			System.err.println("Error getting TrackLevel:\n" + e);
			return 1;
		}
	} // getTrackLevelFromWormFile()

	public static String getWormFileAttribute(String wormFile, String attr) {
		try {
			BufferedReader r = new BufferedReader(new FileReader(wormFile));
			String line = r.readLine();
			attr += ":";
			while (line != null) {
				if (line.startsWith(attr))
					return line.substring(attr.length()).trim();
				line = r.readLine();
			}
		} catch (Exception e) {
			System.err.println(e);
		}
		return null;
	} // getWormFileAttribute()

	public static EventList readMatchFile(String fileName) throws Exception {
		EventList list = new EventList();
		boolean startNote = timingCorrection;
		int eventFlags, numerator, denominator;
		String element;
		BufferedReader inputFile = new BufferedReader(new FileReader(fileName));
		double versionNumber = 1.0;
		double onset, offset, eOffset, beat, duration;
		int velocity, pitch, octave;
		int lineCount = 1;
		Matcher s = new Matcher(inputFile.readLine());
		while (s.hasData()) {
			eventFlags = 0;
			beat = UNKNOWN;
			duration = UNKNOWN;
			// System.out.println("Processing line " + lineCount);
			if (s.matchString("info(")) {	// meta-data
				if (s.matchString("timeSignature,")) {
					numerator = s.getInt();
					// ss1 << "beatsPerBar=" << numerator << ends;
					s.skip('/');
					denominator = s.getInt();
					// ss2 << "beatUnits=" << denominator;
				} else if (s.matchString("beatSubdivision,")) {
					// strcpy(buf, "beatSubdivisions=");
					// int i = strlen(buf);
					// f.getline(buf+i, SZ-i, ']');
					// strcat(buf, "]");
					// parameters->add(buf);
					s.skip(']');
				} else if (s.matchString("matchFileVersion,")) {
					versionNumber = s.getDouble();
				} else if (s.matchString("midiClockUnits,")) {
					clockUnits = s.getInt();
				} else if (s.matchString("midiClockRate,")) {
					clockRate = s.getInt();
				}
				s.set("%");	// don't expect the second half of the Prolog term
			} else if (s.matchString("snote(")) {
				s.skip(',');	// identifier
				s.skip(']');	// note name
				s.skip(',');	// ',' after note name
				s.skip(',');	// octave
				s.skip(',');	// onset time (in beats, integer part, bar:beat)
				boolean isBt = s.matchString("0");
				s.skip(',');	// onset time (in beats, fractional part)
				s.skip(',');	// duration (in beats, fraction)
				try {
					beat = s.getDouble();
				} catch (NumberFormatException e) {
					System.err.println("Bad beat number on line " + lineCount);
					beat = UNKNOWN;
				}
				if ((beat == Math.rint(beat)) != isBt)
					System.err.println("Inconsistent beats on line "+lineCount);
				s.skip(',');	// onset time (in beats, decimal) 
				try {
					duration = s.getDouble() - beat;
				} catch (NumberFormatException e) {
					System.err.println("Bad duration on line " + lineCount);
					duration = UNKNOWN;
				}
				s.skip(',');	// offset time (in beats, decimal)
				s.skip('[');	// additional info (e.g. melody/arpeggio/grace)
				do {
					element = s.getString();
					eventFlags |= flags.getFlag(element);
				} while (s.matchString(","));
				s.skip('-');
			} else if (s.matchString("trill(")) {
				eventFlags |= flags.getFlag("trill");
				s.skip('-');
			} else if (s.matchString("ornament(")) {
				eventFlags |= flags.getFlag("ornament");
				s.skip('-');
			} else if (s.matchString("trailing_played_note-") ||
					   s.matchString("hammer_bounce-") ||   
					   s.matchString("no_score_note-") ||
					   s.matchString("insertion-")) {
				eventFlags |= flags.getFlag("unscored");
			} else if (!s.matchString("%")) {		// Prolog comment
				throw new MatchFileParseException("error 4; line "+lineCount);
			}
			// READ 2nd term of Prolog expression
			if (s.matchString("note(")) {
				s.skip('[');	// skip identifier
				String note = s.getString();
				switch(Character.toUpperCase(note.charAt(0))) {
					case 'A': pitch =  9; break;
					case 'B': pitch = 11; break;
					case 'C': pitch =  0; break; 
					case 'D': pitch =  2; break;
					case 'E': pitch =  4; break;
					case 'F': pitch =  5; break;
					case 'G': pitch =  7; break;
					default:  throw new MatchFileParseException(
											"Bad note on line " + lineCount);
				}
				s.skip(',');
				String mod = s.getString();
				for (int i = 0; i < mod.length(); i++) {
					switch (mod.charAt(i)) {
						case '#': pitch++; break;
						case 'b': pitch--; break;
						case 'n': break;
						default: throw new MatchFileParseException("error 5 " +
																	lineCount);
					}
				}
				s.skip(',');
				octave = s.getInt();
				pitch += 12 * octave;
				s.skip(',');
				onset = s.getInt();
				s.skip(',');
				offset = s.getInt();
				if (versionNumber > 1.0) {
					s.skip(',');
					eOffset = s.getInt();
				} else
					eOffset = offset;
				s.skip(',');
				velocity = s.getInt();
				onset /= clockUnits * 1000000.0 / clockRate;
				offset /= clockUnits * 1000000.0 / clockRate;
				eOffset /= clockUnits * 1000000.0 / clockRate;
				if (timingCorrection) {
					if (startNote) {
						timingDisplacement = onset - timingDisplacement;
						startNote = false;
					}
					onset -= timingDisplacement;
					offset -= timingDisplacement;
					eOffset -= timingDisplacement;
				}
				int m = flags.getFlag("s");
				if ((((eventFlags & m) != 0) && !noMelody) ||
						(((eventFlags & m) == 0) && !onlyMelody)) {
					Event e = new Event(onset, offset, eOffset, pitch, velocity,
										beat, duration, eventFlags);
					list.add(e);
				}
			} else if (!s.matchString("no_played_note.") &&
					   !s.matchString("trailing_score_note.") &&
					   !s.matchString("deletion.") &&
					   !s.matchString("%"))
				throw new MatchFileParseException("error 6; line " + lineCount);
			s.set(inputFile.readLine());
			lineCount++;
		}
		return list;
	} // readMatchFile()

	public static void main(String[] args) throws Exception {	// quick test
		//System.out.println("Test");
		//readLabelFile(args[0]).writeLabelFile("tmp.txt");
		readLabelFile(args[0]).print();
		System.exit(0);
		EventList el = readMatchFile(args[0]);
		WormFile wf = new WormFile(null, el);
		if (args.length >= 2) {
			double sm = Double.parseDouble(args[1]);
			wf.smooth(Worm.FULL_GAUSS, sm, sm, 0);
		} else
			wf.smooth(Worm.NONE, 0, 0, 0);
		wf.write("worm.out");
		if (args.length == 3)
			el.print();
	} // main()

} // class EventList