diff at/ofai/music/util/EventList.java @ 2:4c3f5bc01c97

* Import BeatRoot v0.5.7
author Chris Cannam
date Fri, 08 Oct 2010 16:11:06 +0100
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/util/EventList.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,843 @@
+/*
+	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