changeset 2:4c3f5bc01c97

* Import BeatRoot v0.5.7
author Chris Cannam
date Fri, 08 Oct 2010 16:11:06 +0100
parents 4de8d7f01bd4
children 18ffb64cde0d
files at/ofai/music/audio/Convert.java at/ofai/music/audio/FFT.java at/ofai/music/audio/Util.java at/ofai/music/audio/WavWrite.java at/ofai/music/beatroot/GUI.java at/ofai/music/util/ArrayMap.java at/ofai/music/util/ArrayPrint.java at/ofai/music/util/Colors.java at/ofai/music/util/ConstantTempoMap.java at/ofai/music/util/Event.java at/ofai/music/util/EventList.java at/ofai/music/util/Format.java at/ofai/music/util/FrameMargins.java at/ofai/music/util/MIDI2Matlab.java at/ofai/music/util/MatchTempoMap.java at/ofai/music/util/Matcher.java at/ofai/music/util/PSPrinter.java at/ofai/music/util/Parameters.java at/ofai/music/util/Peaks.java at/ofai/music/util/Profile.java at/ofai/music/util/RandomAccessInputStream.java at/ofai/music/util/TempoMap.java at/ofai/music/util/WormEvent.java at/ofai/music/worm/AudioWorm.java at/ofai/music/worm/MyFileChooser.java at/ofai/music/worm/MyFileFilter.java at/ofai/music/worm/Plot.java at/ofai/music/worm/PlotListener.java at/ofai/music/worm/PlotPanel.java at/ofai/music/worm/TempoInducer.java at/ofai/music/worm/Worm.java at/ofai/music/worm/WormConstants.java at/ofai/music/worm/WormControlPanel.java at/ofai/music/worm/WormFile.java at/ofai/music/worm/WormIcon.java at/ofai/music/worm/WormLoadDialog.java at/ofai/music/worm/WormParameters.java at/ofai/music/worm/WormScrollBar.java at/ofai/music/worm/WormSmoothDialog.java
diffstat 39 files changed, 7412 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/audio/Convert.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,50 @@
+/*
+	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.audio;
+
+public class Convert {
+
+	public static void monoShortToInt(byte[] in, int[] out,boolean isBigEndian){
+		monoShortToInt(in, 0, in.length, out, 0, isBigEndian);
+	} // monoShortToInt()/3
+	
+	public static void monoShortToInt(byte[] in, int inIndex, int bytes,
+						int[] out, int outIndex, boolean isBigEndian){
+		if (isBigEndian)
+			for ( ; inIndex < bytes; inIndex += 2)
+				out[outIndex++] = (in[inIndex+1] & 0xff) | (in[inIndex] << 8);
+		else
+			for ( ; inIndex < bytes; inIndex += 2)
+				out[outIndex++] = (in[inIndex] & 0xff) | (in[inIndex+1] << 8);
+	} // monoShortToInt()
+
+	public static void monoShortToDouble(byte[] in, double[] out,
+											boolean isBigEndian) {
+		int j = 0;
+		if (isBigEndian)
+			for (int i = 0; i < in.length; i += 2)
+				out[j++] = ((in[i+1] & 0xff) | (in[i] << 8)) / 32768.0;
+		else
+			for (int i = 0; i < in.length; i += 2)
+				out[j++] = ((in[i] & 0xff) | (in[i+1] << 8)) / 32768.0;
+	} // monoShortToDouble()
+
+} // class Convert
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/audio/FFT.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,372 @@
+/*
+	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.audio;
+
+/** Class for computing a windowed fast Fourier transform.
+ *  Implements some of the window functions for the STFT from
+ *  Harris (1978), Proc. IEEE, 66, 1, 51-83.
+ */
+public class FFT {
+
+	/** used in {@link FFT#fft(double[], double[], int)} to specify
+	 *  a forward Fourier transform */
+	public static final int FORWARD = -1;
+	/** used in {@link FFT#fft(double[], double[], int)} to specify
+	 *  an inverse Fourier transform */
+	public static final int REVERSE = 1;
+	/** used in {@link FFT#makeWindow(int,int,int)} to specify a
+	 *  rectangular window function */
+	public static final int RECT = 0;
+	/** used in {@link FFT#makeWindow(int,int,int)} to specify a
+	 *  Hamming window function */
+	public static final int HAMMING = 1;
+	/** used in {@link FFT#makeWindow(int,int,int)} to specify a
+	 *  61-dB 3-sample Blackman-Harris window function */
+	public static final int BH3 = 2;
+	/** used in {@link FFT#makeWindow(int,int,int)} to specify a
+	 *  74-dB 4-sample Blackman-Harris window function */
+	public static final int BH4 = 3;
+	/** used in {@link FFT#makeWindow(int,int,int)} to specify a
+	 *  minimum 3-sample Blackman-Harris window function */
+	public static final int BH3MIN = 4;
+	/** used in {@link FFT#makeWindow(int,int,int)} to specify a
+	 *  minimum 4-sample Blackman-Harris window function */
+	public static final int BH4MIN = 5;
+	/** used in {@link FFT#makeWindow(int,int,int)} to specify a
+	 *  Gaussian window function */
+	public static final int GAUSS = 6; 
+	static final double twoPI = 2 * Math.PI;
+
+	/** The FFT method. Calculation is inline, for complex data stored
+	 *  in 2 separate arrays. Length of input data must be a power of two.
+	 *  @param re        the real part of the complex input and output data
+	 *  @param im        the imaginary part of the complex input and output data
+	 *  @param direction the direction of the Fourier transform (FORWARD or
+	 *  REVERSE)
+	 *  @throws IllegalArgumentException if the length of the input data is
+	 *  not a power of 2
+	 */
+	public static void fft(double re[], double im[], int direction) {
+		int n = re.length;
+		int bits = (int)Math.rint(Math.log(n) / Math.log(2));
+		if (n != (1 << bits))
+			throw new IllegalArgumentException("FFT data must be power of 2");
+		int localN;
+		int j = 0;
+		for (int i = 0; i < n-1; i++) {
+			if (i < j) {
+				double temp = re[j];
+				re[j] = re[i];
+				re[i] = temp;
+				temp = im[j];
+				im[j] = im[i];
+				im[i] = temp;
+			}
+			int k = n / 2;
+			while ((k >= 1) &&  (k - 1 < j)) {
+				j = j - k;
+				k = k / 2;
+			}
+			j = j + k;
+		}
+		for(int m = 1; m <= bits; m++) {
+			localN = 1 << m;
+			double Wjk_r = 1;
+			double Wjk_i = 0;
+			double theta = twoPI / localN;
+			double Wj_r = Math.cos(theta);
+			double Wj_i = direction * Math.sin(theta);
+			int nby2 = localN / 2;
+			for (j = 0; j < nby2; j++) {
+				for (int k = j; k < n; k += localN) {
+					int id = k + nby2;
+					double tempr = Wjk_r * re[id] - Wjk_i * im[id];
+					double tempi = Wjk_r * im[id] + Wjk_i * re[id];
+					re[id] = re[k] - tempr;
+					im[id] = im[k] - tempi;
+					re[k] += tempr;
+					im[k] += tempi;
+				}
+				double wtemp = Wjk_r;
+				Wjk_r = Wj_r * Wjk_r  - Wj_i * Wjk_i;
+				Wjk_i = Wj_r * Wjk_i  + Wj_i * wtemp;
+			}
+		}
+	} // fft()
+
+	/** Computes the power spectrum of a real sequence (in place).
+	 *  @param re the real input and output data; length must be a power of 2
+	 */
+	public static void powerFFT(double[] re) {
+		double[] im = new double[re.length];
+		fft(re, im, FORWARD);
+		for (int i = 0; i < re.length; i++)
+			re[i] = re[i] * re[i] + im[i] * im[i];
+	} // powerFFT()	
+
+	/** Converts a real power sequence from to magnitude representation,
+	 *  by computing the square root of each value.
+	 *  @param re the real input (power) and output (magnitude) data; length
+	 *  must be a power of 2
+	 */
+	public static void toMagnitude(double[] re) {
+		for (int i = 0; i < re.length; i++)
+			re[i] = Math.sqrt(re[i]);
+	} // toMagnitude()
+	
+	/** Computes the magnitude spectrum of a real sequence (in place).
+	 *  @param re the real input and output data; length must be a power of 2
+	 */
+	public static void magnitudeFFT(double[] re) {
+		powerFFT(re);
+		toMagnitude(re);
+	} // magnitudeFFT()
+
+	/** Computes a complex (or real if im[] == {0,...}) FFT and converts
+	 *  the results to polar coordinates (power and phase). Both arrays
+	 *  must be the same length, which is a power of 2.
+	 *  @param re the real part of the input data and the power of the output
+	 *  data
+	 *  @param im the imaginary part of the input data and the phase of the
+	 *  output data
+	 */
+	public static void powerPhaseFFT(double[] re, double[] im) {
+		fft(re, im, FORWARD);
+		for (int i = 0; i < re.length; i++) {
+			double pow = re[i] * re[i] + im[i] * im[i];
+			im[i] = Math.atan2(im[i], re[i]);
+			re[i] = pow;
+		}
+	} // powerPhaseFFT()
+	
+	/** Inline computation of the inverse FFT given spectral input data
+	 *  in polar coordinates (power and phase).
+	 *  Both arrays must be the same length, which is a power of 2.
+	 *  @param pow the power of the spectral input data (and real part of the
+	 *  output data)
+	 *  @param ph the phase of the spectral input data (and the imaginary part
+	 *  of the output data)
+	 */
+	public static void powerPhaseIFFT(double[] pow, double[] ph) {
+		toMagnitude(pow);
+		for (int i = 0; i < pow.length; i++) {
+			double re = pow[i] * Math.cos(ph[i]);
+			ph[i] = pow[i] * Math.sin(ph[i]);
+			pow[i] = re;
+		}
+		fft(pow, ph, REVERSE);
+	} // powerPhaseIFFT()
+	
+	/** Computes a complex (or real if im[] == {0,...}) FFT and converts
+	 *  the results to polar coordinates (magnitude and phase). Both arrays
+	 *  must be the same length, which is a power of 2.
+	 *  @param re the real part of the input data and the magnitude of the
+	 *  output data
+	 *  @param im the imaginary part of the input data and the phase of the
+	 *  output data
+	 */
+	public static void magnitudePhaseFFT(double[] re, double[] im) {
+		powerPhaseFFT(re, im);
+		toMagnitude(re);
+	} // magnitudePhaseFFT()
+
+
+	/** Fill an array with the values of a standard Hamming window function
+	 *  @param data the array to be filled
+	 *  @param size the number of non zero values; if the array is larger than
+	 *  this, it is zero-padded symmetrically at both ends 
+	 */
+	static void hamming(double[] data, int size) {
+		int start = (data.length - size) / 2;
+		int stop = (data.length + size) / 2;
+		double scale = 1.0 / (double)size / 0.54;
+		double factor = twoPI / (double)size;
+		for (int i = 0; start < stop; start++, i++)
+			data[i] = scale * (25.0/46.0 - 21.0/46.0 * Math.cos(factor * i));
+	} // hamming()
+
+	/** Fill an array with the values of a minimum 4-sample Blackman-Harris
+	 *  window function
+	 *  @param data the array to be filled
+	 *  @param size the number of non zero values; if the array is larger than
+	 *  this, it is zero-padded symmetrically at both ends 
+	 */
+	static void blackmanHarris4sMin(double[] data, int size) {
+		int start = (data.length - size) / 2;
+		int stop = (data.length + size) / 2;
+		double scale = 1.0 / (double)size / 0.36;
+		for (int i = 0; start < stop; start++, i++)
+			data[i] = scale * ( 0.35875 -
+								0.48829 * Math.cos(twoPI * i / size) +
+								0.14128 * Math.cos(2 * twoPI * i / size) -
+								0.01168 * Math.cos(3 * twoPI * i / size));
+	} // blackmanHarris4sMin()
+
+	/** Fill an array with the values of a 74-dB 4-sample Blackman-Harris
+	 *  window function
+	 *  @param data the array to be filled
+	 *  @param size the number of non zero values; if the array is larger than
+	 *  this, it is zero-padded symmetrically at both ends 
+	 */
+	static void blackmanHarris4s(double[] data, int size) {
+		int start = (data.length - size) / 2;
+		int stop = (data.length + size) / 2;
+		double scale = 1.0 / (double)size / 0.4;
+		for (int i = 0; start < stop; start++, i++)
+			data[i] = scale * ( 0.40217 -
+								0.49703 * Math.cos(twoPI * i / size) +
+								0.09392 * Math.cos(2 * twoPI * i / size) -
+								0.00183 * Math.cos(3 * twoPI * i / size));
+	} // blackmanHarris4s()
+
+	/** Fill an array with the values of a minimum 3-sample Blackman-Harris
+	 *  window function
+	 *  @param data the array to be filled
+	 *  @param size the number of non zero values; if the array is larger than
+	 *  this, it is zero-padded symmetrically at both ends 
+	 */
+	static void blackmanHarris3sMin(double[] data, int size) {
+		int start = (data.length - size) / 2;
+		int stop = (data.length + size) / 2;
+		double scale = 1.0 / (double) size / 0.42;
+		for (int i = 0; start < stop; start++, i++)
+			data[i] = scale * ( 0.42323 -
+								0.49755 * Math.cos(twoPI * i / size) +
+								0.07922 * Math.cos(2 * twoPI * i / size));
+	} // blackmanHarris3sMin()
+
+	/** Fill an array with the values of a 61-dB 3-sample Blackman-Harris
+	 *  window function
+	 *  @param data the array to be filled
+	 *  @param size the number of non zero values; if the array is larger than
+	 *  this, it is zero-padded symmetrically at both ends 
+	 */
+	static void blackmanHarris3s(double[] data, int size) {
+		int start = (data.length - size) / 2;
+		int stop = (data.length + size) / 2;
+		double scale = 1.0 / (double) size / 0.45;
+		for (int i = 0; start < stop; start++, i++)
+			data[i] = scale * ( 0.44959 -
+								0.49364 * Math.cos(twoPI * i / size) +
+								0.05677 * Math.cos(2 * twoPI * i / size));
+	} // blackmanHarris3s()
+
+	/** Fill an array with the values of a Gaussian window function
+	 *  @param data the array to be filled
+	 *  @param size the number of non zero values; if the array is larger than
+	 *  this, it is zero-padded symmetrically at both ends 
+	 */
+	static void gauss(double[] data, int size) { // ?? between 61/3 and 74/4 BHW
+		int start = (data.length - size) / 2;
+		int stop = (data.length + size) / 2;
+		double delta = 5.0 / size;
+		double x = (1 - size) / 2.0 * delta;
+		double c = -Math.PI * Math.exp(1.0) / 10.0;
+		double sum = 0;
+		for (int i = start; i < stop; i++) {
+			data[i] = Math.exp(c * x * x);
+			x += delta;
+			sum += data[i];
+		}
+		for (int i = start; i < stop; i++)
+			data[i] /= sum;
+	} // gauss()
+
+	/** Fill an array with the values of a rectangular window function
+	 *  @param data the array to be filled
+	 *  @param size the number of non zero values; if the array is larger than
+	 *  this, it is zero-padded symmetrically at both ends 
+	 */
+	static void rectangle(double[] data, int size) {
+		int start = (data.length - size) / 2;
+		int stop = (data.length + size) / 2;
+		for (int i = start; i < stop; i++)
+			data[i] = 1.0 / (double) size;
+	} // rectangle()
+
+	/** Returns an array of values of a normalised smooth window function,
+	 *  as used for performing a short time Fourier transform (STFT).
+	 *  All functions are normalised by length and coherent gain.
+	 *  More information on characteristics of these functions can be found
+	 *  in F.J. Harris (1978), On the Use of Windows for Harmonic Analysis
+	 *  with the Discrete Fourier Transform, <em>Proceedings of the IEEE</em>,
+	 *  66, 1, 51-83.
+	 *  @param choice  the choice of window function, one of the constants
+	 *  defined above
+	 *  @param size    the size of the returned array
+	 *  @param support the number of non-zero values in the array
+	 *  @return the array containing the values of the window function
+	 */
+	public static double[] makeWindow(int choice, int size, int support) {
+		double[] data = new double[size];
+		if (support > size)
+			support = size;
+		switch (choice) {
+			case RECT:		rectangle(data, support);			break;
+			case HAMMING:	hamming(data, support);				break;
+			case BH3:		blackmanHarris3s(data, support);	break;
+			case BH4:		blackmanHarris4s(data, support);	break;
+			case BH3MIN:	blackmanHarris3sMin(data, support);	break;
+			case BH4MIN:	blackmanHarris4sMin(data, support);	break;
+			case GAUSS:		gauss(data, support);				break;
+			default:		rectangle(data, support);			break;
+		}
+		return data;
+	} // makeWindow()
+
+	/** Applies a window function to an array of data, storing the result in
+	 *  the data array.
+	 *  Performs a dot product of the data and window arrays. 
+	 *  @param data   the array of input data, also used for output
+	 *  @param window the values of the window function to be applied to data
+	 */
+	public static void applyWindow(double[] data, double[] window) {
+		for (int i = 0; i < data.length; i++)
+			data[i] *= window[i];
+	} // applyWindow()
+
+	/** Unit test of the FFT class.
+	 *  Performs a forward and inverse FFT on a 1MB array of random values
+	 *  and checks how closely the values are preserved.
+	 *  @param args ignored
+	 */
+	public static void main(String[] args) {
+		final int SZ = 1024 * 1024;
+		double[] r1 = new double[SZ];
+		double[] i1 = new double[SZ];
+		double[] r2 = new double[SZ];
+		double[] i2 = new double[SZ];
+		for (int j = 0; j < SZ; j++) {
+			r1[j] = r2[j] = Math.random();
+			i1[j] = i2[j] = Math.random();
+		}
+		System.out.println("start");
+		fft(r2, i2, FORWARD);
+		System.out.println("reverse");
+		fft(r2, i2, REVERSE);
+		System.out.println("result");
+		double err = 0;
+		for (int j = 0; j < SZ; j++)
+			err += Math.abs(r1[j] - r2[j] / SZ) + Math.abs(i1[j] - i2[j] / SZ);
+		System.out.printf( "Err: %12.10f   Av: %12.10f\n", err, err / SZ);
+	} // main()
+
+} // class FFT
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/audio/Util.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,56 @@
+/*
+	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.audio;
+
+public class Util {
+
+	public static double rms(double[] d) {
+		double sum = 0;
+		for (int i=0; i < d.length; i++)
+			sum += d[i] * d[i];
+		return Math.sqrt(sum / d.length);
+	}
+
+	public static double min(double[] d) {
+		double min = d[0];
+		for (int i=1; i < d.length; i++)
+			if (d[i] < min)
+				min = d[i];
+		return min;
+	}
+
+	public static double max(double[] d) {
+		double max = d[0];
+		for (int i=1; i < d.length; i++)
+			if (d[i] > max)
+				max = d[i];
+		return max;
+	}
+
+	public static double threshold(double value, double min, double max) {
+		if (value < min)
+			return min;
+		if (value > max)
+			return max;
+		return value;
+	}
+
+} // class Util
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/audio/WavWrite.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,79 @@
+/*
+	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.audio;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+public class WavWrite {
+
+	public static void toByte(byte[] out, String data, int offset) {
+		try {
+			byte[] b = data.getBytes("US-ASCII");
+			for (int i = 0; i < b.length; i++)
+				out[offset++] = b[i];
+		} catch (UnsupportedEncodingException e) {
+			System.err.println(e);
+		}
+	} // toByte()
+
+	public static void toByte(byte[] out, long data,int offset,int len){
+		for (int stop = offset + len; offset < stop; offset++) {
+			out[offset] = (byte)data;
+			data >>= 8;
+		}
+	} // toByte()
+
+	/** Opens a file output stream and writes a WAV file header to it */
+	public static FileOutputStream open(String fileName, int byteLength,
+				int channels, int rate, int audioSize) {
+		FileOutputStream out;
+		try {
+			out = new FileOutputStream(new File(fileName));
+			byte[] wavHeader = new byte[44];
+			toByte(wavHeader, "RIFF", 0);
+			toByte(wavHeader, byteLength+36, 4, 4);
+			toByte(wavHeader, "WAVEfmt ", 8);
+			toByte(wavHeader, 16, 16, 4);				// chunk length
+			toByte(wavHeader, 1, 20, 2);				// PCM encoding
+			toByte(wavHeader, channels, 22, 2);			// channels
+			toByte(wavHeader, rate, 24, 4);				// sampling rate
+			toByte(wavHeader, audioSize * channels * rate, 28, 4);// bytes per s
+			toByte(wavHeader, audioSize * channels, 32, 2);	// block alignment
+			toByte(wavHeader, 8 * audioSize, 34, 2);	// bits per sample
+			toByte(wavHeader, "data", 36);
+			toByte(wavHeader, byteLength, 40, 4);
+			out.write(wavHeader);
+		} catch (FileNotFoundException e) {
+			System.err.println("WavWrite: Error opening output file: "+
+								fileName + "\n" + e);
+			return null;
+		} catch (IOException e) {
+			System.err.println("Error writing output file header\n"+e);
+			return null;
+		}
+		return out;
+	} // open()
+
+} // class WavWrite()
--- a/at/ofai/music/beatroot/GUI.java	Fri Oct 08 16:09:10 2010 +0100
+++ b/at/ofai/music/beatroot/GUI.java	Fri Oct 08 16:11:06 2010 +0100
@@ -95,7 +95,7 @@
 	
 	/** Version number of program - displayed as part of window title.
 	 *  DO NOT EDIT: This line is also used in creating the file name of the jar file. */
-	public static final String version = "0.5.6";
+	public static final String version = "0.5.7";
 	
 	/** Strings displayed on menus and buttons */
 	public static final String LOAD_AUDIO = "Load Audio Data";
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/util/ArrayMap.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,178 @@
+/*
+	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.util.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+// An implementation of the Map interface, backed by an ArrayList, which
+//  preserves the elements in the order that they are added to the map.
+//  Operations will take linear rather than constant time (as for the efficient
+//  implementations of Map). Operations are not synchronized; caveat programmer!
+// Used by class Parameters
+// Updated to use generics; demonstrates that generics do not necessarily make
+//  programs more readable, simple, safe, etc.
+class ArrayMap implements Map<String,Object> {
+
+	protected ArrayList<Entry> entries;
+
+	protected class Entry implements Map.Entry<String,Object>,
+									 Comparable<Object> {
+		protected String key;
+		protected Object value;
+		protected Entry(String k, Object v) {
+			key = k;
+			value = v;
+		} // constructor
+		public boolean equals(Object o) {
+			return (o instanceof Entry) && key.equals(((Entry)o).key) &&
+											value.equals(((Entry)o).value);
+		} // equals()
+		public String getKey() { return key; }
+		public Object getValue() { return value; }
+		public Object setValue(Object newValue) {
+			Object oldValue = value;
+			value = newValue;
+			return oldValue;
+		} // setValue()
+		public int hashCode() {
+			return (key==null? 0 : key.hashCode()) ^
+				   (value==null? 0 : value.hashCode());
+		} // hashCode()
+		public int compareTo(Object o) {
+			return key.compareTo(((Entry)o).key);
+		} // compareTo()
+	} // inner class Entry
+
+	public ArrayMap() { entries = new ArrayList<Entry>(); }//default constructor
+	public ArrayMap(Map<String,Object> m) { this(); putAll(m); }		// copy constructor
+
+	// Returns the index of an entry, given its key, or -1 if it is not in map.
+	//  Note that ArrayList.indexOf() can't be used, because it doesn't call
+	//  ArrayMap$Entry.equals()  [bug?? or does it call key.equals(entry)??]
+	public int indexOf(String key) {
+		for (int i = 0; i < size(); i++)
+			if (key.equals(entries.get(i).key))
+				return i;
+		return -1;
+	} // indexOf()
+
+	// Returns the map entry at the given index
+	public Entry getEntry(int i) { return entries.get(i); }
+
+	// Removes all mappings from this map (optional operation).
+	public void clear() { entries.clear(); }
+
+	// Returns true if this map contains a mapping for the specified key.
+    public boolean containsKey(Object key) {
+		return indexOf((String)key) >= 0;
+	} // containsKey()
+
+	// Returns true if this map maps one or more keys to the specified value.
+	public boolean containsValue(Object value) {
+		for (int i = 0; i < size(); i++)
+			if (value.equals(entries.get(i).value))
+				return true;
+		return false;
+	} // containsValue()
+
+	// Returns a set view of the mappings contained in this map.
+	public Set<Map.Entry<String,Object>> entrySet() {
+		TreeSet<Map.Entry<String,Object>> s =
+					new TreeSet<Map.Entry<String,Object>>();
+		for (int i = 0; i < size(); i++)
+			s.add(entries.get(i));
+		return s;
+	} // entrySet()
+
+	// Compares the specified object with this map for equality.
+	public boolean equals(Object o) { return (o == this); }
+	
+	// Returns the value to which this map maps the specified key.
+	public Object get(Object key) {
+		int i = indexOf((String)key);
+		if (i == -1)
+			return null;
+		return entries.get(i).value;
+	} // get()
+
+	// Returns the hash code value for this map.
+	public int hashCode() {
+		int h = 0;
+		for (int i = 0; i < size(); i++)
+			 h ^= entries.get(i).hashCode();
+		return h;
+	} // hashCode()
+	
+	// Returns true if this map contains no key-value mappings.
+	public boolean isEmpty() { return entries.isEmpty(); }
+	
+	// Returns a set view of the keys contained in this map.
+	public Set<String> keySet() {
+		TreeSet<String> s = new TreeSet<String>();
+		for (int i = 0; i < size(); i++)
+			s.add(entries.get(i).key);
+		return s;
+	} // keySet()
+
+	// Associates the specified value with the specified key in this map
+	public Object put(String key, Object value) {
+		int i = indexOf(key);
+		if (i < 0) {
+			entries.add(new Entry(key, value));
+			return null;
+		} else
+			return entries.get(i).setValue(value);
+	} // put()
+
+	// Copies all of the mappings from the specified map to this map
+	public void putAll(Map m) {
+		// The following warning seems to be unavoidable:
+		// warning: [unchecked] unchecked conversion
+		Map<String,Object> m1 = (Map<String,Object>)m;
+		for (Map.Entry<String,Object> me : m1.entrySet()) {
+			put(me.getKey(), me.getValue());
+		}
+	} // putAll()
+
+	// Removes the mapping for this key from this map if present
+	public Object remove(Object key) {
+		int i = indexOf((String)key);
+		if (i < 0)
+			return null;
+		return entries.remove(i);
+	} // remove()
+
+	// Returns the number of key-value mappings in this map.
+	public int size() { return entries.size(); }
+
+	// Returns a collection view of the values contained in this map.
+	public Collection<Object> values() {
+		ArrayList<Object> s = new ArrayList<Object>();
+		for (int i = 0; i < size(); i++)
+			s.add(entries.get(i).value);
+		return s;
+	} // values()
+
+} // class ArrayMap
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/util/ArrayPrint.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,45 @@
+/*
+	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;
+
+public class ArrayPrint {
+
+	public static void show(String s, double[] arr, int line) {
+		System.out.println(s + " (length = " + arr.length + ")");
+		for (int i = 0; i < arr.length; i++) {
+			System.out.printf("%7.3f ", arr[i]);
+			if (i % line == line - 1)
+				System.out.println();
+		}
+		System.out.println();
+	} // show()
+		
+	void show(String s, int[] arr, int line) {
+		System.out.println(s + " (length = " + arr.length + ")");
+		for (int i = 0; i < arr.length; i++) {
+			System.out.printf("%7d ", arr[i]);
+			if (i % line == line - 1)
+				System.out.println();
+		}
+		System.out.println();
+	} // show()
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/util/Colors.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,30 @@
+/*
+	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.awt.Color;
+
+public interface Colors {
+	Color getBackground();
+	Color getForeground();
+	Color getButton();
+	Color getButtonText();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/util/ConstantTempoMap.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,56 @@
+/*
+	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;
+
+public class ConstantTempoMap implements TempoMap {
+	
+	protected double interBeatInterval;
+
+	public ConstantTempoMap(double bpm) {
+		interBeatInterval = 60 / bpm;
+	} // constructor
+
+	public ConstantTempoMap() {
+		this(120);
+	} // default constructor
+
+	public void add(double time, double tempo) {
+		throw new RuntimeException("ConstantTempoMap: cannot change tempo");
+	} // add()
+		
+	public double toRealTime(double value) {
+		return value * interBeatInterval;
+	} // toRealTime()
+
+	public double toScoreTime(double value) {
+		return value / interBeatInterval;
+	} // toScoreTime()
+
+	public static void main(String[] args) { // unit test
+		TempoMap mtm = new ConstantTempoMap(100);
+		System.out.println(mtm.toRealTime(1));
+		System.out.println(mtm.toScoreTime(mtm.toRealTime(1)));
+		System.out.println(mtm.toScoreTime(4));
+		System.out.println(mtm.toRealTime(mtm.toScoreTime(4)));
+		mtm.add(5, 120);
+	} // main()
+
+} // ConstantTempoMap
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/util/Event.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,98 @@
+/*
+	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;
+
+public class Event implements Comparable, Cloneable, java.io.Serializable {
+
+	public double keyDown, keyUp, pedalUp, scoreBeat, scoreDuration, salience;
+	public int midiPitch, midiVelocity, flags, midiCommand, midiChannel,
+				midiTrack;
+	//public String label;
+
+	public Event(double onset, double offset, double eOffset, int pitch,
+				 int velocity, double beat, double duration, int eventFlags,
+				 int command, int channel, int track) {
+		this(onset, offset, eOffset, pitch, velocity, beat,duration,eventFlags);
+		midiCommand = command;
+		midiChannel = channel;
+		midiTrack = track;
+	} // constructor
+
+	public Event(double onset, double offset, double eOffset, int pitch,
+				 int velocity, double beat, double duration, int eventFlags) {
+		keyDown = onset;
+		keyUp = offset;
+		pedalUp = eOffset;
+		midiPitch = pitch;
+		midiVelocity = velocity;
+		scoreBeat = beat;
+		scoreDuration = duration;
+		flags = eventFlags;
+		midiCommand = javax.sound.midi.ShortMessage.NOTE_ON;
+		midiChannel = 1;
+		midiTrack = 0;
+		salience = 0;
+	} // constructor
+
+	public Event clone() {
+		return new Event(keyDown, keyUp, pedalUp, midiPitch, midiVelocity,
+					scoreBeat, scoreDuration, flags, midiCommand, midiChannel,
+					midiTrack);
+	} // clone()
+
+	// Interface Comparable
+	public int compareTo(Object o) {
+		Event e = (Event) o;
+		return (int)Math.signum(keyDown - e.keyDown);
+	} // compareTo()
+
+	public String toString() {
+		return "n=" + midiPitch + " v=" + midiVelocity + " t=" + keyDown +
+				" to " + keyUp + " (" + pedalUp + ")";
+	} // toString()
+
+	public void print(Flags f) {
+		System.out.printf("Event:\n");
+		System.out.printf("\tkeyDown / Up / pedalUp: %5.3f / %5.3f /  %5.3f\n",
+			keyDown, keyUp, pedalUp);
+		//System.out.printf("\tkeyUp: %5.3f\n", keyUp);
+		//System.out.printf("\tpedalUp: %5.3f\n", pedalUp);
+		System.out.printf("\tmidiPitch: %d\n", midiPitch);
+		System.out.printf("\tmidiVelocity: %d\n", midiVelocity);
+		System.out.printf("\tmidiCommand: %02x\t", midiCommand | midiChannel);
+		//System.out.printf("\tmidiChannel: %d\n", midiChannel);
+		System.out.printf("\tmidiTrack: %d\n", midiTrack);
+		System.out.printf("\tsalience: %5.3f\t", salience);
+		System.out.printf("\tscoreBeat: %5.3f\t", scoreBeat);
+		System.out.printf("\tscoreDuration: %5.3f\n", scoreDuration);
+		System.out.printf("\tflags: %X", flags);
+		if (f != null) {
+			int ff = flags;
+			for (int i=0; ff != 0; i++) {
+				if (ff % 2 == 1)
+					System.out.print(" " + f.getLabel(i));
+				ff >>>= 1;
+			}
+		}
+		System.out.print("\n\n");
+	} // print()
+
+} // class Event
--- /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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/util/Format.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,201 @@
+/*
+	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.text.NumberFormat;
+import java.io.*;
+
+/** A simple utility class for easier formatted output of numeric data.
+ *  Formatting is controlled by static objects, so that number widths and
+ *  precisions can be set once, and used multiple times.
+ */
+public class Format {
+
+	/** The object which performs formatting of integers */
+	protected static NumberFormat intFormat = NumberFormat.getInstance();
+
+	/** The object which performs formatting of doubles */
+	protected static NumberFormat doubleFormat = NumberFormat.getInstance();
+	
+	/** The preferred notation for positive numbers (default is no '+' sign) */
+	protected static char plusSign = ' ';
+
+	/** Set the number of digits to appear after the decimal point
+	 *  @param dp the number of characters after the decimal point
+	 */
+	public static void setPostDigits(int dp) {
+		doubleFormat.setMinimumFractionDigits(dp);
+		doubleFormat.setMaximumFractionDigits(dp);
+	} // setPostDigits()
+	
+	/** Set the number of digits to appear before the decimal point.
+	 *  If the number does not require this many digits, it will be
+	 *  padded with spaces.
+	 *  @param dp the number of characters before the decimal point
+	 */
+	public static void setPreDigits(int dp) {
+		doubleFormat.setMinimumIntegerDigits(dp);
+	} // setPreDigits()
+	
+	/** Set the number of digits for displaying integers.
+	 *  If the number does not require this many digits, it will be
+	 *  padded with spaces.
+	 *  @param dp the number of digits for displaying integers
+	 */
+	public static void setIntDigits(int dp) {
+		intFormat.setMinimumIntegerDigits(dp);
+		intFormat.setMinimumFractionDigits(0);
+		intFormat.setMaximumFractionDigits(0);
+	} // setIntegerDigits()
+
+	/** Set whether digits should be grouped in 3's as in 12,000,000.
+	 *  @param flag true if grouping should be used, false if not
+	 */	
+	public static void setGroupingUsed(boolean flag) {
+		doubleFormat.setGroupingUsed(flag);
+		intFormat.setGroupingUsed(flag);
+	} // setGroupingUsed()
+
+	/** Sets the initial character for positive numbers (usually blank or '+')
+	 *  @param c the character to prefix to positive numbers
+	 */
+	public static void setPlusSign(char c) {
+		plusSign = c;
+	} // setPlusSign()
+
+	/** Initialise the formatting objects with the desired settings.
+	 *  @param id       the number of characters for displaying integers
+	 *  @param did      the number of characters before the decimal point
+	 *  @param dfd      the number of characters after the decimal point
+	 *  @param grouping true if grouping should be used, false if not
+	 */
+	public static void init(int id, int did, int dfd, boolean grouping) {
+		setIntDigits(id);
+		setPreDigits(did);
+		setPostDigits(dfd);
+		setGroupingUsed(grouping);
+	} // init()
+
+	/** Convert a double to a String with a set number of decimal places and
+	 *  padding to the desired minimum number of characters before the decimal
+	 *  point.
+	 *  @param n the number to convert
+	 *  @param id the number of characters before the decimal point
+	 *  @param fd the number of characters after the decimal point
+	 */
+	public static String d(double n, int id, int fd) {
+		setPreDigits(id);
+		return d(n, fd);
+	} // d()
+
+	/** Convert a double to a String with a set number of decimal places
+	 *  @param n the number to convert
+	 *  @param fd the number of characters after the decimal point
+	 */
+	public static String d(double n, int fd) {
+		setPostDigits(fd);
+		return d(n);
+	} // d()
+
+	/** Convert a double to a String. The number of decimal places and
+	 *  characters before the decimal point are stored in the static members
+	 *  of this class.
+	 *  @param n the number to convert
+	 */
+	public static String d(double n) {
+		String s;
+		if (Double.isNaN(n))
+			return "NaN";
+		s = doubleFormat.format(n);
+		if (n >= 0)
+			s = plusSign + s;
+		char[] c = s.toCharArray();
+		int i;
+		for (i = 1; (i < c.length-1) && (c[i] == '0') && (c[i+1] != '.'); i++) {
+			c[i] = c[i-1];
+			c[i-1] = ' ';
+		}
+		if (i > 1)
+			s = new String(c);
+		return s;
+	} // d()
+
+	/** Convert an integer to a String with padding to the desired minimum width
+	 *  @param n the number to convert
+	 *  @param id the desired minimum width
+	 */
+	public static String i(int n, int id) {
+		setIntDigits(id);
+		return i(n);
+	} // i()
+
+	/** Convert an integer to a String with padding to the desired minimum width
+	 *  @param n the number to convert
+	 */
+	public static String i(int n) {
+		return (n < 0)? intFormat.format(n): plusSign+intFormat.format(n);
+	} // i()
+
+	/** Output an array to file as an assignment statement for input to Matlab
+	 *  @param data the values to print to 4 decimal places
+	 *  @param name the variable name in the Matlab assignment statement; the
+	 *  file name is the same name with ".m" appended, or standard out if
+	 *  unable to open this file
+	 */
+	public static void matlab(double[] data, String name) {
+		matlab(data, name, 4);
+	} // matlab()
+	
+	/** Output an array to file as an assignment statement for input to Matlab
+	 *  @param data the values to print
+	 *  @param name the variable name in the Matlab assignment statement; the
+	 *  file name is the same name with ".m" appended, or standard out if
+	 *  unable to open this file
+	 *  @param dp the number of decimal places to print
+	 */
+	public static void matlab(double[] data, String name, int dp) {
+		setPostDigits(dp);
+		PrintStream out;
+		try {
+			out = new PrintStream(new FileOutputStream(name+".m"));
+		} catch (FileNotFoundException e) {
+			out = System.out;
+		}
+		matlab(data, name, out);
+		if (out != System.out)
+			out.close();
+	} // matlab
+
+	/** Output an array to a printstream as an assignment statement for input to
+	 *  Matlab
+	 *  @param data the values to print
+	 *  @param name the variable name in the Matlab assignment statement
+	 *  @param out  the output stream to print to
+	 */
+	public static void matlab(double[] data, String name, PrintStream out) {
+		out.println(name + " = [");
+		setGroupingUsed(false);
+		for (int i=0; i < data.length; i++)
+			out.println(d(data[i]));
+		out.println("];");
+	} // matlab()
+
+} // class Format
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/util/FrameMargins.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,97 @@
+/*
+	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.awt.*;
+import javax.swing.*;
+
+/** Java has problems communicating with window managers like fvwm2.
+ *  This class is a workaround to find the size of borders of JFrames in
+ *  the current environment, so that programs can size their JFrames correctly.
+ *  Since we don't know how big borders are until after the window is created,
+ *  we create a dummy JFrame, query the size of its borders, and destroy it.
+ *  The values are saved for later calls to this class, so that the dummy JFrame
+ *  is only created once.
+ */
+public class FrameMargins {
+
+	protected static Insets i = null;
+	protected static Dimension insetsWithMenu = null;
+	protected static Dimension insetsWithoutMenu = null;
+	protected static Dimension topLeftWithMenu = null;
+	protected static Dimension topLeftWithoutMenu = null;
+
+	/** Returns the total size of the insets of a JFrame, that is the size of
+	 *  the title bar, menu bar (if requested) and the borders of the JFrame.
+	 *  In other words, the return value is the difference in size between the
+	 *  JFrame itself and its content pane.
+	 *  @param withMenuFlag indicates whether a menu bar should be included in
+	 *  the calculations
+	 *  @return the height and width of the insets of a JFrame, unless
+	 *  <code>getInsets()</code> returns a ridiculously large value, in which
+	 *  case we gracefully return a guess of (30,20).
+	 */
+	public static Dimension get(boolean withMenuFlag) {
+		if (i == null) {
+			JFrame f = new JFrame("Get size of window borders");
+			JMenuBar mb = new JMenuBar();
+			f.setJMenuBar(mb);
+			mb.add(new JMenu("OK"));
+			f.setVisible(true);
+			i = f.getInsets();
+			f.dispose();
+			if ((i.left>100) || (i.right>100) || (i.top>100) || (i.bottom>100)){
+				i.left = 10;	// Code around a bug in getInsets()
+				i.right = 10;	//  - don't believe ridiculously high values
+				i.top = 20;
+				i.bottom = 10;
+			}
+			insetsWithMenu = new Dimension(i.left + i.right,
+										i.top + i.bottom + mb.getHeight());
+			insetsWithoutMenu = new Dimension(i.left + i.right, i.top+i.bottom);
+			topLeftWithoutMenu = new Dimension(i.left, i.top);
+			topLeftWithMenu = new Dimension(i.left, i.top + mb.getHeight());
+		}
+		return withMenuFlag? insetsWithMenu: insetsWithoutMenu;
+	} // get()
+
+	/** Returns the location of the content pane with respect to its JFrame.
+	 *  @param withMenuFlag indicates whether a menu bar should be included in
+	 *  the calculations
+	 *  @return the x and y offsets of the top left corner of the content pane
+	 *  from the top left corner of the JFrame
+	 */
+	public static Dimension getOrigin(boolean withMenuFlag) {
+		if (i == null)
+			get(withMenuFlag);
+		return withMenuFlag? topLeftWithMenu: topLeftWithoutMenu;
+	} // getOrigin()
+
+	/** Returns the Insets object for a JFrame.
+	 *  @return the Insets object measuring the size of the borders of a JFrame
+	 */
+	public static Insets getFrameInsets() {
+		if (i == null)
+			get(false);
+		return i;
+	} // getFrameInsets()
+
+} // class FrameMargins
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/util/MIDI2Matlab.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,52 @@
+/*
+	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.util.ListIterator;
+
+class MIDI2Matlab {
+
+	public static void main(String[] args) {
+		EventList e = EventList.readMidiFile(args[0]);
+		Event[] events = e.toArray(0x90);
+		int len = events.length;
+		double[] pitch = new double[len];
+		double[] vel = new double[len];
+		double[] onset = new double[len];
+		double[] offset = new double[len];
+		for (int i=0; i<len; i++) {
+			pitch[i] = events[i].midiPitch;
+			vel[i] = events[i].midiVelocity;
+			onset[i] = events[i].keyDown;
+			offset[i] = events[i].keyUp;
+		}
+		Format.init(1,5,3,false);
+		System.out.println("notes = zeros(" + len + ",4);");
+		Format.matlab(onset, "notes(:,1)", System.out);
+		Format.matlab(offset, "notes(:,2)", System.out);
+		Format.matlab(pitch, "notes(:,3)", System.out);
+		Format.matlab(vel, "notes(:,4)", System.out);
+		//	if (event.midiCommand == 0x90)
+		//		count++;
+		//	else
+		//		System.out.println("*** Other command: " + event.midiCommand);
+	} // main()
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/util/MatchTempoMap.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,175 @@
+/*
+	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 at.ofai.music.util.Format;
+
+// Stores a list of corresponding performance (real) times and
+//   notated (score or MIDI) times. Automatically resizes when necessary.
+//   Very inefficient if the list is edited after being used.
+//   Assumes a monotonic mapping (t1 > t2  <=>  s1 > s2)
+public class MatchTempoMap implements TempoMap {
+
+	protected double[] realTime;		// in seconds
+	protected double[] scoreTime;		// in beats or MIDI units
+	protected int[] repeats;			// for calculating the average if needed
+	protected int size;					// number of entries being used
+
+	public MatchTempoMap() {
+		this(5000);		// Mozart files are up to 3251 notes
+	} // default constructor
+
+	public MatchTempoMap(int sz) {
+		realTime = new double[sz];
+		scoreTime = new double[sz];
+		repeats = new int[sz];
+		size = 0;
+	} // constructor
+
+	protected void makeSpace() {
+		if (size == realTime.length)
+			resize(new MatchTempoMap(2 * size));
+	} // makeSpace()
+
+	protected void closeList() {
+		if (size != realTime.length)
+			resize(new MatchTempoMap(size));
+	} // closeList()
+
+	protected void resize(MatchTempoMap newList) {
+		for (int i = 0; i < size; i++) {
+			newList.realTime[i] = realTime[i];
+			newList.scoreTime[i] = scoreTime[i];
+			newList.repeats[i] = repeats[i];
+		}
+		realTime = newList.realTime;
+		scoreTime = newList.scoreTime;
+		repeats = newList.repeats;
+	} // resize()
+
+	public double toRealTime(double sTime) {
+		closeList();
+		return lookup(sTime, scoreTime, realTime);
+	} // toRealTime()
+	
+	public double toScoreTime(double rTime) {
+		closeList();
+		return lookup(rTime, realTime, scoreTime);
+	} // toScoreTime()
+	
+	public double lookup(double value, double[] domain, double[] range) {
+		int index = java.util.Arrays.binarySearch(domain, value);
+		if (index >= 0)
+			return range[index];
+		if ((size == 0) || ((size == 1) &&
+				((range[0] == 0) || (domain[0] == 0))))
+			throw new RuntimeException("Insufficient entries in tempo map");
+		if (size == 1)
+			return value * range[0] / domain[0];
+		index = -1 - index;		// do linear interpolation
+		if (index == 0) 		// unless at ends, where it is extrapolation
+			index++;
+		else if (index == size)
+			index--;
+		return (range[index] * (value - domain[index - 1]) +
+				range[index - 1] * (domain[index] - value)) /
+				(domain[index] - domain[index - 1]);
+	} // lookup()
+	
+	public void add(double rTime, double sTime) {
+		if (Double.isNaN(sTime))
+			return;
+		makeSpace();
+		int index;
+		for (index = 0; index < size; index++)
+			if (sTime <= scoreTime[index])
+				break;
+		if ((index == size) || (sTime != scoreTime[index])) {
+			for (int j = size; j > index; j--) {
+				scoreTime[j] = scoreTime[j-1];
+				realTime[j] = realTime[j-1];
+				repeats[j] = repeats[j-1];
+			}
+			size++;
+			scoreTime[index] = sTime;
+			realTime[index] = rTime;
+			repeats[index] = 1;
+		} else {	// average time of multiple nominally simultaneous notes
+			realTime[index] = (repeats[index] * realTime[index] + rTime) /
+								(repeats[index] + 1);
+			repeats[index]++;
+		}
+	} // add()
+
+	public void dump(double[] tempo, double step) {
+		if (size < 2) {
+			System.err.println("dump() failed: Empty tempo map");
+			return;
+		}
+		double[] tmp = new double[tempo.length];
+		int i = 0;
+		for (int j = 1; j < size; j++)
+			for ( ; i * step < realTime[j]; i++)
+				tmp[i] = (realTime[j] - realTime[j - 1]) /
+							(scoreTime[j] - scoreTime[j - 1]);
+		for ( ; i < tmp.length; i++)
+			tmp[i] = (realTime[size - 1] - realTime[size - 2]) /
+							(scoreTime[size - 1] - scoreTime[size - 2]);
+		int window = (int)(0.1 / step);     // smooth over 2.0 second window
+		double sum = 0;
+		for (i = 0; i < tmp.length; i++) {
+			sum += tmp[i];
+			if (i >= window) {
+				sum -= tmp[i - window];
+				tempo[i] = sum / window;
+			} else
+				tempo[i] = sum / (i + 1);
+			// System.out.println(i + " " + Format.d(tmp[i],3) +
+			// 					   " " + Format.d(tempo[i], 3));
+			if (tempo[i] != 0)
+				tempo[i] = 60.0 / tempo[i];
+		}
+	} // dump
+
+	public void print() {
+		System.out.println("Score  |  Perf.\n-------+-------");
+		for (int i = 0; i < size; i++)
+			System.out.println(Format.d(scoreTime[i], 3) + "  |  " +
+								Format.d(realTime[i], 3));
+	} // print()
+
+	public static void main(String[] args) { // unit test
+		TempoMap mtm = new MatchTempoMap();
+		mtm.add(0.6, 1);
+		mtm.add(0.8, 2);
+		mtm.add(0.95, 2.5);
+		mtm.add(1.0, 3);
+		double[] st = {0, 1, 2, 3, 4};
+		for (int i = 0 ; i < st.length; i++)
+			System.out.println(st[i] + " -> " + mtm.toRealTime(st[i]) +
+							   " -> " + mtm.toScoreTime(mtm.toRealTime(st[i])));
+		double[] rt = {0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1};
+		for (int i = 0 ; i < rt.length; i++)
+			System.out.println(rt[i] + " => " + mtm.toScoreTime(rt[i]) +
+							   " => " + mtm.toRealTime(mtm.toScoreTime(rt[i])));
+	} // main()
+
+} // class MatchTempoMap
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/util/Matcher.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,242 @@
+/*
+	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;
+
+/**	A simple parser for Prolog-type notation, but only handling the subset of
+ *	Prolog used in "match" files.
+ */
+public class Matcher {
+
+	/** The unparsed part of the current line of text */
+	protected String s;
+
+	/** The constructor is initialised with the input line of text for parsing*/
+	public Matcher(String data) { s = data; }
+
+	/** Reinitialise the parser with a new line of input */
+	public void set(String data) { s = data; }
+
+	/** Return the unparsed part of the input line */
+	public String get() { return s; }
+
+	/** Returns true if there is input data remaining */
+	public boolean hasData() {
+		return (s != null) && (s.length() > 0);
+	} // hasData()
+
+	/** Matches a String with the unparsed input data.
+	 *	If the complete String occurs at the beginning of the unparsed data,
+	 *	the unparsed data is advanced to the end of the String; otherwise
+	 *	the data is left unchanged.
+	 *
+	 *	@param m	the String to match
+	 *	@return		true if m matches the beginning of the unparsed data
+	 */
+	public boolean matchString(String m) {
+		if (s.startsWith(m)) {
+			s = s.substring(m.length());
+			return true;
+		}
+		return false;
+	} // matchString()
+
+	/**	Skips input up to and including the next instance of a given character.
+	 *	It is an error for the character not to occur in the data.
+	 *	@param c	the character to skip to
+	 */
+	public void skip(char c) {
+		int index = s.indexOf(c);
+		if (index >= 0)
+			s = s.substring(index + 1);
+		else
+			throw new RuntimeException("Parse error in skip(), expecting " + c);
+	} // skip()
+
+	/**	Removes whitespace from the beginning and end of the line.
+	 */
+	public void trimSpace() {
+		s = s.trim();
+	} // trimSpace()
+
+	/**	Returns and consumes the next character of unparsed data. */
+	public char getChar() {
+		char c = s.charAt(0);
+		s = s.substring(1);
+		return c;
+	} // getChar()
+
+	/** Returns and consumes an int value from the head of the unparsed data. */
+	public int getInt() {
+		int sz = 0;
+		trimSpace();
+		while ((sz < s.length()) && (Character.isDigit(s.charAt(sz)) ||
+					((sz==0) && (s.charAt(sz) == '-'))))
+			sz++;
+		int val = Integer.parseInt(s.substring(0, sz));
+		s = s.substring(sz);
+		return val;
+	} // getInt()
+
+	/**	Returns and consumes a double value, with two limitations:
+	 *	1) exponents are ignored  e.g. 5.4e-3 is read as 5.4;
+	 *	2) a value terminated by a 2nd "." causes an Exception to be thrown
+	 */
+	public double getDouble() {
+		int sz = 0;
+		trimSpace();
+		while ((sz < s.length()) && (Character.isDigit(s.charAt(sz)) ||
+					((sz==0)&&(s.charAt(sz) == '-')) || (s.charAt(sz) == '.')))
+			sz++;
+		double val = Double.parseDouble(s.substring(0, sz));
+		s = s.substring(sz);
+		return val;
+	} // getDouble()
+
+	/** Returns and consumes a string terminated by the first comma,
+	 *	parenthesis, bracket or brace. Equivalent to getString(false).
+	 */
+	public String getString() {
+		return getString(false);
+	} // getString()
+
+	/**
+	 *	Returns and consumes a string terminated by various punctuation symbols.
+	 *	Terminators include: '(', '[', '{', ',', '}', ']' and ')'.
+	 *	An Exception is thrown if no terminator is found.
+	 *
+	 *	@param extraPunctuation Specifies whether '-' and '.' are terminators
+	 */
+	public String getString(boolean extraPunctuation) {
+		char[] stoppers = {'(','[','{',',','}',']',')','-','.'};
+		int index1 = s.indexOf(stoppers[0]);
+		for (int i = 1; i < stoppers.length - (extraPunctuation? 0:2); i++) {
+			int index2 = s.indexOf(stoppers[i]);
+			if (index1 >= 0) {
+				if ((index2 >= 0) && (index1 > index2))
+					index1 = index2;
+			} else
+				index1 = index2;
+		}
+		if (index1 < 0)
+			throw new RuntimeException("getString(): no terminator: " + s);
+		String val = s.substring(0, index1);
+		s = s.substring(index1);
+		return val;
+	} // getString()
+
+	/** Returns and consumes a comma-separated list of terms, surrounded by a
+	 *	matching set of parentheses, brackets or braces.
+	 *	The list may have any number of levels of recursion.
+	 *	@return	The return value is a linked list of the terms
+	 *			(which themselves may be lists or String values)
+	 */
+	public ListTerm getList() {
+		if ("([{".indexOf(s.charAt(0)) >= 0)
+			return new ListTerm(getChar());
+		return null;
+	} // getList()
+
+	/**	Returns and consumes a Prolog-style predicate, consisting of a functor
+	 *	followed by an optional list of arguments in parentheses.
+	 */
+	public Predicate getPredicate() {
+		return new Predicate();
+	} // getPredicate()
+
+	class Predicate {
+
+		String head;
+		ListTerm args;
+
+		protected Predicate() {
+			head = getString(true);
+			args = getList();
+		}
+		
+		public Object arg(int index) {
+			ListTerm t = args;
+			for (int i = 0; i < index; i++)
+				t = t.next;
+			return t.term;
+		} // arg
+
+		public String toString() {
+			return (args == null)? head: head + args;
+		}
+
+	} // inner class Predicate
+
+	class ListTerm {
+
+		Object term;
+		ListTerm next;
+		char opener, closer;
+
+		protected ListTerm(char c) {
+			opener = c;
+			term = null;
+			next = null;
+			if (hasData()) {
+				switch(s.charAt(0)) {
+					case '(':
+					case '[':
+					case '{':
+						term = new ListTerm(getChar());
+						break;
+					default:
+						term = getString();
+						break;
+				}
+			}
+			if (hasData()) {
+				closer = getChar();
+				switch(closer) {
+					case ')':
+						if (opener == '(')
+							return;
+						break;
+					case ']':
+						if (opener == '[')
+							return;
+						break;
+					case '}':
+						if (opener == '{')
+							return;
+						break;
+					case ',':
+						next = new ListTerm(opener);
+						return;
+				}
+			}
+			throw new RuntimeException("Parse error in ListTerm(): " + s);
+		} // constructor
+
+		public String toString() {
+			String s = "" + opener;
+			for (ListTerm ptr = this; ptr != null; ptr = ptr.next)
+				s += ptr.term.toString() + ptr.closer;
+			return s;
+		} // toString()
+
+	} // inner class ListTerm
+
+} // class Matcher
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/util/PSPrinter.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,171 @@
+/*
+	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.awt.Component;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.geom.AffineTransform;
+import java.awt.print.PageFormat;
+import java.awt.print.Printable;
+import java.awt.print.PrinterException;
+import java.awt.print.PrinterJob;
+
+import at.ofai.music.util.Format;
+
+/** A utility class for converting graphical user interface components to
+ *  PostScript, which can be sent directly to a printer or printed to a file.
+ *  This gives much higher quality illustrations for articles
+ *  than if a screenshot is used, since scaling should not reduce quality.
+ *  The only requirement is that the component to be printed has a
+ *  <code>paint(Graphics)</code> method.
+ *  <p>There are some bugs in this code which require manual editing of
+ *  the PostScript file. First, there doesn't seem to be any way to include
+ *  the bounding box, although it is possible to calculate it. Second, the
+ *  cliprect produced in the PostScript output is wrong.
+ *  (Check: has this been fixed in more recent Java versions?
+ *  Apparently not, as of 1.5.0, but if scaling is not performed, the cliprect
+ *  is OK and the bounding box correct.)
+ *  See {@link PSPrinter#print(Graphics, PageFormat, int)}
+ */
+public class PSPrinter implements Printable {
+
+	/** the component to be converted */
+	Component component;
+	/** the desired graphical resolution in pixels per inch */
+	int resolution;	// can't work out how to ask the system
+
+	/** Print a GUI component to a PostScript printer or file.
+	 *  The 2 forms of this method are the normal ways of accessing this class.
+	 *  This form has problems printing some components. It is recommended to
+	 *  use the other version.
+	 *  @param c the component to be rendered in PostScript
+	 *  @param r the resolution of the printer in pixels per inch
+	 */
+	public static void print(Component c, int r) {
+		new PSPrinter(c, r).doPrint();
+	}
+
+	/** Print a GUI component to a PostScript printer or file.
+	 *  The 2 forms of this method are the normal ways of accessing this class.
+	 *  If no resolution is given, the picture is not scaled, and the cliprect
+	 *  is then correct. This is the recommended version to use.
+	 *  @param c the component to be rendered in PostScript
+	 */
+	public static void print(Component c) {
+		new PSPrinter(c, -1).doPrint();
+	}
+	
+	/** Constructs a PSPrinter for a given graphical component and resolution.
+	 *  @param c   the component to be rendered in PostScript
+	 *  @param res the resolution of the printer in pixels per inch; set res to
+	 *             -1 for no scaling (avoids the apparently buggy cliprect)
+	 */
+	public PSPrinter(Component c, int res) {
+		component = c;
+		resolution = res;
+	} // constructor
+
+	/** Produces a print dialog and executes the requested print job.
+	 *  The print job performs its task by callback of the
+	 *  {@link PSPrinter#print(Graphics, PageFormat, int)} method.
+	 */
+	public void doPrint() {
+		PrinterJob printJob = PrinterJob.getPrinterJob();
+		printJob.setPrintable(this);	// tell it where the rendering code is
+		if (printJob.printDialog()) {
+			try {
+				printJob.print();
+			} catch (Exception ex) {
+				ex.printStackTrace();
+			}
+		}
+	} // doPrint()
+
+	/** The callback method for performing the printing / Postscript conversion.
+	 *  The resulting PostScript file requires some post-editing.
+	 *  In particular, there are two problems to be dealt with:
+	 *  <p>1) The file has no bounding box. This method prints the correct
+	 *  bounding box to stardard output, and it must then be cut and pasted
+	 *  into the PostScript file. (There must be a better way!)
+	 *  <p>2) The cliprect is wrong (but only if the resolution is specified).
+	 *  This is solved by using resolution = -1 or by deleting the lines
+	 *  in the PostScript file from <code>newpath</code> to <code>clip</code>.
+	 *  (I don't know if this causes problems for components that try to draw
+	 *  outside of their area.)
+	 *  @param g  the graphics object used for painting
+	 *  @param f  the requested page format (e.g. A4)
+	 *  @param pg the page number (must be 0, or we report an error)
+	 *  @return   the error status; if the page is successfully rendered,
+	 *  Printable.PAGE_EXISTS is returned, otherwise if a page number greater
+	 *  than 0 is requested, Printable.NO_SUCH_PAGE is returned
+	 *  @throws   PrinterException thrown when the print job is terminated
+	 */
+	public int print(Graphics g, PageFormat f, int pg) throws PrinterException {
+		if (pg >= 1)
+			 return Printable.NO_SUCH_PAGE;
+		Graphics2D g2 = (Graphics2D) g;
+		double wd = component.getWidth();
+		double ht = component.getHeight();
+		double imwd = f.getImageableWidth();
+		double imht = f.getImageableHeight();
+		double corr = resolution / 72.0;
+		double scaleFactor = corr * Math.min(imwd / wd, imht / ht);
+		double xmin = f.getImageableX();
+		double ymin = f.getImageableY();
+		AffineTransform scale = new AffineTransform(scaleFactor, 0,
+													0, scaleFactor,
+													corr * xmin, corr * ymin);
+		Format.setGroupingUsed(false);
+		double pgHt = f.getHeight();
+		if (resolution > 0) {
+			g2.setTransform(scale);
+			System.out.println("%%BoundingBox: " +
+					Format.d(xmin, 0) + " " +
+					Format.d(pgHt - ymin - ht * scaleFactor / corr, 0) + " " +
+					Format.d(xmin + wd * scaleFactor / corr, 0) + " " +
+					Format.d(pgHt - ymin, 0));
+		} else {
+			g2.setClip(0, 0, (int)wd, (int)ht); 
+			System.out.println("%%BoundingBox: " +
+					Format.d(0, 0) + " " +
+					Format.d(pgHt - ht, 0) + " " +
+					Format.d(wd, 0) + " " +
+					Format.d(pgHt, 0));
+		}
+
+		// System.out.println(f.getWidth() + " " + f.getHeight() + " " +
+		// 				f.getImageableX() + " " + f.getImageableY() + " " +
+		// 				f.getImageableWidth() + " " + f.getImageableHeight());
+		// Letter = 8.5x11" 612x792pt  DEFAULT
+		// A4 = 210x297mm   595x842pt
+
+	//	AffineTransform scale = new AffineTransform(2.5, 0, 0, 2.5, 200, 1000);
+	//	g2.setTransform(scale);		// The figures need some fiddling.
+									// In particular, the PostScript file has:
+									// 1) no bounding box (add manually)
+									// 2) wrong cliprect (delete lines from
+									//                    "newpath" to "clip")
+		component.printAll(g2);
+		return Printable.PAGE_EXISTS;
+	} // print()
+
+} // class PSPrinter
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/util/Parameters.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,328 @@
+/*
+	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.awt.Color;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.Frame;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+import at.ofai.music.util.FrameMargins;
+
+public class Parameters extends JDialog implements ActionListener {
+	
+	abstract class Value {
+		protected JComponent component;
+		abstract protected Object getValue();
+		abstract protected void update();
+	} // abstract class Value
+
+
+	class ChoiceValue extends Value {
+
+		String[] choices;
+		int currentChoice;
+
+		protected ChoiceValue(String[] values) { this(values, 0); }
+		protected ChoiceValue(String[] values, int init) {
+			choices = values;
+			currentChoice = init;
+			component = new JComboBox(values);
+			((JComboBox)component).setSelectedIndex(currentChoice);
+			component.setBackground(colors.getBackground());
+			component.setForeground(colors.getForeground());
+		} // constructor
+		
+		protected Object getValue() { return choices[currentChoice]; }
+		public String toString() { return choices[currentChoice]; }
+		
+		protected void update() {
+			int tmp = ((JComboBox)component).getSelectedIndex();
+			if (tmp >= 0)
+				currentChoice = tmp;
+		} // update()
+
+	} // class ChoiceValue
+
+	
+	class StringValue extends Value {
+	
+		String currentValue;
+	
+		protected StringValue() { this(""); }
+		protected StringValue(String init) {
+			currentValue = init;
+			component = new JTextField(currentValue);
+			component.setBackground(colors.getBackground());
+			component.setForeground(colors.getForeground());
+		} // constructor
+
+		protected Object getValue() { return currentValue; }
+		public String toString() { return currentValue; }
+
+		protected void update() {
+			currentValue = ((JTextField)component).getText();
+		} // update()
+
+	} // class StringValue
+
+
+	class DoubleValue extends Value {
+	
+		double currentValue;
+
+		protected DoubleValue() { this(0); }
+		protected DoubleValue(double init) {
+			currentValue = init;
+			component = new JTextField(Double.toString(currentValue));
+			component.setBackground(colors.getBackground());
+			component.setForeground(colors.getForeground());
+		} // constructor
+
+		protected Object getValue() { return new Double(currentValue); }
+		public String toString() { return "" + currentValue; }
+
+		protected void update() {
+			try {
+				double tmp =
+						Double.parseDouble(((JTextField)component).getText());
+				currentValue = tmp;
+			} catch (NumberFormatException e) {}
+		} // update()
+
+	} // class DoubleValue
+
+
+	class IntegerValue extends Value {
+	
+		int currentValue;
+
+		protected IntegerValue() { this(0); }
+		protected IntegerValue(int init) {
+			currentValue = init;
+			component = new JTextField(Integer.toString(currentValue));
+			component.setBackground(colors.getBackground());
+			component.setForeground(colors.getForeground());
+		} // constructor
+
+		protected Object getValue() { return new Integer(currentValue); }
+		public String toString() { return "" + currentValue; }
+
+		protected void update() {
+			try {
+				int tmp = Integer.parseInt(((JTextField)component).getText());
+				currentValue = tmp;
+			} catch (NumberFormatException e) {}
+		} // update()
+
+	} // class IntegerValue
+
+
+	class BooleanValue extends ChoiceValue {
+
+		boolean currentValue;
+
+		protected BooleanValue() { this(true); }
+		protected BooleanValue(boolean init) {
+			super(new String[]{"True", "False"}, init? 0: 1);
+			currentValue = init;
+		} // constructor
+
+		protected Object getValue() { return new Boolean(currentValue); }
+		public String toString() { return "" + currentValue; }
+
+		protected void update() {
+			super.update();
+			currentValue = (currentChoice == 0);
+		} // update()
+			
+	} // class BooleanValue
+
+
+	protected ArrayMap map;
+	protected Frame parent;
+	protected JLabel[] keyFields;
+	protected JComponent[] valueFields;
+	protected int sz;
+	protected Colors colors;
+	protected JPanel panel1, panel2;
+	protected JButton okButton, cancelButton;
+	protected boolean cancelled;
+	static final long serialVersionUID = 0;
+
+	public Parameters(Frame f, String name) {
+		this(f, name, new Colors() {
+			public Color getBackground() { return Color.white; }
+			public Color getForeground() { return Color.black; }
+			public Color getButton()	 { return Color.white; }
+			public Color getButtonText() { return Color.black; }
+		});
+	} // constructor
+
+	public Parameters(Frame f, String name, Colors c) {
+		super(f, name, true);
+		colors = c;
+		setLocationRelativeTo(f);
+		Container pane = getContentPane();
+		pane.setLayout(new BoxLayout(pane, BoxLayout.X_AXIS));
+		panel1 = new JPanel();
+		panel2 = new JPanel();
+		pane.add(panel1);
+		pane.add(panel2);
+		panel1.setBackground(colors.getBackground());
+		panel2.setBackground(colors.getBackground());
+		getRootPane().setBorder(
+					BorderFactory.createLineBorder(colors.getBackground(), 10));
+		map = new ArrayMap();
+		okButton = new JButton("OK");
+		okButton.setBackground(colors.getButton());
+		okButton.setForeground(colors.getButtonText());
+		okButton.addActionListener(this);
+		cancelButton = new JButton("Cancel");
+		cancelButton.setBackground(colors.getButton());
+		cancelButton.setForeground(colors.getButtonText());
+		cancelButton.addActionListener(this);
+		parent = f;
+		cancelled = false;
+		setVisible(false);
+	} // constructor
+
+	public void print() {
+		sz = map.size();
+		System.out.println("at.ofai.music.util.Parameters: size = " + sz);
+		for (int i = 0; i < sz; i++) {
+			ArrayMap.Entry e = map.getEntry(i);
+			System.out.println(e.getKey() + " : " + e.getValue());
+		}
+	} // print()
+
+	public void actionPerformed(ActionEvent e) {
+		if (e.getSource() == okButton) {
+			for (int i = 0; i < sz; i++)
+				((Value)map.getEntry(i).getValue()).update();
+			cancelled = false;
+		} else
+			cancelled = true;
+		setVisible(false);
+	}
+
+	public boolean wasCancelled() {
+		return cancelled;
+	}
+
+	public void setVisible(boolean flag) {
+		if (!flag) {
+			super.setVisible(false);
+			return;
+		}
+		sz = map.size();
+		keyFields = new JLabel[sz];
+		valueFields = new JComponent[sz];
+		panel1.removeAll();
+		panel2.removeAll();
+		panel1.setLayout(new GridLayout(sz + 1, 1, 10, 5));
+		panel2.setLayout(new GridLayout(sz + 1, 1, 10, 5));
+		for (int i = 0; i < sz; i++) {
+			ArrayMap.Entry e = map.getEntry(i);
+			keyFields[i] = new JLabel((String) e.getKey());
+			panel1.add(keyFields[i]);
+			valueFields[i] = (JComponent) ((Value)e.getValue()).component;
+			panel2.add(valueFields[i]);
+		}
+		panel1.add(okButton);
+		panel2.add(cancelButton);
+		pack();
+		Dimension dim = getContentPane().getSize();
+		Dimension margins = FrameMargins.get(false);
+		int wd = dim.width + margins.width + 20;
+		int ht = dim.height + margins.height + 20;
+		int x = 0;
+		int y = 0;
+		if (parent != null) {
+			x = parent.getLocation().x + (parent.getWidth() - wd) / 2;
+			y = parent.getLocation().y + (parent.getHeight() - ht) / 2;
+		}
+	//	System.out.println("wd=" + wd + " ht=" + ht + " loc=" + x + "," + y);
+	//  java version "1.3.0rc1" has bugs in location/size with fvwm2
+	//	super.setLocation(-wd/2, -ht/2); // x, y);
+		super.setLocation(x, y);
+		super.setSize(wd, ht);
+		super.setVisible(true);
+	} // setVisible()
+
+	public boolean contains(String key) {
+		return map.containsKey(key);
+	} // contains()
+
+	public String getString(String key) {
+		return ((StringValue)map.get(key)).currentValue;
+	} // getString()
+	
+	public double getDouble(String key) {
+		return ((DoubleValue)map.get(key)).currentValue;
+	} // getDouble()
+	
+	public int getInt(String key) {
+		return ((IntegerValue)map.get(key)).currentValue;
+	} // getInt()
+
+	public boolean getBoolean(String key) {
+		return ((BooleanValue)map.get(key)).currentValue;
+	} // getBoolean()
+
+	public String getChoice(String key) {
+		return (String) ((ChoiceValue)map.get(key)).getValue();
+	} // getChoice()
+
+	public void setString(String key, String value) {
+		map.put(key, new StringValue(value));
+	} // setString()
+
+	public void setDouble(String key, double value) {
+		map.put(key, new DoubleValue(value));
+	} // setDouble()
+
+	public void setInt(String key, int value) {
+		map.put(key, new IntegerValue(value));
+	} // setInt()
+
+	public void setBoolean(String key, boolean value) {
+		map.put(key, new BooleanValue(value));
+	} // setBoolean()
+
+	public void setChoice(String key, String[] choices, int value) {
+		map.put(key, new ChoiceValue(choices, value));
+	} // setChoice()
+
+} // class Parameters
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/util/Peaks.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,227 @@
+/*
+	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.util.LinkedList;
+
+public class Peaks {
+
+	public static boolean debug = false;
+	public static int pre = 3;
+	public static int post = 1;
+	
+	/** General peak picking method for finding n local maxima in an array
+	 *  @param data input data
+	 *  @param peaks list of peak indexes
+	 *  @param width minimum distance between peaks
+	 */
+	public static int findPeaks(double[] data, int[] peaks, int width) {
+		int peakCount = 0;
+		int maxp = 0;
+		int mid = 0;
+		int end = data.length;
+		while (mid < end) {
+			int i = mid - width;
+			if (i < 0)
+				i = 0;
+			int stop = mid + width + 1;
+			if (stop > data.length)
+				stop = data.length;
+			maxp = i;
+			for (i++; i < stop; i++)
+				if (data[i] > data[maxp])
+					maxp = i;
+			if (maxp == mid) {
+				int j;
+				for (j = peakCount; j > 0; j--) {
+					if (data[maxp] <= data[peaks[j-1]])
+						break;
+					else if (j < peaks.length)
+						peaks[j] = peaks[j-1];
+				}
+				if (j != peaks.length)
+					peaks[j] = maxp;
+				if (peakCount != peaks.length)
+					peakCount++;
+			}
+			mid++;
+		}
+		return peakCount;
+	} // findPeaks()
+
+	/** General peak picking method for finding local maxima in an array
+	 *  @param data input data
+	 *  @param width minimum distance between peaks
+	 *  @param threshold minimum value of peaks
+	 *  @return list of peak indexes
+	 */
+	public static LinkedList<Integer> findPeaks(double[] data, int width,
+												double threshold) {
+		return findPeaks(data, width, threshold, 0, false);
+	} // findPeaks()
+	
+	/** General peak picking method for finding local maxima in an array
+	 *  @param data input data
+	 *  @param width minimum distance between peaks
+	 *  @param threshold minimum value of peaks
+	 *  @param decayRate how quickly previous peaks are forgotten
+	 *  @param isRelative minimum value of peaks is relative to local average
+	 *  @return list of peak indexes
+	 */
+	public static LinkedList<Integer> findPeaks(double[] data, int width,
+				double threshold, double decayRate, boolean isRelative) {
+		LinkedList<Integer> peaks = new LinkedList<Integer>();
+		int maxp = 0;
+		int mid = 0;
+		int end = data.length;
+		double av = data[0];
+		while (mid < end) {
+			av = decayRate * av + (1 - decayRate) * data[mid];
+			if (av < data[mid])
+				av = data[mid];
+			int i = mid - width;
+			if (i < 0)
+				i = 0;
+			int stop = mid + width + 1;
+			if (stop > data.length)
+				stop = data.length;
+			maxp = i;
+			for (i++; i < stop; i++)
+				if (data[i] > data[maxp])
+					maxp = i;
+			if (maxp == mid) {
+				if (overThreshold(data, maxp, width, threshold, isRelative,av)){
+					if (debug)
+						System.out.println(" peak");
+					peaks.add(new Integer(maxp));
+				} else if (debug)
+					System.out.println();
+			}
+			mid++;
+		}
+		return peaks;
+	} // findPeaks()
+
+	public static double expDecayWithHold(double av, double decayRate,
+										  double[] data, int start, int stop) {
+		while (start < stop) {
+			av = decayRate * av + (1 - decayRate) * data[start];
+			if (av < data[start])
+				av = data[start];
+			start++;
+		}
+		return av;
+	} // expDecayWithHold()
+
+	public static boolean overThreshold(double[] data, int index, int width,
+										double threshold, boolean isRelative,
+										double av) {
+		if (debug)
+			System.out.printf("%4d : %6.3f     Av1: %6.3f    ",
+								index, data[index], av);
+		if (data[index] < av)
+			return false;
+		if (isRelative) {
+			int iStart = index - pre * width;
+			if (iStart < 0)
+				iStart = 0;
+			int iStop = index + post * width;
+			if (iStop > data.length)
+				iStop = data.length;
+			double sum = 0;
+			int count = iStop - iStart;
+			while (iStart < iStop)
+				sum += data[iStart++];
+			if (debug)
+				System.out.printf("    %6.3f    %6.3f   ", sum / count,
+							data[index] - sum / count - threshold);
+			return (data[index] > sum / count + threshold);
+		} else
+			return (data[index] > threshold);
+	} // overThreshold()
+
+	public static void normalise(double[] data) {
+		double sx = 0;
+		double sxx = 0;
+		for (int i = 0; i < data.length; i++) {
+			sx += data[i];
+			sxx += data[i] * data[i];
+		}
+		double mean = sx / data.length;
+		double sd = Math.sqrt((sxx - sx * mean) / data.length);
+		if (sd == 0)
+			sd = 1;		// all data[i] == mean  -> 0; avoids div by 0
+		for (int i = 0; i < data.length; i++) {
+			data[i] = (data[i] - mean) / sd;
+		}
+	} // normalise()
+
+	/** Uses an n-point linear regression to estimate the slope of data.
+	 *  @param data input data
+	 *  @param hop spacing of data points
+	 *  @param n length of linear regression
+	 *  @param slope output data
+	 */
+	public static void getSlope(double[] data, double hop, int n,
+								double[] slope) {
+		int i = 0, j = 0;
+		double t;
+		double sx = 0, sxx = 0, sy = 0, sxy = 0;
+		for ( ; i < n; i++) {
+			t = i * hop;
+			sx += t;
+			sxx += t * t;
+			sy += data[i];
+			sxy += t * data[i];
+		}
+		double delta = n * sxx - sx * sx;
+		for ( ; j < n / 2; j++)
+			slope[j] = (n * sxy - sx * sy) / delta;
+		for ( ; j < data.length - (n + 1) / 2; j++, i++) {
+			slope[j] = (n * sxy - sx * sy) / delta;
+			sy += data[i] - data[i - n];
+			sxy += hop * (n * data[i] - sy);
+		}
+		for ( ; j < data.length; j++)
+			slope[j] = (n * sxy - sx * sy) / delta;
+	} // getSlope()
+
+	public static double min(double[] arr) { return arr[imin(arr)]; }
+
+	public static double max(double[] arr) { return arr[imax(arr)]; }
+
+	public static int imin(double[] arr) {
+		int i = 0;
+		for (int j = 1; j < arr.length; j++)
+			if (arr[j] < arr[i])
+				i = j;
+		return i;
+	} // imin()
+
+	public static int imax(double[] arr) {
+		int i = 0;
+		for (int j = 1; j < arr.length; j++)
+			if (arr[j] > arr[i])
+				i = j;
+		return i;
+	} // imax()
+
+} // class Peaks
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/util/Profile.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,61 @@
+/*
+	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;
+
+public class Profile {
+
+	public static final int MAX_SIZE = 20;
+	private static long[] tmin = new long[MAX_SIZE];
+	private static long[] tmax = new long[MAX_SIZE];
+	private static long[] tsum = new long[MAX_SIZE];
+	private static long[] tprev = new long[MAX_SIZE];
+	private static int[] tcount = new int[MAX_SIZE];
+
+	public static void report(int i) {
+		if ((i < 0) || (i >= MAX_SIZE) || (tcount[i] == 0))
+			return;
+		System.err.println("Profile " + i + ": " + tcount[i] + " calls;  " +
+					(tmin[i]/1000.0) + " - " + (tmax[i]/1000.0) + ";  Av: " +
+					(tsum[i] / tcount[i] / 1000.0));
+	} // report()
+
+	public static void report() {
+		for (int i = 0; i < MAX_SIZE; i++)
+			report(i);
+	} // report()
+
+	public static void start(int i) {
+		tprev[i] = System.nanoTime();
+	} // start()
+
+	public static void log(int i) {
+		long tmp = System.nanoTime();
+		long t = (tmp - tprev[i]) / 1000;
+		tprev[i] = tmp;
+		tsum[i] += t;
+		if ((tcount[i] == 0) || (t > tmax[i]))
+			tmax[i] = t;
+		if ((tcount[i] == 0) || (t < tmin[i]))
+			tmin[i] = t;
+		tcount[i]++;
+	} // log()
+
+} // class Profile
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/util/RandomAccessInputStream.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,128 @@
+/*
+	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.*;
+
+public class RandomAccessInputStream extends InputStream {
+
+	protected RandomAccessFile r;
+	protected long markPosition = 0;
+	
+	public RandomAccessInputStream(String name) throws FileNotFoundException {
+		r = new RandomAccessFile(name, "r");
+	} // constructor
+
+	public RandomAccessInputStream(File f) throws FileNotFoundException {
+		r = new RandomAccessFile(f, "r");
+	} // constructor
+
+    /** Returns the number of bytes that can be read (or skipped over) from
+	 *  this input stream without blocking by the next caller of a method for
+	 *  this input stream.
+	 */
+	public int available() throws IOException {
+		long availableBytes = r.length() - r.getFilePointer();
+		if (availableBytes > Integer.MAX_VALUE)
+			return Integer.MAX_VALUE;
+		else
+			return (int)availableBytes;
+	} // available()
+	
+	/** Closes this input stream and releases any system resources associated
+	 *  with the stream.
+	 */
+	public void close() throws IOException {
+		r.close();
+	} // close()
+	
+	/** Marks the current position in this input stream.
+	 *  Warning: Use mark() instead of mark(int).
+	 *  IOExceptions are caught, because InputStream doesn't allow them to be
+	 *  thrown. The exception is printed and the mark position invalidated.
+	 *  @param readlimit Ignored
+	 */
+	public void mark(int readlimit) {
+		try {
+			mark();
+		} catch (IOException e) {
+			e.printStackTrace();
+			markPosition = -1;
+		}
+	} // mark()
+
+	/** Marks the current position in this input stream.
+	 */
+	public void mark() throws IOException {
+		markPosition = r.getFilePointer();
+	} // mark()
+
+	/** This input stream supports the mark and reset methods.
+	 *  @return true
+	 */
+	public boolean markSupported() {
+		return true;
+	} // markSupported()
+
+	/** Reads the next byte of data from the input stream.
+	 */
+	public int read() throws IOException {
+		return r.read();
+	} // read()
+	
+	/** Reads some number of bytes from the input stream and stores them into
+	 *  the buffer array b.
+	 */
+	public int read(byte[] b) throws IOException {
+		return r.read(b);
+	} // read()
+
+	/** Reads up to len bytes of data from the input stream into an array of
+	 *  bytes.
+	 */
+	public int read(byte[] b, int off, int len) throws IOException {
+		return r.read(b, off, len);
+	} // read()
+	
+	/** Repositions this stream to the position at the time the mark method
+	 *  was last called on this input stream. 
+	 */
+	public void reset() throws IOException {
+		if (markPosition < 0)
+			throw new IOException("reset(): invalid mark position");
+		r.seek(markPosition);
+	} // reset()
+	
+	/** Skips over and discards n bytes of data from this input stream.
+	 */
+	public long skip(long n) throws IOException {
+		long pos = r.getFilePointer();
+		r.seek(n + pos);
+		return r.getFilePointer() - pos;
+	} // skip()
+
+	/** Seek to a position n bytes after the mark.
+	 */
+	public long seekFromMark(long n) throws IOException {
+		r.seek(markPosition + n);
+		return r.getFilePointer() - markPosition;
+	} // seekFromMark()
+
+} // class RandomAccessInputStream
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/util/TempoMap.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,27 @@
+/*
+	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;
+
+interface TempoMap {
+	double toRealTime(double scoreTime);
+	double toScoreTime(double realTime);
+	void add(double time, double value);
+} // TempoMap
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/util/WormEvent.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,44 @@
+/*
+	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;
+
+public class WormEvent extends Event {
+
+	public double tempo;
+	public double loudness;
+	public String label;
+
+	public WormEvent(double on, double off, double eoff, int pitch, int vel,
+			double beat, double dur, int flags, int cmd, int ch, int tr) {
+		super(on, off, eoff, pitch, vel, beat, dur, flags, cmd, ch, tr);
+		tempo = -1;
+		loudness = -1;
+		label = null;
+	} // WormEvent
+
+	public WormEvent(double time, double t, double l, double beat, int flags) {
+		super(time, 0, 0, 0, 0, beat, 0, flags);
+		tempo = t;
+		loudness = l;
+		label = null;
+	} // constructor
+
+} // class WormEvent
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/worm/AudioWorm.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,658 @@
+/*  Performance Worm: Visualisation of Expressive Musical Performance
+	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.worm;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioInputStream;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.DataLine;
+import javax.sound.sampled.Line;
+import javax.sound.sampled.LineUnavailableException;
+import javax.sound.sampled.Mixer;
+import javax.sound.sampled.SourceDataLine;
+import javax.sound.sampled.TargetDataLine;
+import javax.sound.sampled.UnsupportedAudioFileException;
+
+import java.net.URL;
+import at.ofai.music.util.Format;
+import at.ofai.music.util.EventList;
+
+/** AudioWorm is the class that does the hard work.
+  * The constructor initialises the audio objects which process the data.
+  * Each call to nextBlock() reads in a new block of data and sends it to
+  * the output device, as well as processing it and sending a new tempo
+  * estimate to the Worm object. If the output buffer is not too low, the
+  * the Worm object is asked to update its display and the audio processing
+  * sleeps for 75ms to allow this to happen.
+  **/
+public class AudioWorm {
+
+	Worm gui;				// The object that displays the data
+	AudioInputStream in;	// Input stream (from WAV file or sound card)
+	AudioInputStream orig = null;
+	boolean isConverting = false;
+	TargetDataLine targetDataLine;
+	boolean isFileInput;
+	AudioFormat inputFormat;
+	SourceDataLine out;
+	AudioFormat outputFormat;
+	int outputBufferSize;
+	int frameSize;
+	double frameRate;
+	int channels;
+	int sampleSizeInBytes;
+	static final float defaultSampleRate = 44100;
+		// for kiefer's sound card:	  44101.0F instead of 44100
+	static double windowTime = 0.010;
+	static int averageCount = 10;
+	static int fileDelay = 180;		// for liszt; 120 for bach; 70 for Schubert
+	int windowSize;
+	double normalise;
+	byte[] inputBuffer;
+	int bytesRead;
+	int blockCount;
+	TempoInducer ti;
+	WormFile wormData;
+	String audioFileName, audioFilePath;
+	File audioFile;
+	URL audioURL;
+	long bytePosition;	// Number of bytes that have been read from input file
+	long jumpPosition;	// Requested new bytePosition (or -1 for none)
+	long fileLength;	// Length of input file in bytes
+	
+	public AudioWorm(Worm w) {
+		gui = w;
+		jumpPosition = -1;
+		targetDataLine = null;
+		// Input from audio file, with optional matchFile data
+		String matchFile = w.getMatchFile();
+		if ((matchFile != null) && !matchFile.equals("")) {
+			try {
+				EventList.setTimingCorrection(w.getTimingOffset());
+				wormData = new WormFile(w, EventList.readMatchFile(matchFile));
+				if (Math.abs(windowTime * averageCount -
+								wormData.outFramePeriod) > 1e-5)
+					throw new Exception("Incompatible parameters in AudioWorm");
+				wormData.smooth(Worm.FULL_GAUSS, 1, 1, 0);
+			} catch (Exception e) {
+				e.printStackTrace();
+				wormData = null;
+			}
+		} else {
+			wormData = w.getWormFile();
+		}
+		ti = new TempoInducer(windowTime);
+		audioFile = null;
+		audioURL = null;
+		audioFileName = w.getInputFile();
+		audioFilePath = w.getInputPath();
+		if (audioFileName == null)
+			audioFileName = "";
+		if (audioFilePath == null)
+			audioFilePath = "";
+		isFileInput = (!audioFileName.equals(""));
+		if (!isFileInput) {
+			initSoundCardInput(w);
+			return;
+		}
+		if (audioFileName.startsWith("http:"))
+			try {
+				audioURL = new URL(audioFileName);
+			} catch (java.net.MalformedURLException e) {
+				e.printStackTrace();
+			}
+		else {
+			audioFile = new File(audioFileName);
+			if (!audioFile.isFile())
+				audioFile = new File(audioFilePath + audioFileName);
+			if (!audioFile.isFile()) // local hacks for UNC file names + Windows
+				audioFile = new File("//fichte" + audioFileName);
+			if (!audioFile.isFile())
+				audioFile = new File("//fichte" + audioFilePath +audioFileName);
+		}
+		resetAudioFile();
+		init(gui);
+		if ((wormData != null) && (wormData.time[0] > 0.5))
+			skipTo(wormData.time[0] - 0.5);
+	} // AudioWorm constructor
+
+	protected void resetAudioFile() {
+		try {
+			isConverting = false;
+			orig = null;
+			if (audioFile == null)
+				in = AudioSystem.getAudioInputStream(audioURL);
+			else {
+				if (!audioFile.isFile())
+					throw(new FileNotFoundException("No file: "+audioFileName));
+				in = AudioSystem.getAudioInputStream(audioFile);
+			}
+			inputFormat = in.getFormat();
+			if (inputFormat.getEncoding() != AudioFormat.Encoding.PCM_SIGNED) {
+				AudioFormat desiredFormat = new AudioFormat(
+					AudioFormat.Encoding.PCM_SIGNED,
+					inputFormat.getSampleRate(),
+					16,
+					inputFormat.getChannels(),
+					inputFormat.getChannels() * 2,
+					inputFormat.getSampleRate(),
+					false);
+		//		System.out.println("Old Format\n" + inputFormat + "\n\n" +
+		//							"Desired Format\n" + desiredFormat);
+				orig = in;
+				in = AudioSystem.getAudioInputStream(desiredFormat, orig);
+				inputFormat = in.getFormat();
+		//		System.out.println("New Format\n" + inputFormat);
+				isConverting = true;
+			}
+			fileLength = in.available();	// note this is 0 for mp3 files
+		//	System.out.println("Bytes available: " + fileLength);
+			bytePosition = 0;
+			bytesRead = 0;
+			blockCount = 0;
+		} catch (IOException e) {	// includes FileNotFoundException
+			e.printStackTrace();
+			in = null;
+		} catch (IllegalArgumentException e) {	// conversion not supported
+			e.printStackTrace();
+			in = null;
+		} catch (UnsupportedAudioFileException e) {
+			e.printStackTrace();
+			in = null;
+		}
+	} // resetAudioFile()
+
+	// Live input from microphone, line in, etc (selected by external mixer)
+	protected void initSoundCardInput(Worm w) {
+		//System.out.println("Entering initSoundCardInput()");
+		if (in != null) {
+			try {
+				in.close();
+				//System.out.println("in CLOSED");
+			} catch (Exception e) {}
+			in = null;
+		}
+		if (targetDataLine != null) {
+			try {
+				targetDataLine.close();
+				//System.out.println("targetDataLine CLOSED");
+			} catch (Exception e) {}
+			targetDataLine = null;
+		}
+		inputFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
+					  defaultSampleRate, 16, 2, 4, defaultSampleRate, false);
+		Mixer.Info[] mInfo = AudioSystem.getMixerInfo();
+		System.out.println("Number of mixers: " + mInfo.length);
+		for (int i = 0; i < mInfo.length; i++) {
+			System.out.println("Mixer info : " + mInfo[i]);
+			Mixer t = AudioSystem.getMixer(mInfo[i]);
+			Line.Info[] li = t.getTargetLineInfo(); // get input devices
+			Class c;
+			System.out.println("Number of target lines: " + li.length);
+			for (int j = 0; j < li.length; j++) {   // find one that matches
+				System.out.println("Line info: " + li[j]);
+				c = li[j].getLineClass();
+				AudioFormat[] af = ((DataLine.Info)li[j]).getFormats();
+				for (int k = 0; k < af.length; k++) {
+					// some sound cards support 44101 Hz but not 44100 Hz!
+					double err = checkAudioFormats(af[k], inputFormat);
+					if (err < 0.01) {	// match OK (allow up to 1% error)
+						if (err >= 0)		// sample rate is within 1%
+							inputFormat = af[k];
+						// else device accepts any sample rate; use default
+						DataLine.Info info = new DataLine.Info(c, inputFormat);
+						try {
+							System.out.println("Getting line with " + info);
+							if (AudioSystem.getLine(info)
+												instanceof TargetDataLine){
+								targetDataLine = null;
+								targetDataLine =
+									(TargetDataLine)AudioSystem.getLine(info);
+								System.out.println("Opening line ... ");
+								targetDataLine.open(inputFormat); // , 16384);
+								// buffer size request ignored
+								//   default size: bach 65536; kiefer 16384
+								// System.out.println("Buffer: " +
+								// 	targetDataLine.getBufferSize());
+								System.out.println("Creating AudioInputStream");
+								in = new AudioInputStream(targetDataLine);
+								init(w);
+								return;
+							}
+						} catch (Exception e) {
+							System.err.println("Unable to open input line");
+							e.printStackTrace();
+							System.exit(1);
+						}
+					}
+				}
+			}
+		}
+		throw new RuntimeException("No suitable input line found");
+	} // initSoundCardInput()
+	
+	/** Normal initialisation of AudioWorm for reading from a PCM file
+		or for reading compressed data which is uncompressed by AudioSystem.
+	 **/
+	protected void init(Worm w) {
+		if (out != null)
+			out.close();
+		gui = w;
+		gui.setDelay(isFileInput? fileDelay / averageCount: 0);
+		gui.setFramePeriod(averageCount * windowTime);
+		try {
+			if (inputFormat.getEncoding() != AudioFormat.Encoding.PCM_SIGNED)
+				throw new UnsupportedAudioFileException("Not PCM_SIGNED but " + 
+			//	System.out.println("Input format: " +
+													inputFormat.getEncoding());
+			frameSize = inputFormat.getFrameSize();
+			frameRate = inputFormat.getFrameRate();
+			channels = inputFormat.getChannels();
+			sampleSizeInBytes = frameSize / channels;
+			windowSize = (int)(windowTime * frameRate);
+			normalise = (double) channels * windowSize *
+						(1 << (inputFormat.getSampleSizeInBits() - 1));
+			inputBuffer = new byte[windowSize * frameSize];
+			bytePosition = 0;
+			bytesRead = 0;
+			blockCount = 0;
+			if (!isFileInput)
+				return;
+			Mixer.Info[] mInfo = AudioSystem.getMixerInfo();
+			for (int i = 0; i < mInfo.length; i++) {
+				Mixer t = AudioSystem.getMixer(mInfo[i]);
+				Line.Info[] li = t.getSourceLineInfo();	// get output devices
+				Class c;
+				for (int j = 0; j < li.length; j++) {	// find one that matches
+					c = li[j].getLineClass();
+					AudioFormat[] af = ((DataLine.Info)li[j]).getFormats();
+					for (int k = 0; k < af.length; k++) {
+						// some sound cards support 44101 Hz but not 44100 Hz!
+						double err = checkAudioFormats(af[k], inputFormat);
+						if (err < 0.01) {	// match OK (allow up to 1% error)
+							if (err < 0)	// device accepts any sample rate
+								outputFormat = inputFormat;
+							else			// sample rate is within 1%
+								outputFormat = af[k];
+							outputBufferSize = (int)outputFormat.getFrameRate()
+												* frameSize * 1;	// 1 sec bf
+							DataLine.Info info = new DataLine.Info(c,
+												outputFormat, outputBufferSize);
+							if (AudioSystem.getLine(info) instanceof
+									SourceDataLine) {
+								out = (SourceDataLine)AudioSystem.getLine(info);
+								out.open();
+								return;	// accept the first one we find
+							}
+						}
+					}
+				}
+			}
+			throw new LineUnavailableException("Unable to find output device" +
+												" matching:\n\t" + inputFormat);
+		} catch (LineUnavailableException e) {
+			e.printStackTrace();
+			System.exit(1);
+		} catch (UnsupportedAudioFileException e) {
+			e.printStackTrace();
+			System.exit(1);
+		}
+	} // init()
+
+	public void start() { 
+		//System.out.println("Start called");//DEBUG
+		if (isFileInput) {
+			gui.setDelay(fileDelay / averageCount);
+			out.start();
+		} else {
+			//System.out.println("Flushing targetDataLine");//DEBUG
+			targetDataLine.flush();
+			//System.out.println("Restarting targetDataLine");//DEBUG
+			targetDataLine.start();
+		}
+		//System.out.println("Start completed");//DEBUG
+	} // start()
+
+	public void pause() {
+		//System.out.println("Pause called");//DEBUG
+		if (isFileInput)
+			out.stop();
+		else
+			targetDataLine.stop();
+		//System.out.println("Pause completed");//DEBUG
+	} // pause()
+
+	public void stop() {
+		//System.out.println("Stop called");//DEBUG
+		if (isFileInput) {
+			out.stop();
+			out.flush();
+		} else {
+			targetDataLine.stop();
+			//System.out.println("Flushing targetDataLine");//DEBUG
+			targetDataLine.flush();
+		}
+		//System.out.println("Stop completed");//DEBUG
+	} // stop()
+
+	public boolean nextBlock() throws IOException {
+		double rms = 0, tempo = 0;
+		for (int i = 0; i < averageCount; i++) {	// 5 => 20 FPS
+			int waitCount = 1;//D
+			while ((in.available() < inputBuffer.length) && !isFileInput) {
+				try {
+					int before = in.available();
+					//System.out.print("WAIT: "+(waitCount++)+" "+before);//DEBG
+					Thread.sleep((int)(1000.0 * windowTime));
+					int after = in.available();
+					//System.out.println(" " + after +
+					//					";   input line active/running: " +
+					//					targetDataLine.isActive() + "/" +
+					//					targetDataLine.isRunning());
+					if ((waitCount > 5) && (before == after)) {
+						// bytesRead = in.read(inputBuffer); // HANGS HERE
+					//	System.out.println("Read(): " + bytesRead +
+					//						" " + in.available());
+						break;
+					}
+					if ((waitCount > 3) && (!targetDataLine.isActive() ||
+											!targetDataLine.isRunning()))
+						return false;
+				} catch (InterruptedException e) {}
+			}
+			long avail = (isConverting? orig.available(): in.available());
+			// This isn't really correct for uncompressing, since the number
+			//  of bytes will be different after uncompression, but it is a
+			//  hack to get around the fact that in.available() returns 0.
+			if (avail >= inputBuffer.length) {
+				double tmp = processWindow();
+				if (bytesRead < 0) {
+					System.err.println("nextBlock(): Audio read error");
+					return false;
+				}
+				rms += tmp * tmp;
+				if (wormData == null)
+					tempo = ti.getTempo(tmp); // rms amp or dB?
+				// tempo = ti.getTempo(120 + 20 / Math.log(10) * Math.log(tmp));
+				if (isFileInput) {
+					if (ti.onset)	// mark detected onsets with a click
+						for (int j = 0; j < 882; ) {
+							inputBuffer[j++] = (byte)(100.0 *
+												Math.sin(2.0*Math.PI*j/441.0));
+							inputBuffer[j++] = 0;
+						}
+					int chk = out.write(inputBuffer,0,bytesRead);
+				//	if (chk != bytesRead) {	// shouldn't happen; write() blocks
+				//		System.err.println("Problem writing " + bytesRead +
+				//						   " bytes.  Only " + chk +" written.");
+				//	}
+				}
+				blockCount++;
+			} else {
+				System.err.println("nextBlock(): Audio data not available");
+				return false;
+			}
+		}
+		double dB = Math.max(0, 120 + 20 / Math.log(10) *
+								Math.log(Math.sqrt(rms / averageCount)));
+		int index = (blockCount - 1) / averageCount;
+		if (wormData != null) {
+			gui.scrollBar.setValueNoFeedback( // don't want to call skipAudio()
+							(int)(1000 * bytePosition / fileLength));
+			if (index >= wormData.outTempo.length) {
+			//	System.err.println("DEBUG: end of wormfile");
+			//	System.err.println("DEBUG: " + blockCount +
+			//					   " " + index +
+			//					   " " + wormData.outIntensity[index]);
+				return false;
+			}
+			gui.addPoint(wormData.outTempo[index], wormData.outIntensity[index],
+							wormData.label[index]);
+		} else {
+			gui.addPoint(60 / tempo, dB, Format.d((blockCount-1)*windowTime,1));
+			// System.out.println(Format.d(tempo,3) + " " + Format.d(dB, 3));
+		}
+		if (!isFileInput) {
+			gui.repaint();
+			return true;
+		}
+		int space = out.available();
+		double buffContents = (double)(outputBufferSize - space) *
+								windowTime / inputBuffer.length;
+		// System.out.println("dB = " + Format.d(dB) +
+		// 				"  In buffer = " + Format.d(buffContents) +
+		// 				" sec  Space left = " + space +
+		// 				" bytes  Size = " + outputBufferSize);
+		if (buffContents > 0.1) {				// shouldn't starve?
+			gui.repaint();
+			try {					// Give it some time to paint.
+				Thread.sleep(75);
+			} catch (InterruptedException e) {}
+			// According to System.currentTimeMillis(), paint() takes ~16ms,
+			// but it updates much smoother (i.e. calls paint() more frequently)
+			// when the value is higher. Not sure how to convince it otherwise.
+		}
+		return true;
+	} // nextBlock()
+	
+	/** Reads a block of audio data, summing the channels and returning the
+	 *   normalised (in the range 0.0-1.0) RMS average of the sample values.
+	 *   Assumes sufficient data is queued, or else blocks while it waits
+	 *   for the buffer to fill sufficiently.
+	 **/
+	protected double processWindow() throws IOException {
+		if (jumpPosition >= 0)
+			skipAudio();
+		bytesRead = in.read(inputBuffer);
+		bytePosition += bytesRead;
+		// System.out.println("read(): " + bytePosition);//DEBUG
+		if (wormData != null)		// only need to play audio; no calculation
+			return 0;
+		long sample;
+		double sum = 0;
+		if (sampleSizeInBytes == 1) {		// 8 bit samples
+			if (channels == 1) {
+				for (int i = 0; i < bytesRead; i += frameSize) {
+					sample = ((int)inputBuffer[i]);
+					sum += (double)(sample * sample);
+				}
+			} else if (channels == 2) {
+				for (int i = 0; i < bytesRead; i += frameSize) {
+					sample = ((int)inputBuffer[i]) +
+							 ((int)inputBuffer[i+1]);
+					sum += (double)(sample * sample);
+				}
+			} else {
+				for (int i = 0; i < bytesRead; ) {
+					sample = 0;
+					for (int c = 0; c < channels; c++, i++)
+						sample += ((int)inputBuffer[i]);
+					sum += (double)(sample * sample);
+				}
+			}
+		} else if (sampleSizeInBytes == 2) {	// 16 bit samples
+			if (inputFormat.isBigEndian()) {
+				if (channels == 1) {
+					for (int i = 0; i < bytesRead; i += frameSize) {
+						sample = ((int)inputBuffer[i] << 8) |
+								 ((int)inputBuffer[i+1] & 0xFF);
+						sum += (double)(sample * sample);
+					}
+				} else if (channels == 2) {
+					for (int i = 0; i < bytesRead; i += frameSize) {
+						sample = (((int)inputBuffer[i] << 8) |
+								 ((int)inputBuffer[i+1] & 0xFF)) +
+								 (((int)inputBuffer[i+2] << 8) |
+								 ((int)inputBuffer[i+3] & 0xFF));
+						sum += (double)(sample * sample);
+					}
+				} else {
+					for (int i = 0; i < bytesRead; ) {
+						sample = 0;
+						for (int c = 0; c < channels; c++) {
+							sample += ((int)inputBuffer[i] << 8) |
+									  ((int)inputBuffer[i+1] & 0xFF);
+							i += 2;
+						}
+						sum += (double)(sample * sample);
+					}
+				}
+			} else {	// little-endian
+				if (channels == 1) {
+					for (int i = 0; i < bytesRead; i += frameSize) {
+						sample = ((int)inputBuffer[i+1] << 8) |
+								 ((int)inputBuffer[i] & 0xFF);
+						sum += (double)(sample * sample);
+					}
+				} else if (channels == 2) {
+					for (int i = 0; i < bytesRead; i += frameSize) {
+						sample = (((int)inputBuffer[i+1] << 8) |
+								 ((int)inputBuffer[i] & 0xFF)) +
+								 (((int)inputBuffer[i+3] << 8) |
+								 ((int)inputBuffer[i+2] & 0xFF));
+						sum += (double)(sample * sample);
+					}
+				} else {
+					for (int i = 0; i < bytesRead; ) {
+						sample = 0;
+						for (int c = 0; c < channels; c++) {
+							sample += ((int)inputBuffer[i+1] << 8) |
+									  ((int)inputBuffer[i] & 0xFF);
+							i += 2;
+						}
+						sum += (double)(sample * sample);
+					}
+				}
+			}
+		} else {	// not 8-bit or 16-bit samples
+			for (int i = 0; i < bytesRead; ) {
+				long longSample = 0;
+				for (int c = 0; c < channels; c++) {
+					if (inputFormat.isBigEndian()) {
+						sample = (int)inputBuffer[i++];
+						for (int b = 1; b < sampleSizeInBytes; b++, i++)
+							sample = (sample<<8) | ((int)inputBuffer[i] & 0xFF);
+					} else {
+						sample = 0;
+						int b;
+						for (b = 0; b < sampleSizeInBytes-1; b++, i++)
+							sample |= ((int)inputBuffer[i] & 0xFF) << (b * 8);
+						sample |= ((int)inputBuffer[i++]) << (b * 8);
+					}
+					longSample += sample;
+				}
+				sum += (double)longSample * (double)longSample;
+			}
+		}
+		return Math.sqrt(sum) / normalise;
+	} // processWindow()
+
+	/** Returns the relative difference  |a-b| / ((a+b)/2)  in sample rates,
+	 *  or 2.0 if the formats do not match in some other characteristic,
+	 *  or -1 if AudioFormat `out' allows all sampling rates;
+	 *  normal return value is in [0,2).
+	 **/
+	public static double checkAudioFormats(AudioFormat out, AudioFormat in) {
+		if ((out.getChannels() != in.getChannels()) ||
+				(out.getSampleSizeInBits() != in.getSampleSizeInBits()) ||
+				(out.getEncoding() != in.getEncoding()) ||
+				(out.isBigEndian() != in.isBigEndian()) ||
+				(out.getSampleRate() == 0.0F))
+			return 2.0;
+		if (out.getSampleRate() < 0)
+			return -1.0;
+		return Math.abs(2.0 * (out.getSampleRate() - in.getSampleRate()) /
+							  (out.getSampleRate() + in.getSampleRate()));
+	} // checkAudioFormats()
+
+	/** Returns the RMS average of an array of double.
+	 **/
+	public static double rms(double[] data) {
+		double sum = 0;
+		for (int i = 0; i < data.length; i++)
+			sum += data[i] * data[i];
+		return Math.sqrt(sum / (double)data.length);
+	} // rms()
+
+	/** Reposition the audio file input to a new point in seconds.
+	 **/
+	public void skipTo(double time) {
+		jumpPosition = Math.round(time * frameRate) * frameSize;
+		if (jumpPosition > fileLength)
+			jumpPosition = fileLength;
+	} // skipTo()
+
+	/** Reposition the audio file input (from Scrollbar) on a scale of
+	 *  0 (beginning of file) to 1000 (end of file).
+	 **/
+	public void skipTo(int thousandths) {
+		jumpPosition = fileLength / frameSize * thousandths / 1000 * frameSize;
+	} // skipTo()
+
+	/** For input from an audio file, performs repositioning within the file,
+	 *  after a call to the public method skipTo().
+	 **/
+	protected void skipAudio() {
+		// gui.pause();
+		long toSkip = jumpPosition;
+		long hasSkipped = 0;
+		if (jumpPosition >= bytePosition)			// fast forward
+			toSkip -= bytePosition;
+		else if (jumpPosition < bytePosition)		// rewind
+			resetAudioFile();
+		try {
+			while (toSkip > hasSkipped) {	// due to buffering, skip() must be
+				//  called multiple times before it reaches the requested spot
+				//  (see JavaSound mailing list Item #4658 (5 Nov 2000 20:58))
+				long skipped = in.skip(toSkip - hasSkipped);
+				if (skipped <= 0)
+					throw new IOException("skip() error: "+skipped+" returned");
+				hasSkipped += skipped;
+			}
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		bytePosition += hasSkipped;
+		if (out != null)
+			out.flush();
+		int currentPoint = (blockCount - 1) / averageCount;
+		blockCount = (int) (bytePosition / (long)inputBuffer.length);
+		if (wormData != null) {
+			int stop = Math.min(wormData.outTempo.length, 
+								(blockCount - 1) / averageCount);
+			int start = Math.max(stop - WormConstants.wormLength, 0);
+			if (currentPoint < stop)
+				start = Math.max(start, currentPoint);
+			else
+				gui.clear();
+			for (int index = start; index < stop; index++)
+				gui.addPoint(wormData.outTempo[index],
+						wormData.outIntensity[index], wormData.label[index]);
+			gui.repaint();
+		}
+		jumpPosition = -1;
+	} // skipAudio()
+
+} // class AudioWorm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/worm/MyFileChooser.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,65 @@
+/*  Performance Worm: Visualisation of Expressive Musical Performance
+	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.worm;
+
+import javax.swing.JFileChooser;
+import javax.swing.JOptionPane;
+import java.awt.TextField;
+import java.io.File;
+import javax.swing.filechooser.FileFilter;
+
+public class MyFileChooser extends JFileChooser {
+
+	static final long serialVersionUID = 0;	// silence compiler warning
+	
+	public MyFileChooser() {
+		super(".");
+		addChoosableFileFilter(MyFileFilter.mp3Filter);
+		addChoosableFileFilter(MyFileFilter.waveFilter);
+		addChoosableFileFilter(MyFileFilter.matchFilter);
+		addChoosableFileFilter(MyFileFilter.wormFilter);
+		addChoosableFileFilter(getAcceptAllFileFilter());
+	} // constructor
+
+	public String browseOpen(TextField t, FileFilter ff) {
+		setSelectedFile(new File(t.getText()));
+		setFileFilter(ff);
+		if (showOpenDialog(null) == JFileChooser.APPROVE_OPTION)
+			t.setText(getSelectedFile().getAbsolutePath());
+		return t.getText();
+	} // browseOpen()
+
+	public String browseSave() {
+		// setSelectedFile(s);
+		setFileFilter(MyFileFilter.wormFilter);
+		if (showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
+			if (!getSelectedFile().exists() || (
+				JOptionPane.showConfirmDialog(null, "File " +
+						getSelectedFile().getAbsolutePath() +
+						" exists.\nDo you want to replace it?",
+						"Are you sure?", JOptionPane.YES_NO_OPTION) ==
+						JOptionPane.YES_OPTION))
+			return getSelectedFile().getAbsolutePath();
+		}
+		return null;
+	} // browseSave()
+	
+} // class MyFileChooser
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/worm/MyFileFilter.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,51 @@
+/*  Performance Worm: Visualisation of Expressive Musical Performance
+	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.worm;
+
+import java.io.File;
+import javax.swing.filechooser.FileFilter;
+
+class MyFileFilter extends FileFilter {
+
+	public static final MyFileFilter mp3Filter =
+						new MyFileFilter(".mp3", "MPEG1 level 3");
+	public static final MyFileFilter waveFilter =
+						new MyFileFilter(".wav", "Wave");
+	public static final MyFileFilter matchFilter =
+						new MyFileFilter(".match", "Match");
+	public static final MyFileFilter wormFilter =
+						new MyFileFilter(".worm", "Worm");
+
+	protected String suffix;
+	protected String description;
+	
+	public MyFileFilter(String suff, String desc) {
+		suffix = suff;
+		description = desc + " files (*" + suff + ")";
+	} // constructor
+
+	public boolean accept(File f) {
+		return (f != null) && (f.isDirectory() || f.getName().endsWith(suffix));
+	} // accept()
+
+	public String getDescription() { return description; }
+
+} // class MyFileFilter
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/worm/Plot.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,105 @@
+/*  Performance Worm: Visualisation of Expressive Musical Performance
+	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.worm;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.GraphicsConfiguration;
+import java.awt.Rectangle;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+
+import javax.swing.BoxLayout;
+import javax.swing.JFrame;
+import javax.swing.WindowConstants;
+
+import at.ofai.music.util.FrameMargins;
+
+public class Plot extends JFrame {
+	
+	static final long serialVersionUID = 0;	// silence compiler warning
+	
+	JFrame frame;
+	public PlotPanel panel;
+
+	public Plot(double[] xData, double[] yData) {
+		this();
+		panel.addPlot(xData, yData);
+	}
+
+	public Plot() {
+		frame = new JFrame();
+		panel = new PlotPanel(frame);
+		frame.getContentPane().setBackground(WormConstants.backgroundColor);
+		frame.getContentPane().setLayout(new BoxLayout(frame.getContentPane(),
+					BoxLayout.Y_AXIS));
+		frame.getContentPane().add(panel);
+		panel.addHierarchyBoundsListener(panel);
+		Dimension borderSize = FrameMargins.get(false);
+		frame.setSize(panel.getWidth() + borderSize.width,
+				  panel.getHeight() + borderSize.height);
+				  // + WormConstants.cpHeight);
+		GraphicsConfiguration gc = frame.getGraphicsConfiguration();
+		Rectangle bounds = gc.getBounds();	// [x=0 y=0 w=1280 h=1024]
+		frame.setLocation(bounds.x + (bounds.width - frame.getWidth()) / 2,
+					  bounds.height - frame.getHeight());
+		//			  bounds.y + (bounds.height - frame.getHeight()) / 2);
+		frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
+		frame.setVisible(true);
+		frame.setIconImage(WormIcon.getWormIcon(1, frame));
+	}
+
+	public void setTitle(String s) { panel.setTitle(s); }
+	public void setAxis(String s) { panel.setAxis(s); }
+	public void setXAxis(double min, double max) { panel.setXAxis(min, max); }
+	public void setYAxis(double min, double max) { panel.setYAxis(min, max); }
+	public void setLength(int i, int l) { panel.setLength(i, l); }
+	public void rotateCurrent() { panel.rotateCurrent(); }
+	public void update() { panel.update(); }
+	public void fitAxes() { panel.fitAxes(); }
+	public void fitAxes(int current) { panel.fitAxes(current); }
+	public void setMode(int m) { panel.setMode(m); }
+	public void clear() { panel.clear(); }
+	public void close() { frame.setVisible(false); }
+	public void addPlot(double[] x, double[] y) {
+		addPlot(x, y, Color.blue);
+	}
+	public void addPlot(double[] x, double[] y, Color c) {
+		addPlot(x, y, c, PlotPanel.IMPULSE | PlotPanel.HOLLOW);
+	}
+	public void addPlot(double[] x, double[] y, Color c, int mode) {
+		panel.addPlot(x, y, c, mode);
+	}
+
+	public static void main(String[] args) {	// simple test of this class
+		double[] x = new double[100];
+		double[] y = new double[100];
+		for (int i = 0; i < 100; i++) {
+			x[i] = i;
+			y[i] = Math.sin(2 * Math.PI * i / 50);
+		}
+		Plot testPlot = new Plot(x, y);
+		testPlot.panel.xAxis.test();	//SD: remove
+		testPlot.frame.addWindowListener(new WindowAdapter() {
+			public void windowClosed(WindowEvent e) { System.exit(0); } });
+	} // main()
+
+} // class Plot
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/worm/PlotListener.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,72 @@
+/*  Performance Worm: Visualisation of Expressive Musical Performance
+	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.worm;
+
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+
+class PlotListener implements KeyListener {
+	PlotPanel callback;
+	public PlotListener(PlotPanel p) { callback = p; }
+	public void keyTyped(KeyEvent e) {}	// doesn't get all keys; keyCode == 0
+	public void keyPressed(KeyEvent e) {}
+	public void keyReleased(KeyEvent e) {
+		if ((e.getModifiers() & KeyEvent.CTRL_MASK) != 0)		// CTRL + key
+			switch (e.getKeyCode()) {
+				case KeyEvent.VK_UP:	callback.yZoom(true, false);	break;
+				case KeyEvent.VK_DOWN:	callback.yZoom(false, false);	break;
+				case KeyEvent.VK_LEFT:	callback.xZoom(false, false);	break;
+				case KeyEvent.VK_RIGHT:	callback.xZoom(true, false);	break;
+			}
+		else if ((e.getModifiers() & KeyEvent.SHIFT_MASK) != 0)	// SHIFT + key
+			switch (e.getKeyCode()) {
+				case KeyEvent.VK_P:		callback.print(600);			break;
+				case KeyEvent.VK_UP:	callback.yZoom(false, true);	break;
+				case KeyEvent.VK_DOWN:	callback.yZoom(true, true);		break;
+				case KeyEvent.VK_LEFT:	callback.xZoom(true, true);		break;
+				case KeyEvent.VK_RIGHT:	callback.xZoom(false, true);	break;
+			}
+		else													// normal key
+			switch (e.getKeyCode()) {
+				case KeyEvent.VK_0:		callback.setMode(0);			break;
+				case KeyEvent.VK_1:		callback.setMode(1);			break;
+				case KeyEvent.VK_2:		callback.setMode(2);			break;
+				case KeyEvent.VK_3:		callback.setMode(3);			break;
+				case KeyEvent.VK_4:		callback.setMode(4);			break;
+				case KeyEvent.VK_5:		callback.setMode(5);			break;
+				case KeyEvent.VK_6:		callback.setMode(6);			break;
+				case KeyEvent.VK_7:		callback.setMode(7);			break;
+				case KeyEvent.VK_8:		callback.setMode(8);			break;
+				case KeyEvent.VK_9:		callback.setMode(9);			break;
+				case KeyEvent.VK_F:		callback.fitAxes();				break;
+				case KeyEvent.VK_N:		callback.rotateCurrent();		break;
+				case KeyEvent.VK_P:		callback.print(300);			break;
+				case KeyEvent.VK_R:		callback.resize();				break;
+				case KeyEvent.VK_U:		callback.update();				break;
+				case KeyEvent.VK_Q:		callback.close();				break;
+				case KeyEvent.VK_UP:	callback.yMoveDown(false);		break;
+				case KeyEvent.VK_DOWN:	callback.yMoveDown(true);		break;
+				case KeyEvent.VK_LEFT:	callback.xMoveRight(false);		break;
+				case KeyEvent.VK_RIGHT:	callback.xMoveRight(true);		break;
+			}
+	} // keyReleased()
+
+} // class PlotListener
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/worm/PlotPanel.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,471 @@
+/*  Performance Worm: Visualisation of Expressive Musical Performance
+	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.worm;
+
+import java.awt.Color;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.Shape;
+import java.awt.event.HierarchyBoundsListener;
+import java.awt.event.HierarchyEvent;
+import java.util.NoSuchElementException;
+import java.util.StringTokenizer;
+
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+
+import at.ofai.music.util.Format;
+import at.ofai.music.util.PSPrinter;
+
+class PlotPanel extends JPanel implements HierarchyBoundsListener {
+	
+	static final long serialVersionUID = 0;
+	
+	JFrame theFrame;					// Parent container of this panel
+	String title;
+	public static final int NONE = 0,
+							HOLLOW = 1,
+							FILLED = 2,
+							JOIN = 4,
+							IMPULSE = 8,
+							STEP = 16;
+	int plots;
+	PlotData[] data;
+	PlotData current;
+	int currentPtr;
+	AxisData xAxis, yAxis;
+	boolean initialised;
+	FontMetrics fm;		// of the font used for axis labels
+	int ht;				// the height of this font
+
+	class AxisData {
+		double min, max, maxValue, minValue, scale;
+		int ticks, digits, margin, size, maxTicks;
+		String label;
+		boolean isVertical;
+
+		public AxisData(boolean v) {
+			isVertical = v;
+			margin = isVertical? WormConstants.footMargin:
+								 WormConstants.sideMargin;
+			size = isVertical? WormConstants.Y_SZ: WormConstants.X_SZ;
+			setMaxTicks(10,10);
+			ticks = maxTicks;
+			digits = 2;
+			label = "";
+		}
+		
+		void test() {
+			try {
+				java.io.BufferedReader b =
+						new java.io.BufferedReader(
+						new java.io.InputStreamReader(System.in));
+				while (true) {
+					StringTokenizer t = new StringTokenizer(b.readLine());
+					double mn = Double.parseDouble(t.nextToken());
+					double mx = Double.parseDouble(t.nextToken());
+					setBounds(mn, mx);
+				}
+			} catch (Exception e) {
+				System.out.println(e);
+			}
+		} // test()
+
+		void setMaxTicks(double mn, double mx) {
+			if (fm == null)
+				maxTicks = 10;
+			else if (isVertical)
+				maxTicks = Math.max(3, Math.min(10, size / ht * 2 / 3));
+			else {
+				int wd = Math.max(fm.stringWidth(Format.d(mn, digits)),
+								  fm.stringWidth(Format.d(mx, digits)));
+				maxTicks = Math.max(3, Math.min(10, size / wd * 2 / 3));
+			}
+		} // setMaxTicks()
+		
+		void setBounds(double mn, double mx) {
+			if (mn > mx) {
+				double tmp = mn;
+				mn = mx;
+				mx = tmp;
+			}
+			setMaxTicks(mn, mx);
+			double diff = (mx - mn) / maxTicks;
+			double base = Math.pow(10, Math.floor(Math.log(diff)/Math.log(10)));
+			int head = (int)Math.ceil(diff / base);
+			if (head > 5)
+				diff = 10 * base;
+			else if (head > 2)
+				diff = 5 * base;
+			else // head = 1 or 2
+				diff = head * base;
+			int low = (int) Math.floor(mn / diff);
+			int high = (int) Math.ceil(mx / diff);
+			if (high - low > maxTicks) {
+				if (head > 5)
+					diff = 20 * base;
+				else if (head > 2)
+					diff = 10 * base;
+				else if (head == 2)
+					diff = 5 * base;
+				else
+					diff = 2 * base;
+				low = (int) Math.floor(mn / diff);
+				high = (int) Math.ceil(mx / diff);
+			}
+			int pow = (int)Math.floor(Math.log(diff)/Math.log(10));
+			if (pow < 0)
+				digits = -pow;
+			else
+				digits = 0;
+			ticks = high - low;	// alt. code (below) replaces this line
+			rescale(low * diff, high * diff);
+			return;
+		} // setBounds()
+
+// Alternative code which keeps the number of ticks fixed.
+//			int empty = maxTicks - high + low;
+//			int[] next = {5,4,4};
+//			int count = 0;
+//			for (int n = 2; empty > 0; ) {
+//				System.out.println("need to shift bounds outwards: "
+//									+low * diff+ " " +high * diff+ " "+empty);
+//				int rem = low % n;
+//				if (rem < 0)
+//					rem += n;
+//				int rem1 = high % n;
+//				if (rem1 < 0)
+//					rem1 += n;
+//				System.out.println(low + " % " + n + " = " + rem);
+//				System.out.println(high + " % " + n + " = " + rem1);
+//				if ((rem != 0) && (rem <= empty))
+//					low -= rem;
+//				else if ((rem1 != 0) && (n - rem1 <= empty))
+//					high += n - rem1;
+//				else if (n >= maxTicks)	// force exit
+//					high += empty;
+//				else
+//					n = n * next[count++ % 3] / 2;
+//				empty = maxTicks - high + low;
+//			}
+//			rescale(low * diff, high * diff);
+
+		void translate(boolean positive) {
+			double diff = (max - min) / (positive? -4: 4);
+			rescale(min - diff, max - diff);
+		} // translate()
+
+		void zoom(boolean in, boolean positive) {
+			double diff = (max - min) / (in? -2: 1);
+			if (positive)
+				setBounds(min, max + diff);
+			else
+				setBounds(min - diff, max);
+		} // zoom()
+		
+		void rescale(double newMin, double newMax) {
+			size = isVertical? getHeight(): getWidth();
+			min = newMin;
+			max = newMax;
+			scale = (double)(size - 2 * margin) / (max - min);
+			repaint();
+		} // rescale()
+
+	} // class AxisData
+
+	class PlotData {
+		double[] x;							// x-coordinates
+		double[] y;							// y-coordinates
+		Color pointColour, joinColour;		// Colours of plot
+		int pointSize;						// Size of a point
+		int mode;
+		int length;
+
+		protected PlotData(double[] xData, double[] yData) {
+			this(xData, yData, xData.length);
+		} // constructor
+		
+		protected PlotData(double[] xData, double[] yData, int len) {
+			x = xData;
+			y = yData;
+			length = len;
+			pointColour = Color.blue;
+			joinColour = Color.red;
+			pointSize = 6;
+			mode = IMPULSE | HOLLOW;
+			if ((xData != null) && (yData != null)) {
+				if (plots < data.length) {
+					currentPtr = plots;
+					data[plots++] = this;
+				} else
+					throw new RuntimeException("Too many plots");
+			}
+		} // constructor
+
+	} // plotData
+
+	public PlotPanel(JFrame frame) {
+		this(frame, null, null);
+	} // constructor
+	
+	public PlotPanel(JFrame frame, double[] xData, double[] yData) {
+		theFrame = frame;
+		title = "";
+		data = new PlotData[10];
+		currentPtr = plots = 0;
+		initialised = false;
+		if ((xData != null) && (yData != null))
+			current = new PlotData(xData, yData);
+		xAxis = new AxisData(false);
+		yAxis = new AxisData(true);
+		setSize(xAxis.size, yAxis.size);
+		fitAxes();
+		frame.addKeyListener(new PlotListener(this));
+	} // constructor
+
+	public void fitAxes(int current) {
+		setCurrent(current);
+		fitAxes();
+	} // fitAxes()
+
+	public void fitAxes() {
+		if (current != null) {
+			xAxis.setBounds(min(current.x, current.length), max(current.x, current.length));
+			yAxis.setBounds(min(current.y, current.length), max(current.y, current.length));
+		}
+	} // fitAxes()
+
+	void resize() {
+		xAxis.setBounds(xAxis.min, xAxis.max);
+		yAxis.setBounds(yAxis.min, yAxis.max);
+	} // resize()
+
+	void update() {
+		repaint();
+	} // update()
+
+	void setCurrent(int c) {
+		if (c < plots) {
+			currentPtr = c;
+			current = data[currentPtr];
+		}
+	} // setCurrent()
+
+	void rotateCurrent() {
+		if (plots != 0) {
+			currentPtr = (currentPtr + 1) % plots;
+			current = data[currentPtr];
+		}
+	} // rotateCurrent()
+
+	void clear() {
+		plots = 0;
+		current = null;
+	} // clear
+
+	// Allows multiple plots, similar to Matlab's "hold on"
+	boolean addPlot(double[] xData, double[] yData, Color c, int m) {
+		if (addPlot(xData, yData)) {
+			current.pointColour = c;
+			current.joinColour = c;
+			current.mode = m;
+			return true;
+		}
+		return false;
+	} // addPlot()
+
+	boolean addPlot(double[] xData, double[] yData) {
+		if ((xData != null) && (yData != null) && (plots < data.length)) {
+			current = new PlotData(xData, yData);
+			if (!initialised) {
+				resize();
+				initialised = true;
+			}
+			return true;
+		}
+		return false;
+	} // addPlot()
+
+	void setLength(int index, int len) {
+		data[index].length = len;
+	} // setLength()
+	
+	void setTitle(String t) { title = t; theFrame.setTitle(t); }
+
+	void setXLabel(String s) { xAxis.label = s; }
+	void setYLabel(String s) { yAxis.label = s; }
+	
+	void setAxis(String s) {
+		StringTokenizer tk = new StringTokenizer(s);
+		try {
+			xAxis.rescale(Double.parseDouble(tk.nextToken()),
+					 Double.parseDouble(tk.nextToken()));
+			yAxis.rescale(Double.parseDouble(tk.nextToken()),
+					 Double.parseDouble(tk.nextToken()));
+		} catch (NoSuchElementException e) {	// ignore illegal values
+			System.err.println("Illegal axes specification: " + e);
+		} catch (NumberFormatException e) {		// ignore illegal values
+			System.err.println("Illegal axes specification: " + e);
+		}
+	} // setAxis()
+
+	void setXAxis(double min, double max) { xAxis.rescale(min, max); }
+	void setYAxis(double min, double max) { yAxis.rescale(min, max); }
+
+	void setMode(int m) { if (current != null) { current.mode = m; repaint(); }}
+
+	void close() { theFrame.dispose(); }
+	
+	public void print(int res) {
+		PSPrinter.print(this, res);
+	} // print()
+
+	public void paint(Graphics g) {
+		if (fm == null) {
+			fm = g.getFontMetrics();
+			ht = fm.getHeight();
+		}
+		// Paint background
+		g.setColor(WormConstants.backgroundColor);
+		g.fillRect(0, 0, xAxis.size, yAxis.size);
+		// Plot points
+		int xprev = 0, yprev = 0;
+		int yZero = yAxis.margin + (int)(yAxis.max * yAxis.scale);
+		Shape saveClip = g.getClip();
+		g.clipRect(xAxis.margin, yAxis.margin, xAxis.size - 2 * xAxis.margin,
+					yAxis.size - 2 * yAxis.margin);
+		for (int j = 0; j < plots; j++) {
+			for (int i = 0; i < data[j].x.length; i++) {
+				int xx = xAxis.margin +
+							(int)((data[j].x[i] - xAxis.min) * xAxis.scale);
+				int yy = yAxis.margin +
+							(int)((yAxis.max - data[j].y[i]) * yAxis.scale);
+				int sz = data[j].pointSize;
+				g.setColor(data[j].pointColour);
+				if ((data[j].mode & HOLLOW) != 0)
+					g.drawOval(xx - sz / 2, yy - sz / 2, sz, sz);
+				if ((data[j].mode & FILLED) != 0)
+					g.fillOval(xx - sz / 2, yy - sz / 2, sz, sz);
+				g.setColor(data[j].joinColour);
+				if ((data[j].mode & IMPULSE) != 0)
+					g.drawLine(xx, yy, xx, yZero);
+				else if (((data[j].mode & JOIN) != 0) && (i != 0)) {
+					if ((data[j].mode & STEP) != 0) {
+						g.drawLine(xx, yy, xx, yprev);	// join with step fn
+						g.drawLine(xx, yprev, xprev, yprev); 
+					} else
+						g.drawLine(xx, yy, xprev, yprev);	// join directly
+				}
+				xprev = xx;
+				yprev = yy;
+			}
+		}
+		g.setClip(saveClip);
+		// Draw axes and labels
+		g.setColor(WormConstants.axesColor);
+		g.drawRect(xAxis.margin, yAxis.margin, xAxis.size - 2 * xAxis.margin,
+					yAxis.size - 2 * yAxis.margin);
+		for (int i = 1; i < xAxis.ticks; i++) {
+			int z = xAxis.margin +
+						i * (xAxis.size - 2 * xAxis.margin) / xAxis.ticks;
+			String label = Format.d(xAxis.min + i * (xAxis.max - xAxis.min) /
+									xAxis.ticks, xAxis.digits);
+			int wd = fm.stringWidth(label);
+			g.drawString(label, z - wd / 2, yAxis.size - 5);
+			g.drawLine(z, yAxis.size - yAxis.margin - 2,
+						z, yAxis.size - yAxis.margin + 2);
+			g.drawLine(z, yAxis.margin - 2,
+						z, yAxis.margin + 2);
+		}
+		for (int i = 1; i < yAxis.ticks; i++) {
+			int z = yAxis.margin + i*(yAxis.size-2 * yAxis.margin)/yAxis.ticks;
+			String label = Format.d(yAxis.max - i * (yAxis.max - yAxis.min) /
+									yAxis.ticks, yAxis.digits);
+			int wd = fm.stringWidth(label);
+			g.drawString(label, xAxis.margin - wd - 5, z + ht / 2);
+			g.drawLine(xAxis.margin - 2, z,
+						xAxis.margin + 2, z);
+			g.drawLine(xAxis.size - xAxis.margin - 2, z,
+						xAxis.size - xAxis.margin + 2, z);
+		}
+		int wd = fm.stringWidth(yAxis.label);
+		g.drawString(yAxis.label, xAxis.margin - wd - 5, yAxis.margin + ht / 2);
+		wd = fm.stringWidth(xAxis.label);
+		g.drawString(xAxis.label, xAxis.size - xAxis.margin-wd/2, yAxis.size-5);
+	} // paint()
+
+	public void ancestorMoved(HierarchyEvent e) {}
+
+	public void ancestorResized(HierarchyEvent e) {
+		if ((xAxis.size == getWidth()) && (yAxis.size == getHeight()))
+			return;
+		xAxis.setBounds(xAxis.min, xAxis.max);
+		yAxis.setBounds(yAxis.min, yAxis.max);
+		xAxis.setBounds(xAxis.min, xAxis.max);	// recalculate to fix labels
+		yAxis.setBounds(yAxis.min, yAxis.max);
+	} // ancestorResized()
+
+	void xMoveRight(boolean right) { xAxis.translate(right); }
+	void xZoom(boolean in, boolean right) { xAxis.zoom(in, right); }
+	void yMoveDown(boolean down) { yAxis.translate(down); }
+	void yZoom(boolean in, boolean down) { yAxis.zoom(in, down); }
+
+	static double min(double[] d) {
+		if ((d == null) || (d.length == 0))
+			return 0;
+		return min(d, d.length);
+	} // max()
+	
+	static double min(double[] d, int len) {
+		if (d == null)
+			return 0;
+		if (d.length < len)
+			len = d.length;
+		if (len == 0)
+			return 0;
+		double m = d[0];
+		for (int i = 1; i < d.length; i++)
+			if ((m == Double.NaN) || (d[i] < m))
+				m = d[i];
+		return m;
+	} // min()
+	
+	static double max(double[] d) {
+		if ((d == null) || (d.length == 0))
+			return 0;
+		return max(d, d.length);
+	} // max()
+	
+	static double max(double[] d, int len) {
+		if (d == null)
+			return 0;
+		if (d.length < len)
+			len = d.length;
+		if (len == 0)
+			return 0;
+		double m = d[0];
+		for (int i = 1; i < len; i++)
+			if ((m == Double.NaN) || (d[i] > m))
+				m = d[i];
+		return m;
+	} // max()
+
+} // class PlotPanel
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/worm/TempoInducer.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,448 @@
+/*  Performance Worm: Visualisation of Expressive Musical Performance
+	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.worm;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+import at.ofai.music.util.Format;
+
+/** CLASS TempoInducer finds tempo (rate) but not beat times (phase) of
+  * a performance.
+  * It implements a real-time incremental tempo induction algorithm,
+  * keeps track of multiple hypotheses, and allows switching between these
+  * hypotheses by the user.
+  * It assumes time is measured on a discrete scale (the timeBase),
+  * where an amplitude (rms?) value is supplied for each time point and
+  * the estimate of the tempo at this point is returned (method getTempo()).
+  * Constants define the maximum, minimum and default tempos in terms of the
+  * corresponding interbeat interval (IBI), and the length of time over which
+  * tempo is induced (MEMORY).
+  */
+public class TempoInducer {
+
+	double timeBase;
+	double[] envelope;
+	double[] slope;
+	double[] peakTime;
+	double[] peakSPL;
+	double[] iois;
+	double[] cluster;
+	double[] clusterWgt;
+	double[] newCluster;
+	double[] newClusterWgt;
+	double[] best;
+	double[] bestWgt;
+	boolean[] bestUsed;
+	int peakHead;	// position of next peak (i.e. currently empty)
+	int peakTail;	// position before first peak (i.e. currently empty)
+	int bestCount;
+	double tempo;
+	int counter;
+	double longTermSum;
+	double recentSum;
+	double[] prevAmp;
+	static final double MEMORY = 8.0; 	// time in seconds used for IOI calcns
+//	static final double MEMORY = 1000.0; //SD WAS 8 - changed for dance test
+	static final double MIN_IOI = 0.1;
+	static final double MAX_IOI = 2.5;
+//	static final double MAX_IOI = 5;	//SD was 2.5 - changed for dance test
+	static final double MIN_IBI = 0.200;		// 300 BPM  = 0.2
+	static final double LOW_IBI = 0.400;		// 150 BPM  = 0.4
+	static final double DEF_IBI = 0.800;		// 100 BPM  = 0.6
+	static final double HI_IBI = 1.200;			//  75 BPM  = 0.8
+	static final double MAX_IBI = 1.500;		//  40 BPM	= 1.5
+//	static final double MIN_IBI = 0.100;
+//	static final double LOW_IBI = 0.100;
+//	static final double DEF_IBI = 0.800;		// 100 BPM  = 0.6
+//	static final double HI_IBI = 5.000;
+//	static final double MAX_IBI = 5.000;	//SD all changed for dance test
+	static final double HYP_CHANGE_FACTOR = (1 - 0.4);	//WAS 1-.4
+	static final double AMP_MEM_FACTOR = 0.95;
+	static final double DECAY_BEST = 0.9;	// weight of best[0] decreases 10%/s
+	static final double DECAY_OTHER = 0.8;	// weight of best[i] decreases 20%/s
+	static final double RATIO_ERROR = 0.1;	// 1.9-2.1, 2.9-3.1, ...,7.9-8.1
+	static final int CLUSTER_WIDTH = 8;			//WAS 4
+	static final int CLUSTER_FACTOR = 30;		//WAS 10
+	static final int CLUSTER_POINTS = 10;		//WAS 20
+//	static final int CLUSTER_POINTS = 20;		//SD dance test WAS 10
+	static final int REGRESSION_SIZE = 4;
+	static final int SLOPE_POINTS = 15;
+	static final int MID_POINT = SLOPE_POINTS / 2;
+	static final int PEAK_POINTS = (int)MEMORY * 20;
+	static final int OVERLAP = 4;
+	int ioiPoints;
+	public boolean onset;
+	static Plot plot = null;
+	static boolean plotFlag = false;
+	double[] xplot, yplot, xplot2, yplot2, xplot3, yplot3;
+
+	public TempoInducer(double tb) {
+	//	System.err.println("Using infinite memory....");	//SD for dance test
+		timeBase = tb;
+		ioiPoints = (int)Math.ceil(MAX_IOI / tb) + 1;
+		envelope = new double[SLOPE_POINTS];
+		slope = new double[SLOPE_POINTS];
+		peakTime = new double[PEAK_POINTS];
+		peakSPL = new double[PEAK_POINTS];
+		iois = new double[ioiPoints];
+		cluster = new double[CLUSTER_POINTS];
+		clusterWgt = new double[CLUSTER_POINTS];
+		newCluster = new double[CLUSTER_POINTS];
+		newClusterWgt = new double[CLUSTER_POINTS];
+		best = new double[CLUSTER_POINTS];
+		bestWgt = new double[CLUSTER_POINTS];
+		bestUsed = new boolean[CLUSTER_POINTS];
+		bestCount = 0;
+		peakHead = 0;
+		peakTail = PEAK_POINTS - 1;
+		tempo = DEF_IBI;
+		counter = 0;
+		longTermSum = 0;
+		recentSum = 0;
+		prevAmp = new double[OVERLAP];
+		xplot = new double[ioiPoints];
+		for (int i = 0; i < ioiPoints; i++)
+			xplot[i] = timeBase * i;
+		yplot = new double[ioiPoints];
+		xplot2 = new double[CLUSTER_POINTS];
+		yplot2 = new double[CLUSTER_POINTS];
+		xplot3 = new double[CLUSTER_POINTS];
+		yplot3 = new double[CLUSTER_POINTS];
+		if (plotFlag)
+			makePlot();
+	} // constructor
+
+	void makePlot() {
+		System.out.println("makePlot() " + (plot == null));//DEBUG
+		if (plot == null)
+			plot = new Plot();
+		else
+			plot.clear();
+		plot.addPlot(xplot, yplot, java.awt.Color.blue, PlotPanel.IMPULSE);
+		plot.addPlot(xplot2, yplot2, java.awt.Color.green);
+		plot.addPlot(xplot3, yplot3, java.awt.Color.red);
+		plot.setTitle("Tempo Tracking Histogram and Clusters");
+	} // constructor
+
+	public double getTempo(double amp) {
+		int i;
+		counter++;
+		for (i = 0; i < SLOPE_POINTS - 1; i++) {
+			envelope[i] = envelope[i+1];
+			slope[i] = slope[i+1];
+		}
+		envelope[i] = amp;
+		for (int j = 0; j < OVERLAP; j++) { // overlap windows
+			envelope[i] += prevAmp[j];
+			if (j == OVERLAP - 1)
+				prevAmp[j] = amp;
+			else
+				prevAmp[j] = prevAmp[j+1];
+		}
+		longTermSum += amp;
+		if (recentSum == 0)
+			recentSum = amp;
+		else
+			recentSum = AMP_MEM_FACTOR * recentSum + (1 - AMP_MEM_FACTOR) * amp;
+		slope[i] = getSlope();	// slope of points up to i (i.e. env[i-3 : i])
+		for (i = 0; i < SLOPE_POINTS; i++)
+			if ((i != MID_POINT) && (slope[MID_POINT] <= slope[i]))
+				break;
+		// System.out.println(i + " " + Format.d(envelope[MID_POINT], 5) + " " +
+		// 		Format.d(slope[MID_POINT],5) + " " +
+		// 		Format.d(longTermSum / (counter*4*REGRESSION_SIZE*timeBase),3));
+		// System.out.println(i + " " + Format.d(1000*recentSum,3) + " " +
+		// 							Format.d(1000*longTermSum / counter, 3));
+		if ((i == SLOPE_POINTS) &&
+				(envelope[MID_POINT] > recentSum / 5) &&
+				(slope[MID_POINT] > recentSum /
+								(2 * REGRESSION_SIZE * timeBase))) {
+			// onset = true;	// click on onsets (for debugging)
+			addPeak(timeBase * (counter - MID_POINT), envelope[MID_POINT]);
+		} else
+			onset = false;
+		return tempo;
+	} // getTempo
+	
+	public void switchLevels(boolean faster) {
+		for (int i = 1; i < bestCount; i++)
+			if ((best[i] < best[0]) == faster) {
+				double tmp = best[0];
+				best[0] = best[i];
+				best[i] = tmp;
+				break;
+			}
+	} // switchLevels()
+	
+	static int next(int p) {
+		return (p == PEAK_POINTS - 1)? 0: p + 1;
+	} // next()
+
+	static int prev(int p) {
+		return (p == 0)? PEAK_POINTS - 1: p - 1;
+	} // prev()
+
+	static int top(int p) {		// Required nearness of IOI's for clustering
+		return p + p / CLUSTER_FACTOR + CLUSTER_WIDTH; // was 2*0.010=20ms (3pt)
+	} // top()
+
+	// Updates tempo based on new peak
+	void addPeak(double t, double dB) {
+		java.util.Arrays.fill(iois, 0);
+		// add new peak
+		peakTime[peakHead] = t;
+		peakSPL[peakHead] = dB;
+		peakHead = next(peakHead);
+		if (peakHead == peakTail)
+			System.err.println("Overflow: too many peaks");
+		// delete old peaks
+		int loPtr = next(peakTail);			// first peak
+		while (t - peakTime[loPtr] > MEMORY) {
+			peakTail = loPtr;
+			loPtr = next(peakTail);
+		}
+		// get all IOIs
+		int hiPtr;
+		for ( ; loPtr != peakHead; loPtr = next(loPtr))
+			for (hiPtr = next(loPtr); hiPtr != peakHead; hiPtr = next(hiPtr)) {
+				double ioi = peakTime[hiPtr] - peakTime[loPtr];
+				if (ioi >= MAX_IOI)
+					break;
+				iois[(int)Math.rint(ioi / timeBase)] += // 1 +	// better??
+								Math.sqrt(peakSPL[hiPtr] * peakSPL[loPtr]);
+			}
+		for (int i = 0; i < iois.length; i++)
+			yplot[i] = iois[i];	// copy values before they are destroyed
+		// System.out.println("x = [");
+		// for (int p = 0; p < 200; p++)
+		// 	System.out.println(Format.d(1000*iois[p], 3));
+		// System.out.println("];");
+		// make clusters (with width defined by top())
+		int clusterCount = 0;
+		for ( ; clusterCount < CLUSTER_POINTS; clusterCount++) {
+			double sum = 0;
+			double max = 0;
+			int maxIndex = 0;
+			hiPtr = (int)(MIN_IOI / timeBase);			// ignore < 100ms
+			loPtr = hiPtr;
+			while (hiPtr < ioiPoints) {	// find window with greatest average
+				if (hiPtr >= top(loPtr))
+					sum -= iois[loPtr++];
+				else {
+					sum += iois[hiPtr++];
+					if (sum / (top(loPtr) - loPtr) > max) {
+						max = sum / (top(loPtr) - loPtr);
+						maxIndex = loPtr;
+					}
+				}
+			}
+			if (max == 0)
+				break;
+			hiPtr = top(maxIndex);
+			if (hiPtr > ioiPoints)
+				hiPtr = ioiPoints;
+			sum = 0;
+			double weights = 0;
+			for (loPtr = maxIndex; loPtr < hiPtr; loPtr++) {
+				sum += loPtr * iois[loPtr];
+				weights += iois[loPtr];
+				iois[loPtr] = 0;	// use each value once
+			}
+			cluster[clusterCount] = sum / weights * timeBase; // Weighted av (s)
+			clusterWgt[clusterCount] = max;
+			// System.out.println(Format.d(cluster[clusterCount], 3) + "   " +
+			// 				Format.d(clusterWgt[clusterCount], 3) + "   " +
+			// 				Format.d(weights, 3) + "   " + maxIndex);
+		}
+		// re-weight clusters using related clusters
+		for (int i = 0; i < clusterCount; i++) {
+			newCluster[i] = cluster[i] * clusterWgt[i];
+			newClusterWgt[i] = clusterWgt[i];
+			for (int j = 0; j < clusterCount; j++) {
+				if (i != j) {
+					int ratio = getRatio(cluster[i], cluster[j]);
+					// newCluster = sum[(val{*|/}rat)*wgt/rat] / sum[wgt/rat] 
+					if (ratio > 0) {
+						newCluster[i] += cluster[j] * clusterWgt[j];
+						newClusterWgt[i] += clusterWgt[j] / ratio;
+					} else if (ratio < 0) {
+						newCluster[i] += cluster[j] * clusterWgt[j] / 
+											(ratio * ratio);
+						newClusterWgt[i] += clusterWgt[j] / -ratio;
+					}
+				}
+			}
+			newCluster[i] /= newClusterWgt[i];
+		}
+		for (int i = 0; i < CLUSTER_POINTS; i++) {
+			if (i < clusterCount) {
+				xplot2[i] = cluster[i];
+				yplot2[i] = clusterWgt[i] * 3;
+				xplot3[i] = newCluster[i];
+				yplot3[i] = newClusterWgt[i] * 1.5;
+			} else
+				xplot2[i] = yplot2[i] = xplot3[i] = yplot3[i] = 0;
+		}
+		if (plotFlag) {
+			if (plot == null)
+				makePlot();
+			plot.update();
+		} else if (plot != null) {
+			plot.close();
+			plot = null;
+		}
+		// update best clusters; smooth over time
+		double dt = t - peakTime[prev(prev(peakHead))];
+		// System.out.println("CURRENT IOI = " + Format.d(dt,3));
+		//if (false)	//SD for dance tests
+		for (int i = 0; i < bestCount; i++) {
+			bestUsed[i] = false;
+			if (i != 0)
+				bestWgt[i] *= Math.pow(DECAY_OTHER, dt); // memory decay 20%/s
+			else
+				bestWgt[i] *= Math.pow(DECAY_BEST, dt);  // memory decay 10%/s
+			if (best[i] < LOW_IBI)		// penalise if too fast ...
+				bestWgt[i] *= 1 - Math.pow((LOW_IBI - best[i]) /
+											(2 * (LOW_IBI - MIN_IBI)), 3);
+			else if (best[i] > HI_IBI)	// ... or too slow
+				bestWgt[i] *= 1 - Math.pow((best[i] - HI_IBI) /
+											(2 * (MAX_IBI - HI_IBI)), 3);
+		}
+		for (int i = 0; i < clusterCount; i++) {
+			if ((newCluster[i] < MIN_IBI) || (newCluster[i] > MAX_IBI))
+				continue;
+			double dMax = newCluster[i]/CLUSTER_FACTOR + CLUSTER_WIDTH*timeBase;
+			double dMin = dMax / 2;	// NEW:"/2"; don't allow values too far away
+			int index = -1;
+			for (int j = 0; j < bestCount; j++) {	// find the nearest match
+				double diff = Math.abs(newCluster[i] - best[j]);
+				if (diff < dMin) {
+					dMin = diff;
+					index = j;
+				}
+			}
+			if (index >= 0) {						// match found; update best
+				if (bestUsed[index])
+					continue;
+				// update is equivalent to exp.-decaying memory used in paint()
+				best[index] += (newCluster[i] - best[index]) *HYP_CHANGE_FACTOR;
+				//	* newClusterWgt[i] * (1 - dMin / dMax) / bestWgt[index];
+				bestWgt[index] += newClusterWgt[i] * (1 - dMin / dMax);
+				// bestUsed[index] = true;
+			} else if (bestCount < CLUSTER_POINTS) {	// not full yet; add
+				best[bestCount] = newCluster[i];
+				bestWgt[bestCount++] = newClusterWgt[i];
+			} else if (bestWgt[bestCount-1] < newClusterWgt[i]) { // best full;
+				best[bestCount-1] = newCluster[i];				// add if better
+				bestWgt[bestCount-1] = newClusterWgt[i];
+			}
+		}
+		for (int i = 0; i < bestCount; i++)		// merge clusters
+			for (int j = i + 1; j < bestCount; j++)
+				if (Math.abs(best[i] - best[j]) <
+										CLUSTER_WIDTH * timeBase / 2) {
+					best[i] = (best[i] * bestWgt[i] +
+									best[j] * bestWgt[j]) /
+									(bestWgt[i] + bestWgt[j]);
+					bestWgt[i] += bestWgt[j];
+					for (int k = j + 1; k < bestCount; k++) {
+						best[k-1] = best[k];
+						bestWgt[k-1] = bestWgt[k];
+					}
+					bestCount--;
+					j--;
+				}
+		boolean change = true;		// bubble sort, since almost sorted
+		while (change) {
+			change = false;
+			for (int i = bestCount-1; i > 0; i--)	// sort by weight
+				if (bestWgt[i] > bestWgt[i-1]) {
+					change = true;
+					double tmp = bestWgt[i];
+					bestWgt[i] = bestWgt[i - 1];
+					bestWgt[i - 1] = tmp;
+					tmp = best[i];
+					best[i] = best[i - 1];
+					best[i - 1] = tmp;
+				}
+		}
+		if (bestCount > 0) {
+			// int maxIndex = 0;
+			// for (int i = 1; i < clusterCount; i++)
+			// 	if (newClusterWgt[i] > newClusterWgt[maxIndex])
+			// 		maxIndex = i;
+			tempo = best[0];
+			// System.out.println("Best: " + Format.d(best[0], 3));
+		}
+	} // addPeak()
+
+	void showTime() {
+		System.out.println("Time = " +
+						  Format.d(timeBase * (counter - MID_POINT), 3) + "\n");
+	} // showTime()
+
+	void saveHist() { //SD for dance test
+		try {
+			Format.init(1,1,3,false);
+			FileWriter outfile = new FileWriter(new File("worm-dance.tmp"));
+			for (int i = 0; i < CLUSTER_POINTS; i++)
+				outfile.write(// Format.d(cluster[i], 3) + " : " +
+							//	Format.d(clusterWgt[i], 3) + "     " +
+							//	Format.d(newCluster[i], 3) + " : " +
+							//	Format.d(newClusterWgt[i], 3) + "     " +
+								((i < bestCount)? (Format.d(best[i], 3) + "   "+
+									Format.d(bestWgt[i], 3)) : "") +
+								"\n");
+			outfile.write("\n");
+			outfile.close();
+			System.exit(0);
+		} catch (IOException e) {
+			System.err.println("Exception in saveHist(): " + e);
+		}
+	} // saveHist()
+
+	int getRatio(double a, double b) {
+		double r = a / b;
+		if (r < 1)
+			r = -1 / r;
+		int round = (int)Math.rint(r);
+		if ((Math.abs((r - round) / r) < RATIO_ERROR) &&
+					(Math.abs(round) >= 2) && (Math.abs(round) <= 8))
+			return round;
+		return 0;
+	} // getRatio()
+
+	// Uses an n-point linear regression to estimate the slope of envelope
+	double getSlope() {
+		int start = SLOPE_POINTS - REGRESSION_SIZE;
+		double sx = 0, sxx = 0, sy = 0, sxy = 0;
+		for (int i = 0; i < REGRESSION_SIZE; i++) {
+			sx += i;
+			sxx += i * i;
+			sy += envelope[start+i];
+			sxy += i * envelope[start+i];
+		}
+		return (4 * sxy - sx * sy) / (4 * sxx - sx * sx) / timeBase;
+	} // getSlope()
+
+} // class TempoInducer
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/worm/Worm.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,719 @@
+/*  Performance Worm: Visualisation of Expressive Musical Performance
+	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.worm;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.GraphicsConfiguration;
+import java.awt.Rectangle;
+import java.awt.event.HierarchyBoundsListener;
+import java.awt.event.HierarchyEvent;
+import java.util.NoSuchElementException;
+import java.util.StringTokenizer;
+
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.JRadioButtonMenuItem;
+import javax.swing.WindowConstants;
+
+import at.ofai.music.util.FrameMargins;
+import at.ofai.music.util.Format;
+
+public class Worm extends JPanel implements Runnable, HierarchyBoundsListener {
+	static final long serialVersionUID = 0;
+	double[] x;				// Circular buffer of x-coordinates (in BPM)
+	double[] y;				// Circular buffer of y-coordinates (in dB)
+	String[] labels;		// Circular buffer of labels
+	int tail;				// pointer for circular buffer
+	private double xmin, xmax, ymin, ymax,		// Extremes of plotable display
+			xSum, xCount,						// For autoScale() on x-axis
+			xScale, yScale;						// To convert values to pixels
+	int xSize, ySize;							// Size of this panel 
+	Color[] rimColours, bodyColours;			// Range of colours of worm
+	static final int STOP = 0, PLAY = 1, PAUSE = 2;
+	int state;
+	static final int MIN_WORM_SIZE = 4;	// Diameter of worm
+	static final int WORM_SIZE = 24;	// Diameter of worm
+	static final double X_MIN_DEF = 60;	// Default is always
+	static final double X_MAX_DEF = 120;	//  on the screen
+	static final double Y_MIN_DEF = 35;
+	static final double Y_MAX_DEF = 85;
+	double[] xmem;	// memory of past values for smoothing
+	double[] ymem;
+	int memSize;	// number of points in xmem and ymem
+	int smoothMode;
+	public static final int NONE=0, EXPONENTIAL=1, HALF_GAUSS=2, FULL_GAUSS=3;
+	public static final String[] smoothLabels = {
+		"No Smoothing", "Exponential", "Half Gaussian", "Full Gaussian"};
+	static final int DEFAULT_MODE = HALF_GAUSS;
+	static final double sDecay = 0.97;		// for automatic scaling
+	static final double xDecay = 0.98;		// for exponential smoothing
+	static final double yDecay = 0.97;
+	// Decay constants: 0.5000 -> -6dB / 1pt		2 ^ (-1 / k)
+	//                  0.7071 -> -6dB / 2pt
+	//                  0.7937 -> -6dB / 3pt 
+	//                  0.8409 -> -6dB / 4pt 
+	//                  0.8706 -> -6dB / 5pt 
+	//                  0.8909 -> -6dB / 6pt 
+	//                  0.9389 -> -6dB / 11pt 
+	//                  0.9576 -> -6dB / 16pt 
+	//                  0.9675 -> -6dB / 21pt 
+	//                  0.9737 -> -6dB / 26pt 
+	//                  0.9779 -> -6dB / 31pt 
+	boolean autoScaleMode;
+	JButton playButton;
+	JCheckBoxMenuItem autoIndicator;
+	JRadioButtonMenuItem[] smoothButtons;
+	AudioWorm audio;
+	int wait;		// Number of points to hold back (due to audio buffer size)
+	double framePeriod;		// Time between successive points
+	String inputPath, inputFile, matchFile, wormFileName,
+			loudnessUnits, tempoUnits;
+	double timingOffset;
+	JFrame theFrame;
+	WormControlPanel controlPanel;
+	WormScrollBar scrollBar;
+	WormFile wormFile;
+	Thread playThread;
+
+	public Worm(JFrame f) {
+		x = new double[WormConstants.wormLength];
+		y = new double[WormConstants.wormLength];
+		labels = new String[WormConstants.wormLength];
+		xmem = new double[WormConstants.wormLength];
+		ymem = new double[WormConstants.wormLength];
+		rimColours = new Color[WormConstants.wormLength];
+		bodyColours = new Color[WormConstants.wormLength];
+		smoothMode = DEFAULT_MODE;
+		autoScaleMode = true;
+		setGlow(false);
+		loudnessUnits = "dB";
+		tempoUnits = "BPM";
+		init();
+		theFrame = f;
+		playThread = new Thread(this);
+		playThread.start();
+		xSize = WormConstants.X_SZ;
+		ySize = WormConstants.Y_SZ;
+		setSize(xSize, ySize);
+		repaint();
+	} // default constructor
+
+	public void setGlow(boolean flag) {
+		if (flag)
+			WormConstants.setNightColours();
+		else
+			WormConstants.setDayColours();
+		int r1 = WormConstants.wormHeadColor.getRed();
+		int r2 = WormConstants.wormTailColor.getRed();
+		int r3 = WormConstants.wormHeadRimColor.getRed();
+		int r4 = WormConstants.wormTailRimColor.getRed();
+		int g1 = WormConstants.wormHeadColor.getGreen();
+		int g2 = WormConstants.wormTailColor.getGreen();
+		int g3 = WormConstants.wormHeadRimColor.getGreen();
+		int g4 = WormConstants.wormTailRimColor.getGreen();
+		int b1 = WormConstants.wormHeadColor.getBlue();
+		int b2 = WormConstants.wormTailColor.getBlue();
+		int b3 = WormConstants.wormHeadRimColor.getBlue();
+		int b4 = WormConstants.wormTailRimColor.getBlue();
+		for (int i = 0; i < WormConstants.wormLength; i++) {
+			bodyColours[i] = new Color(
+	(r1 * i + r2 * (WormConstants.wormLength - i)) / WormConstants.wormLength,
+	(g1 * i + g2 * (WormConstants.wormLength - i)) / WormConstants.wormLength,
+	(b1 * i + b2 * (WormConstants.wormLength - i)) / WormConstants.wormLength);
+			rimColours[i] = new Color(
+	(r3 * i + r4 * (WormConstants.wormLength - i)) / WormConstants.wormLength,
+	(g3 * i + g4 * (WormConstants.wormLength - i)) / WormConstants.wormLength,
+	(b3 * i + b4 * (WormConstants.wormLength - i)) / WormConstants.wormLength);
+		}
+		if (controlPanel != null) {
+			Component[] c = controlPanel.getComponents();
+			for (int i = 0; i < c.length; i++) {
+				c[i].setForeground(WormConstants.buttonTextColor);
+				c[i].setBackground(WormConstants.buttonColor);
+			}
+		}
+		if (scrollBar != null)
+			scrollBar.setBackground(WormConstants.backgroundColor);
+		repaint();
+	} // setGlow()
+	
+	void init() {
+		state = STOP;
+		clear();
+		xRescale(X_MIN_DEF, X_MAX_DEF);
+		yRescale(Y_MIN_DEF, Y_MAX_DEF);
+		wait = 0;
+		framePeriod = 0;
+		timingOffset = 0;
+		xSum = 0.600;
+	} // init()
+
+	public void clearWithoutRepaint() {
+		tail = 0;
+		memSize = 0;
+		for (int i=0; i < WormConstants.wormLength; i++)
+			x[i] = -1;
+		xCount = 0;
+	} // clearWithoutRepaint()
+
+	public void clear() {
+		clearWithoutRepaint();
+		repaint();
+	} // clear()
+
+	void editParameters() {
+		if (wormFile != null)
+			wormFile.editParameters();
+	} // editParameters()
+
+	void save(String s) {
+		if ((wormFile != null) && (s != null))
+			wormFile.write(s);
+	}
+	void setInputFile(String s) { inputFile = s; }
+	void setInputFile(String path, String file) {
+		inputPath = path;
+		if (!path.endsWith("/"))
+			inputPath += '/';
+		inputFile = file;
+	}
+	String getInputFile() { return inputFile; }
+	String getInputPath() { return inputPath; }
+	void setMatchFile(String s) { matchFile = s; }
+	String getMatchFile() { return matchFile; }
+	void clearWormFile() {
+		clear();
+		wormFileName = null;
+		wormFile = null;
+	}
+	void setWormFile(String s) {
+		if ((s != null) && (s.length() > 0)) {
+			clear();
+			wormFileName = s;
+			wormFile = new WormFile(this, s);
+		}
+	}
+	public WormFile getWormFile() { return wormFile; }
+	public String getWormFileName() { return wormFileName; }
+	void setPlayButton(JButton b) { playButton = b; }
+	void setAutoButton(JCheckBoxMenuItem b) {
+		autoIndicator = b;
+		autoIndicator.setSelected(autoScaleMode);
+	}
+	void setSmoothButtons(JRadioButtonMenuItem[] sb) {
+		smoothButtons = sb;
+		smoothButtons[smoothMode].setSelected(true);
+	}
+	public void setDelay(int t) { wait = t; }
+	public void setFramePeriod(double t) { framePeriod = t; }
+	void setSmoothMode(int mode) {
+		smoothMode = mode;
+		if (smoothButtons != null)
+			smoothButtons[mode].setSelected(true);
+	}
+	int getSmoothMode() { return smoothMode; }
+	public void smooth() {
+		if (wormFile != null) {
+			new WormSmoothDialog(this, wormFile);
+		}
+	}
+	void setFileDelay(int d) { AudioWorm.fileDelay = d; }
+	void setFileDelayString(String s) {
+		try {
+			AudioWorm.fileDelay = Integer.parseInt(s);
+		} catch (NumberFormatException e) {
+			System.err.println("Invalid delay: " + s);
+		}
+	}
+	int getFileDelay() { return AudioWorm.fileDelay; }
+	String getFileDelayString() { return "" + AudioWorm.fileDelay; }
+	void setTimingOffset(double d) { timingOffset = d; }
+	void setTimingOffsetString(String s) {
+		try {
+			timingOffset = Double.parseDouble(s);
+		} catch (NumberFormatException e) {
+			System.err.println("Invalid offset: " + s);
+		}
+	}
+	String getTimingOffsetString() { return Format.d(timingOffset, 4); }
+	double getTimingOffset() { return timingOffset; }
+	void setControlPanel(WormControlPanel p) { controlPanel = p; }
+	void setScrollBar(WormScrollBar p) { scrollBar = p; }
+	void setTitle(String t) { theFrame.setTitle(t); }
+	void setLoudnessUnits(String s) { loudnessUnits = s; }
+	void setTempoUnits(String s) { tempoUnits = s; }
+	void setAxis(String s) {
+		StringTokenizer tk = new StringTokenizer(s);
+		try {
+			xRescale(Double.parseDouble(tk.nextToken()),
+					 Double.parseDouble(tk.nextToken()));
+			yRescale(Double.parseDouble(tk.nextToken()),
+					 Double.parseDouble(tk.nextToken()));
+			setAutoScaleMode(false);
+		} catch (NoSuchElementException e) {	// ignore illegal values
+			System.err.println("Illegal axes specification: " + e);
+		} catch (NumberFormatException e) {		// ignore illegal values
+			System.err.println("Illegal axes specification: " + e);
+		}
+	} // setAxis()
+	
+	// Starts/continues playing in a separate thread; immediately returns
+	void play() {
+		playButton.setText("Pause");
+		playButton.repaint();
+		if (state == STOP) {
+			clear();
+			audio = new AudioWorm(this);
+		}
+		state = PLAY;
+		audio.start();
+		synchronized(this) { // run() can now enter loop to process audio blocks
+			notify();		 // informs playThread that it can start playing
+		}
+	} // play()
+
+	public void run() {		// Code for play, executed as separate thread
+		while (true) {		// This is always running
+			try {
+				synchronized(this) {	// wait until there is something to play
+					wait();
+				}
+				Thread.sleep(200);	// wait 0.2s
+				try {
+					while ((state == PLAY) && audio.nextBlock())
+						;
+				} catch (ArrayIndexOutOfBoundsException e) {
+					e.printStackTrace();
+					audio.ti.showTime();
+				}
+				//System.out.println("loop ended " + (state != PLAY));
+				if (state == PLAY) {		// end of file; let audio drain
+				//	audio.ti.saveHist();	//SD for dance test
+					for ( ; wait > 0; wait--) {
+					//	System.err.println("DEBUG: wait = " + wait);
+						repaint();
+						Thread.sleep((int)(AudioWorm.averageCount *
+										AudioWorm.windowTime * 1000));	// wait 1.2s
+					}
+					//System.err.println("DEBUG: wait = " + wait);
+					repaint();
+					stop();
+				}
+			} catch (Exception e) {
+				e.printStackTrace();
+			}
+		}
+	} // run()
+
+	void pause() {
+		if (state == PLAY) {
+			state = PAUSE;
+			playButton.setText("Cont");
+			playButton.repaint();
+			audio.pause();
+		}
+	} // pause()
+
+	void stop() {
+		state = STOP;
+		playButton.setText("Play");
+		playButton.repaint();
+		if (audio != null)
+			audio.stop();
+	} // stop()
+
+	public void paint(Graphics g) {
+		FontMetrics fm = g.getFontMetrics();
+		int ht = fm.getHeight();
+		// Paint background
+		g.setColor(WormConstants.backgroundColor);
+		g.fillRect(0, 0, xSize, ySize);
+		if (xCount == 0) {
+			int x = xSize / 2;
+			int y = ySize / 2;
+			g.drawImage(WormIcon.getWormIcon(2,theFrame), x - 150, y - 50, null);
+			g.setColor(WormConstants.axesColor);
+			g.drawString(WormConstants.title,
+						 x - fm.stringWidth(WormConstants.title) / 2, y + 70);
+			return;
+		}
+		// Draw worm itself
+		int labelLeft = xSize - WormConstants.sideMargin -
+						fm.stringWidth("Time:9999.9m");
+		int labelRight = xSize - WormConstants.sideMargin - fm.stringWidth("m");
+		int labelHeight = WormConstants.footMargin + ht + 10;
+		String barLabel = null, beatLabel = null, trackLabel = null,
+			   timeLabel = null, prev = "0";
+		for (int i = 0; i < WormConstants.wormLength - wait; i++) {
+			int ind = (tail + i) % WormConstants.wormLength;
+			int d = MIN_WORM_SIZE +
+						WORM_SIZE * i / (WormConstants.wormLength - wait);
+			int xx = WormConstants.sideMargin + (int)((x[ind] - xmin) * xScale);
+			int yy = WormConstants.footMargin + (int)((ymax - y[ind]) * yScale);
+			if (x[ind] >= 0) {	// if there is data, draw a circle
+				int e = 0;
+				if (labels[ind].indexOf(':') < 0)
+					timeLabel = labels[ind];
+				else {
+					StringTokenizer st = new StringTokenizer(labels[ind], ":");
+					barLabel = st.nextToken();
+					beatLabel = st.nextToken();
+					trackLabel = st.nextToken();
+					timeLabel = st.nextToken();
+					if (!barLabel.equals(prev)) {
+						prev = barLabel;
+						e = 4;
+					}
+				}
+				g.setColor(rimColours[i]);	// add a dark rim
+				g.fillOval(xx - (d+e) / 2, yy - (d+e) / 2, d+e, d+e);
+				if (e == 0) {
+					g.setColor(bodyColours[i]);
+					g.fillOval(xx - (d-2) / 2, yy - (d-2) / 2, d-2, d-2);
+				}
+				if (i == WormConstants.wormLength - wait - 1) {	// draw face
+					g.setColor(WormConstants.axesColor);
+					g.drawString("Time: ", labelLeft, labelHeight);
+					g.drawString(timeLabel,
+							labelRight - fm.stringWidth(timeLabel),
+							labelHeight);
+					if (barLabel != null) {
+						g.drawString("Bar: ", labelLeft, labelHeight + ht);
+						g.drawString(barLabel,
+								labelRight - fm.stringWidth(barLabel),
+								labelHeight + ht);
+					}
+					if ((beatLabel != null) && (beatLabel.length() > 0)) {
+						g.drawString("Beat: ", labelLeft, labelHeight + 2 * ht);
+						g.drawString(beatLabel,
+								labelRight - fm.stringWidth(beatLabel),
+								labelHeight + 2 * ht);
+					}
+					if (e == 0)
+						g.setColor(WormConstants.wormFaceColor);
+					else
+						g.setColor(WormConstants.altFaceColor);
+					if (barLabel != null) {
+						int wd = fm.stringWidth(barLabel); 
+						g.drawString(barLabel, xx - wd / 2, yy + ht / 2);
+					} else {	// draw face :)
+						g.fillOval(xx -d / 5 -2, yy - d / 5, 5, 2);	// l eye
+						g.fillOval(xx +d / 5 -2, yy - d / 5, 5, 2);	// r eye
+						g.fillOval(xx - 2, yy - 3, 3, 3);			// nose
+						g.drawArc(xx - d / 4, yy - d / 4, d/2, d/2, 220, 100);
+					}
+				}
+			}
+		}
+		// Draw axes and labels
+		g.setColor(WormConstants.axesColor);
+		g.drawRect(WormConstants.sideMargin, WormConstants.footMargin,
+					xSize - 2 * WormConstants.sideMargin,
+					ySize - 2 * WormConstants.footMargin);
+		for (int i = 1; i < 10; i++) {
+			int z = WormConstants.sideMargin +
+						i * (xSize - 2 * WormConstants.sideMargin) / 10;
+			String label = Format.d(xmin + i * (xmax - xmin) / 10, 1); // xlabel
+			int wd = fm.stringWidth(label);
+			g.drawString(label, z - wd / 2, ySize - 5);
+			g.drawLine(z, ySize - WormConstants.footMargin - 2,
+						z, ySize - WormConstants.footMargin + 2);
+			g.drawLine(z, WormConstants.footMargin - 2,
+						z, WormConstants.footMargin + 2);
+			z = WormConstants.footMargin +
+						i * (ySize - 2 * WormConstants.footMargin) / 10;
+			label = Format.d(ymax - i * (ymax - ymin) / 10, 1);		// ylabel
+			wd = fm.stringWidth(label);
+			g.drawString(label, WormConstants.sideMargin - wd - 5, z + ht / 2);
+			g.drawLine(WormConstants.sideMargin - 2, z,
+						WormConstants.sideMargin + 2, z);
+			g.drawLine(xSize - WormConstants.sideMargin - 2, z,
+						xSize - WormConstants.sideMargin + 2, z);
+		}
+		int wd = fm.stringWidth(loudnessUnits);
+		g.drawString(loudnessUnits, WormConstants.sideMargin - wd - 5,
+						WormConstants.footMargin + ht / 2);
+		wd = fm.stringWidth(tempoUnits);
+		g.drawString(tempoUnits, xSize - WormConstants.sideMargin - wd / 2,
+						ySize - 5);
+	} // paint()
+
+	public void print() {
+		for (int i = 0; i < WormConstants.wormLength; i++) {
+			int ind = (tail + i) % WormConstants.wormLength;
+			System.out.println(i+" ["+ind+"] = ("+x[ind]+", "+y[ind]+")");
+		}
+		System.out.println("Tail = " + tail);
+	} // print()
+
+	public void setPoints(double[] x1, double[] y1, String[] flags,
+							int start, int len) {
+		if (start < len - WormConstants.wormLength)
+			start = len - WormConstants.wormLength;
+	//	int bar = 0;
+	//	int beat = 0;
+	//	int track = 0;
+		int i = start - 10;	// try 10 steps back for smoothing
+		if (i < 0)
+			i = 0;
+		double smoothx = x1[i];
+		double smoothy = y1[i];
+		double decay = 0.95;
+	//	System.err.println("*************************************************");
+		for ( ; i < len; i++) {
+		//	System.err.println(i + ": " + x1[i] + " " + y1[i] + " " + flags[i]);
+		//	if ((flags[i] & WormFile.BAR) != 0)
+		//		bar++;
+		//	if ((flags[i] & WormFile.BEAT) != 0)
+		//		beat++;
+		//	if ((flags[i] & WormFile.TRACK) != 0)
+		//		track++;
+			smoothx = smoothx * decay + x1[i] * (1 - decay);
+			smoothy = smoothy * decay + y1[i] * (1 - decay);
+			if (i >= start) {
+				tail = i % WormConstants.wormLength;
+				x[tail] = smoothx;
+				y[tail] = smoothy;
+		//		labels[tail] = bar + ":" + beat + ":" + track + ":" +
+				labels[tail] = flags[i] + Format.d(i * framePeriod, 1);
+			}
+		}
+		xCount = len;
+		repaint();
+	} // setPoints()
+	
+	void addPoint(double newx, double newy, String theLabel) {
+		if ((smoothMode == NONE) || (memSize == 0)) {	// no smooth or 1st pt
+			xmem[0] = newx;
+			ymem[0] = newy;
+			memSize = 1;
+		} else if (smoothMode == EXPONENTIAL) { // exp decaying average (IIR)
+			xmem[0] = xDecay * xmem[0] + (1 - xDecay) * newx;
+			ymem[0] = yDecay * ymem[0] + (1 - yDecay) * newy;
+			memSize = 1;
+			newx = xmem[0];
+			newy = ymem[0];
+		} else if (smoothMode == HALF_GAUSS) {	// Gaussian smoothing
+			double r = 120 / newx / framePeriod;		// 2 beats == half bar??
+			int k = (int)Math.ceil(4 * r);	// cut off values <<< 0.01
+			if (memSize == WormConstants.wormLength)
+				memSize--;
+			for (int i = memSize; i > 0; i--) {
+				xmem[i] = xmem[i-1];
+				ymem[i] = ymem[i-1];
+			}
+			xmem[0] = newx;
+			ymem[0] = newy;
+			memSize++;
+			double xTotal = 0;
+			double yTotal = 0;
+			double eTotal = 0;
+			for (int i = 0; (i < memSize) && (i <= k); i++) {
+				double e = (double)i / r;
+				e = Math.exp(-e * e / 2);
+				xTotal += e * xmem[i];
+				yTotal += e * ymem[i];
+				eTotal += e;
+			}
+			if (eTotal != 0) {
+				xTotal /= eTotal;
+				yTotal /= eTotal;
+			}
+			newx = xTotal;
+			newy = yTotal;
+		} else {	// (smoothMode == FULL_GAUSS) {}  // Retrospective smoothing
+			double r = 120 / newx / framePeriod;		// 2 beats == half bar??
+			int k = (int)Math.ceil(3 * r);	// cut off values << 0.01
+			if (k > memSize / 2)
+				k = memSize / 2;
+			if (memSize == WormConstants.wormLength)
+				memSize--;
+			for (int i = memSize; i > 0; i--) {
+				xmem[i] = xmem[i-1];
+				ymem[i] = ymem[i-1];
+			}
+			xmem[0] = newx;
+			ymem[0] = newy;
+			memSize++;
+			for (int start = k; start <= 2 * k; start++) {
+				double xTotal = 0;
+				double yTotal = 0;
+				double eTotal = 0;
+				for (int i = start - k; (i < memSize) && (i <= 2 * k); i++) {
+					double e = (double)(i - start) / r;
+					e = Math.exp(-e * e / 2);
+					xTotal += e * xmem[2 * k - i];
+					yTotal += e * ymem[2 * k - i];
+					eTotal += e;
+				}
+				if (eTotal != 0) {
+					xTotal /= eTotal;
+					yTotal /= eTotal;
+				}
+				newx = xTotal;
+				newy = yTotal;
+				int index = (tail - (2 * k - start) +
+						WormConstants.wormLength) % WormConstants.wormLength;
+				x[index] = newx;
+				y[index] = newy;
+			}
+		}
+	// Calculate x-axis bounds (with decaying average +-10%) and rescale
+		xSum = xSum * sDecay + newx;
+		xCount++;
+		if (autoScaleMode && (
+				(newx < xmin) || (newx > xmax) ||
+				(xmin > xSum * (1 - sDecay) * 0.97) ||
+				(xmax < xSum * (1 - sDecay) * 1.03))) {
+			autoScale();
+		}
+	// Update for all methods
+		x[tail] = newx;
+		y[tail] = newy;
+		labels[tail] = theLabel;
+		tail = (tail + 1) % WormConstants.wormLength;
+	} // addPoint()
+
+	public void ancestorMoved(HierarchyEvent e) {}
+
+	public void ancestorResized(HierarchyEvent e) {
+		if ((xSize == getWidth()) && (ySize == getHeight()))
+			return;
+		xSize = getWidth();
+		ySize = getHeight();
+		xRescale(xmin, xmax);
+		yRescale(ymin, ymax);
+	} // ancestorResized()
+
+	void xMoveRight(boolean right) {
+		setAutoScaleMode(false);
+		double diff = (xmax - xmin) / (right? -2: 2);
+		// if (xmin < diff)
+		// 	diff = xmin;
+		xRescale(xmin - diff, xmax - diff);
+	} // xMoveRight()
+
+	void xZoom(boolean in) {
+		setAutoScaleMode(false);
+		double diff = (xmax - xmin) / (in? -4: 2);
+		// if (xmin < diff)
+		// 	xRescale(0, xmax + diff);
+		// else
+		xRescale(xmin - diff, xmax + diff);
+	} // xZoom()
+	
+	void xRescale(double min, double max) {
+		xSize = getWidth();
+		xmin = min;
+		xmax = max;
+		xScale = (double)(xSize - 2 * WormConstants.sideMargin) / (xmax - xmin);
+		repaint();
+	} // xRescale()
+	
+	void yRescale(double min, double max) {
+		ySize = getHeight();
+		ymin = min;
+		ymax = max;
+		yScale = (double)(ySize - 2 * WormConstants.footMargin) / (ymax - ymin);
+		repaint();
+	} // yRescale()
+
+	void yMoveDown(boolean down) {
+		double diff = (ymax - ymin) / (down? 4: -4);
+		// if (ymin < diff)
+		// 	diff = ymin;
+		yRescale(ymin - diff, ymax - diff);
+	} // yMoveDown()
+
+	void yZoom(boolean in) {
+		double diff = (ymax - ymin) / (in? -4: 2);
+		// if (ymin < diff)
+		// 	yRescale(0, ymax + diff);
+		// else
+		yRescale(ymin - diff, ymax + diff);
+	} // yZoom()
+
+	void setAutoScaleMode(boolean set) {
+		autoScaleMode = set;
+		if (autoScaleMode)
+			autoScale();
+		if (autoIndicator != null)
+			autoIndicator.setSelected(autoScaleMode);
+	} // setAutoScaleMode()
+
+	void autoScale() {
+		double factor = 0.1;
+		xRescale(xSum * (1-sDecay) * (1-factor),xSum * (1-sDecay) * (1+factor));
+	} // autoScale()
+
+	public static void main(String[] args) {
+		createInFrame(args);
+	} // main()
+
+	public static Worm createInFrame(String[] args) {
+		JFrame f = new JFrame(WormConstants.title);
+		Worm w = new Worm(f);
+		for (int i = 0; i < args.length; i++) {
+			if (args[i].startsWith("-d")) {
+				if (args[i].length() == 2)
+					AudioWorm.fileDelay = Integer.parseInt(args[++i]);
+				else
+					AudioWorm.fileDelay =Integer.parseInt(args[i].substring(2));
+			} else if (args[i].endsWith(".wav") || args[i].endsWith(".mp3"))
+				w.setInputFile(args[i]);
+			else if (args[i].endsWith(".match"))
+				w.setMatchFile(args[i]);
+			else if (args[i].endsWith(".worm"))
+				w.setWormFile(args[i]);
+			else
+				w.setTimingOffsetString(args[i]);
+		}
+		f.getContentPane().setBackground(WormConstants.backgroundColor);
+		f.getContentPane().setLayout(new BoxLayout(f.getContentPane(),
+					BoxLayout.Y_AXIS));
+		f.getContentPane().add(w);
+		f.getContentPane().add(new WormScrollBar(w));
+		f.getContentPane().add(new WormControlPanel(w));
+		w.addHierarchyBoundsListener(w);
+		// or f.getContentPane().addHier... -- both seem to work the same
+		Dimension borderSize = FrameMargins.get(false);
+		f.setSize(w.getWidth() + borderSize.width,
+				  w.getHeight() + borderSize.height + WormConstants.cpHeight);
+		GraphicsConfiguration gc = f.getGraphicsConfiguration();
+		Rectangle bounds = gc.getBounds();
+		f.setLocation(bounds.x + (bounds.width - f.getWidth()) / 2,
+					  bounds.y + (bounds.height - f.getHeight()) / 2);
+		f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+		f.setVisible(true);
+		f.setIconImage(WormIcon.getWormIcon(1,f));
+		if (args.length > 0)
+			w.play();
+		return w;
+	} // createInFrame()
+
+} // class Worm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/worm/WormConstants.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,86 @@
+/*  Performance Worm: Visualisation of Expressive Musical Performance
+	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.worm;
+
+import java.awt.Color;
+import at.ofai.music.util.Colors;
+
+class WormConstants implements Colors {
+public static String version = " 1.4-RC2 "; // DON'T EDIT THIS LINE; see make.sh
+	public static String title = "Performance Worm v" + version.substring(1) +
+								 "(c) 2002 ofaiMusic <simon@ofai.at>";
+	public static int X_SZ = 800;		// Default x-size of Worm Panel
+	public static int Y_SZ = 500;		// Default y-size of Worm Panel
+	public static int cpHeight = 50;	// Height of control panel
+	public static int footMargin = 20;	// Distance from x-axis to top/bottom
+	public static int sideMargin = 40;	// Distance from y-axis to sides
+	public static int wormLength = 300;	// Number of points in the worm
+	public static Color buttonTextColor = Color.black;
+	public static Color buttonColor = Color.white;
+	public static Color axesColor = Color.black;
+	public static Color backgroundColor = Color.white;
+	public static Color wormHeadColor = Color.red;
+	public static Color wormTailColor = Color.white;// new Color(255,240,240);
+	public static Color wormHeadRimColor = Color.black;
+	public static Color wormTailRimColor = Color.white;
+	public static Color wormFaceColor = Color.black;
+	public static Color altFaceColor = Color.white;
+	// BROWN version: new Color(255, 200, 160);
+
+	public Color getBackground() { return backgroundColor; }
+	public Color getForeground() { return axesColor; }
+	public Color getButton() { return buttonColor; }
+	public Color getButtonText() { return buttonTextColor; }
+	
+	public static void setDayColours() {
+		buttonTextColor = Color.black;
+		buttonColor = Color.white;
+		axesColor = Color.black;
+		backgroundColor = Color.white;
+		wormHeadColor = Color.red;
+		wormTailColor = Color.white;
+		wormHeadRimColor = Color.black;
+		wormTailRimColor = Color.white;
+		wormFaceColor = Color.black;
+		altFaceColor = Color.white;
+	} // setDayColours()
+
+	public static void setNightColours() {
+		buttonTextColor = Color.yellow;
+		buttonColor = Color.black;
+		axesColor = Color.yellow;
+		backgroundColor = Color.black;
+		wormHeadColor = Color.green;
+		wormTailColor = Color.black;
+		wormHeadRimColor = Color.yellow;
+		wormTailRimColor = Color.black;
+		wormFaceColor = Color.black;
+		altFaceColor = Color.black;
+	} // setNightColours()
+	
+	public static void setGlow(boolean flag) {
+		if (flag)
+			setNightColours();
+		else
+			setDayColours();
+	} // setGlow()
+
+} // class WormConstants
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/worm/WormControlPanel.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,188 @@
+/*  Performance Worm: Visualisation of Expressive Musical Performance
+	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.worm;
+
+import javax.swing.ButtonGroup;
+import javax.swing.JButton;
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JMenuItem;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JRadioButtonMenuItem;
+
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.HierarchyBoundsListener;
+import java.awt.event.HierarchyEvent;
+
+import at.ofai.music.util.PSPrinter;
+
+
+class WormControlPanel extends JPanel
+					   implements ActionListener, HierarchyBoundsListener {	
+
+	static final long serialVersionUID = 0;
+	protected Worm worm;
+	protected JPopupMenu flagMenu;
+	protected JRadioButtonMenuItem[] rb;
+	protected String[] buttonText = {
+	"<<x>>", "<<y>>", "^^^", "<<<", "ML >>", "Smooth", "Flags", "Play","Stop",
+	">>x<<", ">>y<<", "vvv", ">>>", "<< ML", "Header", "Load", "Save","Quit"};
+	
+	public WormControlPanel(Worm w) {
+		worm = w;
+		setBackground(WormConstants.buttonColor);
+		setLayout(new GridLayout(2, buttonText.length / 2));
+		for (int i = 0; i < buttonText.length; i++) {
+			JButton theButton = new JButton(buttonText[i]);
+			theButton.setBackground(WormConstants.buttonColor);
+			theButton.setForeground(WormConstants.buttonTextColor);
+			theButton.addActionListener(this);
+			add(theButton);
+			if (buttonText[i].equals("Flags")) {
+				flagMenu = new JPopupMenu(buttonText[i]);
+				JCheckBoxMenuItem scale = new JCheckBoxMenuItem("AutoScale");
+				scale.setBackground(WormConstants.buttonColor);
+				scale.setForeground(WormConstants.buttonTextColor);
+				scale.setSelected(true);
+				scale.addActionListener(this);
+				worm.setAutoButton(scale);
+				flagMenu.add(scale);
+				flagMenu.addSeparator();
+				JCheckBoxMenuItem glow = new JCheckBoxMenuItem("Glow Worm");
+				glow.setBackground(WormConstants.buttonColor);
+				glow.setForeground(WormConstants.buttonTextColor);
+				glow.setSelected(false);
+				glow.addActionListener(this);
+				flagMenu.add(glow);
+				flagMenu.addSeparator();
+				JCheckBoxMenuItem plots = new JCheckBoxMenuItem("Histograms");
+				plots.setBackground(WormConstants.buttonColor);
+				plots.setForeground(WormConstants.buttonTextColor);
+				plots.setSelected(false);
+				plots.addActionListener(this);
+				flagMenu.add(plots);
+				flagMenu.addSeparator();
+				ButtonGroup bg = new ButtonGroup();
+				rb = new JRadioButtonMenuItem[Worm.smoothLabels.length];
+				for (int j = 0; j < Worm.smoothLabels.length; j++) {
+					rb[j] = new JRadioButtonMenuItem(Worm.smoothLabels[j]);
+					rb[j].setBackground(WormConstants.buttonColor);
+					rb[j].setForeground(WormConstants.buttonTextColor);
+					rb[j].addActionListener(this);
+					flagMenu.add(rb[j]);
+					bg.add(rb[j]);
+				}
+				rb[w.getSmoothMode()].setSelected(true);
+				worm.setSmoothButtons(rb);
+				flagMenu.setInvoker(theButton);
+				flagMenu.addSeparator();
+				JMenuItem printButton = new JMenuItem("Print300");
+				printButton.setBackground(WormConstants.buttonColor);
+				printButton.setForeground(WormConstants.buttonTextColor);
+				printButton.addActionListener(this);
+				flagMenu.add(printButton);
+				JMenuItem printButton2 = new JMenuItem("Print600");
+				printButton2.setBackground(WormConstants.buttonColor);
+				printButton2.setForeground(WormConstants.buttonTextColor);
+				printButton2.addActionListener(this);
+				flagMenu.add(printButton2);
+			} else if (buttonText[i].equals("Play"))
+				worm.setPlayButton(theButton);
+		}
+		setSize(w.getWidth(), WormConstants.cpHeight);
+		setMaximumSize(new Dimension(w.getWidth(), WormConstants.cpHeight));
+		addHierarchyBoundsListener(this);
+		worm.setControlPanel(this); // for callback
+	} // constructor
+	
+	public void actionPerformed(ActionEvent e) {
+		if (e.getActionCommand().equals("<<x>>"))
+			worm.xZoom(true);
+		else if (e.getActionCommand().equals(">>x<<"))
+			worm.xZoom(false);
+		else if (e.getActionCommand().equals("<<<"))
+			worm.xMoveRight(false);
+		else if (e.getActionCommand().equals(">>>"))
+			worm.xMoveRight(true);
+		else if (e.getActionCommand().equals("ML >>"))
+			worm.audio.ti.switchLevels(true);
+		else if (e.getActionCommand().equals("<< ML"))
+			worm.audio.ti.switchLevels(false);
+		else if (e.getActionCommand().equals("Play") ||
+					e.getActionCommand().equals("Cont"))
+			worm.play();
+		else if (e.getActionCommand().equals("Pause"))
+			worm.pause();
+		else if (e.getActionCommand().equals("Stop"))
+			worm.stop();
+		else if (e.getActionCommand().equals("<<y>>"))
+			worm.yZoom(true);
+		else if (e.getActionCommand().equals(">>y<<"))
+			worm.yZoom(false);
+		else if (e.getActionCommand().equals("vvv"))
+			worm.yMoveDown(true);
+		else if (e.getActionCommand().equals("^^^"))
+			worm.yMoveDown(false);
+		else if (e.getActionCommand().equals("Header")) {
+			worm.editParameters();
+		} else if (e.getActionCommand().equals("Smooth"))
+			worm.smooth();
+		else if (e.getActionCommand().equals("Flags")) {
+			flagMenu.setVisible(true);
+			flagMenu.setLocation(
+						((Component)e.getSource()).getLocationOnScreen());
+		} else if (e.getActionCommand().equals("Load")) {
+			new WormLoadDialog(worm);
+		} else if (e.getActionCommand().equals("Save")) {
+			worm.save(new MyFileChooser().browseSave());
+		} else if (e.getActionCommand().equals("Quit"))
+		    System.exit(0);
+		else if (e.getActionCommand().equals("AutoScale"))
+			worm.setAutoScaleMode(
+							((JCheckBoxMenuItem)e.getSource()).isSelected());
+		else if (e.getActionCommand().equals("Glow Worm"))
+			worm.setGlow(((JCheckBoxMenuItem)e.getSource()).isSelected());
+		else if (e.getActionCommand().equals("Histograms"))
+			TempoInducer.plotFlag =
+							((JCheckBoxMenuItem)e.getSource()).isSelected();
+		else if (e.getActionCommand().equals("Print300"))
+			PSPrinter.print(worm, 300);
+		else if (e.getActionCommand().equals("Print600"))
+			PSPrinter.print(worm, 600);
+		else
+			for (int i = 0; i < Worm.smoothLabels.length; i++)
+				if (e.getActionCommand().equals(Worm.smoothLabels[i]))
+					worm.setSmoothMode(i);
+	} // actionPerformed
+
+	public void ancestorMoved(HierarchyEvent e) {}
+
+	public void ancestorResized(HierarchyEvent e) {	
+		setMaximumSize(new Dimension(worm.getWidth(), WormConstants.cpHeight));
+		setSize(worm.getWidth(), WormConstants.cpHeight);
+		repaint();
+	} // ancestorResized()
+
+} // class WormControlPanel
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/worm/WormFile.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,325 @@
+/*  Performance Worm: Visualisation of Expressive Musical Performance
+	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.worm;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.PrintStream;
+import java.io.FileReader;
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.StringTokenizer;
+import java.util.Iterator;
+import java.awt.Frame;
+import at.ofai.music.util.Event;
+import at.ofai.music.util.EventList;
+import at.ofai.music.util.Format;
+import at.ofai.music.util.MatchTempoMap;
+
+// Read/write performance worm data
+public class WormFile {
+
+	Worm worm;
+	double outFramePeriod, inFramePeriod;
+	int length;
+	double[] time;
+	double[] inTempo, outTempo;
+	double[] inIntensity, outIntensity;
+	int[] inFlags, outFlags;
+	String[] label;
+	public static final int
+		TRACK=1, BEAT=2, BAR=4, SEG1=8, SEG2=16, SEG3=32, SEG4=64;
+	public static final double defaultFramePeriod = 0.1;	// 10 FPS
+	WormParameters info;
+
+	private WormFile(Frame f) {
+		info = new WormParameters(f);
+		inFramePeriod = defaultFramePeriod;
+		outFramePeriod = defaultFramePeriod;
+	} // shared constructor
+
+	public WormFile(int size) {
+		this(null);
+		length = size;
+		init();
+	} // constructor
+
+	public WormFile(int size, double step) {
+		this(size);
+		inFramePeriod = step;
+	} // constructor
+
+	public WormFile(EventList el, double step) {
+		this(null);
+		inFramePeriod = step;
+		convertList(el);
+	} // constructor
+
+	public WormFile(Worm w, EventList el) {
+		this(w == null? null: w.theFrame);
+		worm = w;
+		convertList(el);
+	} // constructor
+
+	public WormFile(Worm w, String fileName) {
+		this(w.theFrame);
+		worm = w;
+		read(fileName);
+	} // constructor
+
+	public void init() {
+		inTempo = new double[length];
+		inIntensity = new double[length];
+		inFlags = new int[length];
+		time = new double[length];
+	} // init()
+
+	public void smooth(int mode, double left, double right, int smoothLevel) {
+		if (worm != null)
+			worm.setSmoothMode(Worm.NONE);
+		info.smoothing = "None";
+		if ((outFramePeriod == 0) || ((inFramePeriod == 0) && (time == null))) {
+			System.err.println("Error: smooth() frameLength unspecified");
+			return;
+		}
+		if (inFramePeriod != 0) {
+			for (int i = 0; i < length; i++)
+				time[i] = inFramePeriod * i;
+		}
+		int outLength = 1+(int) Math.ceil(time[time.length-1] / outFramePeriod);
+		if ((outTempo == null) || (outTempo.length != outLength)) {
+			outTempo = new double[outLength];
+			outIntensity = new double[outLength];
+			outFlags = new int[outLength];
+			label = new String[outLength];
+		}
+		if (mode == Worm.NONE) {
+			int i = 0, o = 0;
+			while (o * outFramePeriod < time[0]) {
+				outTempo[o] = inTempo[0];
+				outIntensity[o] = inIntensity[0];
+				o++;
+			}
+			for ( ; i < time.length - 1; i++) {
+				while (o * outFramePeriod < time[i+1]) {
+					outTempo[o] = inTempo[i];
+					outIntensity[o] = inIntensity[i];
+					o++;
+				}
+			}
+			while (o < outLength) {
+				outTempo[o] = inTempo[i];
+				outIntensity[o] = inIntensity[i];
+				o++;
+			}
+		} else {
+			info.smoothing = "Gaussian" + "\t" + Format.d(left, 4) +
+									 "\t" + Format.d(right, 4);
+			if (smoothLevel != 0) {
+				int count = 0;
+				double first = 0, last = 0;
+				for (int i = 0; i < time.length; i++)
+					if ((inFlags[i] & smoothLevel) != 0) {
+						if (count == 0)
+							first = time[i];
+						else
+							last = time[i];
+						count++;
+					}
+				if (count < 2)
+					System.err.println("Warning: Beat data not available");
+				else {
+					double IBI = (last - first) / (count - 1); 
+					left *= IBI;
+					right *= IBI;
+					info.smoothing += "\t" +Format.d(IBI,4) + "\t" +smoothLevel;
+					System.out.println("Smoothing parameters (seconds): pre=" +
+							Format.d(left,3) + " post=" + Format.d(right,3));
+				}
+			}
+			int start = 0;
+			for (int o = 0; o < outLength; o++) {
+				double sum = 0, val = 0, tempo = 0, intensity = 0;
+				for (int i = start; i < time.length; i++) {
+					double d = o * outFramePeriod - time[i];
+					if (d > 4 * left) {	// average over 4 stddevs
+						start++;
+						continue;
+					}
+					if (d < -4 * right)
+						break;
+					if (d < 0)
+						val = Math.exp(-d*d/(left*left*2));
+					else
+						val = Math.exp(-d*d/(right*right*2));
+					sum += val;
+					tempo += val * inTempo[i];
+					intensity += val * inIntensity[i];
+				}
+				if (sum == 0) {		// assume this only occurs at beginning
+					outTempo[o] = inTempo[0];
+					outIntensity[o] = inIntensity[0];
+				} else {
+					outTempo[o] = tempo / sum;
+					outIntensity[o] = intensity / sum;
+				}
+			}
+		}
+		for (int i = 0; i < outFlags.length; i++)
+			outFlags[i] = 0;
+		for (int i = 0; i < inFlags.length; i++)
+			outFlags[(int)Math.round(time[i] / outFramePeriod)] |= inFlags[i];
+		int bar = 0;
+		int beat = 0;
+		int track = 0;
+		for (int i = 0; i < outFlags.length; i++) {
+			if ((outFlags[i] & BAR) != 0)
+				bar++;
+			if ((outFlags[i] & BEAT) != 0)
+				beat++;
+			if ((outFlags[i] & TRACK) != 0)
+				track++;
+			label[i] = bar + ":" + beat + ":" + track + ":" +
+						Format.d(i * outFramePeriod, 1);
+		}
+	} // smooth()
+	
+	public void editParameters() {
+		info.editParameters();
+		update();
+	} // editParameters()
+
+	public void update() {
+		length = info.length;
+		inFramePeriod = info.framePeriod;
+		worm.setTitle(info.composer + ", " + info.piece +
+						", played by " + info.performer);
+		// not used (?) : beatLevel trackLevel upbeat beatsPerBar
+		if ((inTempo == null) || (inTempo.length != length))
+			init();
+		worm.setInputFile(info.audioPath, info.audioFile);
+		worm.setSmoothMode(Worm.NONE);
+		if (info.axis.length() > 0)
+			worm.setAxis(info.axis);
+		worm.setFramePeriod(outFramePeriod);
+		worm.setLoudnessUnits(info.loudnessUnits);
+	} // update()
+
+	public void convertList(EventList el) {
+		double tMax = 0;
+		int count = 0;
+		for (Iterator<Event> i = el.iterator(); i.hasNext(); ) {
+			double pedalUpTime = i.next().pedalUp;
+			if (pedalUpTime > tMax)
+				tMax = pedalUpTime;
+			count++;
+		}
+		length = (int)Math.ceil(tMax / inFramePeriod);
+		init();
+		// double[] decayFactor = new double[128];
+		// for (int i = 0; i < 128; i++)
+		// 	decayFactor[i] = Math.max(5.0, (i - 6.0) / 3.0) * inFramePeriod;
+		// 	// was Math.pow(0.1, inFramePeriod);	// modify for pitch?
+		for (Iterator<Event> i = el.l.iterator(); i.hasNext(); ) {
+			Event e = i.next();
+			double loudness = 30.29 * Math.pow(e.midiVelocity, 0.2609);
+			loudness += (e.midiPitch - 66.0) / 12.0; // +1dB / oct
+			int start = (int)Math.floor(e.keyDown / inFramePeriod);
+			if (start < 0)
+				start = 0;
+			int stop = (int)Math.ceil((e.pedalUp + 0.5) / inFramePeriod);
+			if (stop > inIntensity.length)
+				stop = inIntensity.length;
+			for (int t = start; t < stop; t++) {
+				if (loudness > inIntensity[t])
+					inIntensity[t] = loudness;
+				loudness -= Math.max(5.0, (e.midiPitch - 6.0) / 3.0) *
+											inFramePeriod;
+				// was: mult by decay factor. But since vals are dB, we subtract
+			}
+		}
+		MatchTempoMap tMap = new MatchTempoMap(count);
+		for (Iterator<Event> i = el.l.iterator(); i.hasNext(); ) {
+			Event e = i.next();
+			tMap.add(e.keyDown, e.scoreBeat);
+		}
+		// el.print();
+		// tMap.print();	// for debugging
+		tMap.dump(inTempo, inFramePeriod);
+	} // convertList()
+
+	public void write(String fileName) {
+		PrintStream out;
+		try {
+			out = new PrintStream(new FileOutputStream(fileName));
+		} catch (FileNotFoundException e) {
+			System.err.println("Unable to open output file " + fileName);
+			return;
+		}
+		info.write(out, outTempo.length, outFramePeriod);
+		for (int i = 0; i < outTempo.length; i++) {
+			if (outFramePeriod == 0)
+				out.print(Format.d(time[i],3) + " ");
+			out.println(Format.d(outTempo[i],4) +" "+
+						Format.d(outIntensity[i],4) +" "+outFlags[i]);
+		}
+		out.close();
+	} // write()
+
+	public void read(String fileName) {
+		try {
+			File f = new File(fileName);
+			if (!f.isFile())	// a local hack for UNC file names under Windows
+				f = new File("//fichte" + fileName);
+			if (!f.isFile())
+				throw(new FileNotFoundException("Could not open " + fileName));
+			BufferedReader in = new BufferedReader(new FileReader(f));
+			String input = info.read(in);
+			update();
+			int index = 0;
+			int bar = 0;
+			while ((input != null) && (index < length)) {
+				StringTokenizer tk = new StringTokenizer(input);
+				if (inFramePeriod != 0)
+					time[index] = Double.parseDouble(tk.nextToken());
+				inTempo[index] = Double.parseDouble(tk.nextToken());
+				inIntensity[index] = Double.parseDouble(tk.nextToken());
+				if (tk.hasMoreTokens())
+					inFlags[index] = Integer.parseInt(tk.nextToken());
+				else
+					inFlags[index] = 0;
+				input = in.readLine();
+				index++;
+			}
+			in.close();
+			smooth(Worm.NONE, 0, 0, 0);
+		} catch (FileNotFoundException e) {
+			System.err.println(e);
+		} catch (IOException e) {
+			System.err.println("IOException reading " + fileName);
+		} catch (Exception e) {
+			System.err.println("Error parsing file " + fileName + ": " + e);
+			e.printStackTrace();
+		}
+	} // read()
+
+} // class WormFile
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/worm/WormIcon.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,125 @@
+/*  Performance Worm: Visualisation of Expressive Musical Performance
+	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.worm;
+
+import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.Image;
+
+class WormIcon {
+	protected int xSize, ySize;
+	protected int[] dx;
+	protected int[] dy;
+
+	public WormIcon(int type) {
+		switch (type) {
+			case 1:
+				xSize = 40;
+				ySize = 40;
+				dx = new int[]{3,-1,0,1,2,4, 4, 2, 1,1,2,4, 4, 2, 1, 0,-1};
+				dy = new int[]{8, 5,5,4,3,2,-3,-4,-5,5,4,3,-2,-3,-4,-5,-5};
+				break;
+			case 2:
+				xSize = 300;
+				ySize = 100;
+				dx = new int[] {
+						10,   4, 3, 2, 0,-2,-3,-4,-3,-2,-1, 0, 0, 1, 1, 1,		// P
+						25,  4, 4, 2,-1,-3,-4,-4,-3,-1, 1, 3, 4, 4, 4,			// e
+						13,  0, 0, 0, 1, 3, 5, 3, 1,							// r
+						13,  0, 0, 0,-4, 8,-4, 0, 1, 3, 5, 3, 1,				// f
+						17, -5,-3,-1, 1, 3, 5, 5, 3, 1,-1,-3,-5,				// o
+						20,  0, 0, 0, 1, 3, 5, 3, 1,							// r
+						10, -1, 0, 1, 2, 4, 3, 1, 1, 3, 4, 2, 1, 0,-1,			// m
+						30, -1, 0,-1,-3,-5,-5,-3,-1, 1, 3, 5, 5, 3, 1, 0, 1,	// a
+						12,  0, 0, 0, 1, 3, 5, 3, 1, 0, 0, 0,					// n
+						28, -3,-5,-5,-3,-1, 1, 3, 5, 5, 3,						// c
+						17,  4, 4, 2,-1,-3,-4,-4,-3,-1, 1, 3, 4, 4, 4,			// e
+						-180,-1, 0, 1, 2, 4, 4, 2, 1, 1, 2, 4, 4, 2, 1, 0,-1,	// W
+						20, -5,-3,-1, 1, 3, 5, 5, 3, 1,-1,-3,-5,				// o
+						20,  0, 0, 0, 1, 3, 5, 3, 1,							// r
+						10, -1, 0, 1, 2, 4, 3, 1, 1, 3, 4, 2, 1, 0,-1			// m
+				};
+				dy = new int[] {
+						20,  0,-2,-3,-5,-3,-2, 0, 2, 3, 4, 4, 4, 4, 5, 5,		// P
+						-8,  0,-1,-3,-3,-2,-1, 2, 3, 4, 5, 4, 2, 0,-1,			// e
+						2,  -4,-4,-4,-3,-1, 0, 1, 3,							// r
+						12, -4,-4,-4, 0, 0,-4,-4,-3,-2, 0, 2, 3,				// f
+						3,   1, 3, 5, 5, 3, 1,-1,-3,-5,-5,-3,-1,				// o
+						16, -4,-4,-4,-3,-1, 0, 1, 3,							// r
+						13, -4,-4,-4,-3, 1, 3, 4,-4,-3,-1, 3, 4, 4, 4,			// m
+						-16, 5, 4, 4, 3, 1,-1,-3,-4,-4,-3,-1, 1, 3, 4, 4, 5,	// a
+						0,  -4,-4,-4,-3,-1, 0, 1, 3, 4, 4, 4,					// n
+						-14,-3,-1, 1, 3, 5, 5, 3, 1,-1,-3,						// c
+						-5,  0,-1,-3,-3,-2,-1, 2, 3, 4, 5, 4, 2, 0,-1,			// e
+						20,  5, 5, 4, 3, 2,-3,-4,-5, 5, 4, 3,-2,-3,-4,-5,-5,	// W
+						4,   1, 3, 5, 5, 3, 1,-1,-3,-5,-5,-3,-1,				// o
+						16, -4,-4,-4,-3,-1, 0, 1, 3,							// r
+						13, -4,-4,-4,-3, 1, 3, 4,-4,-3,-1, 3, 4, 4, 4			// m
+				};
+				break;
+			case 3:
+				xSize = 136;
+				ySize = 48;
+				dx = new int[] {
+						9,  -1, 0, 1, 2, 4, 4, 2, 1, 1, 2, 4, 4, 2, 1, 0,-1,
+						20, -5,-3,-1, 1, 3, 5, 5, 3, 1,-1,-3,-5,
+						20,  0, 0, 0, 1, 3, 5, 3, 1,
+						10, -1, 0, 1, 2, 4, 3, 1, 1, 3, 4, 2, 1, 0,-1
+				};
+				dy = new int[] {
+						12,  5, 5, 4, 3, 2,-3,-4,-5, 5, 4, 3,-2,-3,-4,-5,-5,
+						4,   1, 3, 5, 5, 3, 1,-1,-3,-5,-5,-3,-1,
+						16, -4,-4,-4,-3,-1, 0, 1, 3,
+						13, -4,-4,-4,-3, 1, 3, 4,-4,-3,-1, 3, 4, 4, 4
+				};
+				break;
+		}
+	}
+
+	public static Image getWormIcon(int type, Component c) {
+		return new WormIcon(type).getImage(c);
+	}
+
+	public int getX() { return xSize; }
+	public int getY() { return ySize; }
+
+	public Image getImage(Component c) {
+		Image img = c.createImage(xSize, ySize);
+		Graphics g = img.getGraphics();
+		g.setColor(WormConstants.backgroundColor);
+		g.fillRect(0, 0, xSize, ySize);
+		int x = 0;
+		int y = 0;
+		for (int i = 0; i < dx.length; i++) {
+			x += dx[i];
+			y += dy[i];
+			g.setColor(WormConstants.wormHeadRimColor);
+			g.fillOval(x-1, y-1, 9, 9);
+			g.setColor(WormConstants.wormHeadColor);
+			g.fillOval(x, y, 7, 7);
+		}
+		g.setColor(WormConstants.wormHeadRimColor);
+		g.drawLine(x+2, y+2, x+2, y+2);
+		g.drawLine(x+4, y+2, x+4, y+2);
+		return img;
+	} // getImage()
+	
+} // class WormIcon
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/worm/WormLoadDialog.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,188 @@
+/*  Performance Worm: Visualisation of Expressive Musical Performance
+	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.worm;
+
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.GridLayout;
+import java.awt.TextField;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.TextEvent;
+import java.awt.event.TextListener;
+
+import at.ofai.music.util.FrameMargins;
+
+class WormLoadDialog extends JDialog {
+	
+	static final long serialVersionUID = 0;
+	
+	class MyButton extends JButton {
+		
+		static final long serialVersionUID = 0;
+		public MyButton(String text, ActionListener al) {
+			super(text);
+			setBackground(WormConstants.buttonColor);
+			setForeground(WormConstants.buttonTextColor);
+			addActionListener(al);
+		}
+	} // inner class MyButton
+
+	class MyTextField extends TextField {
+
+		static final long serialVersionUID = 0;
+		public MyTextField(String text, TextListener tl) {
+			super(text);
+			setBackground(WormConstants.buttonColor);
+			setForeground(WormConstants.buttonTextColor);
+			addTextListener(tl);
+		}
+	
+	} // inner class MyTextField
+
+	class MyLabel extends JLabel {
+		
+		static final long serialVersionUID = 0;
+		public MyLabel(String text) {
+			super(text);
+			setForeground(WormConstants.buttonTextColor);
+		}
+
+	} // inner class MyLabel
+
+	Worm worm;
+	TextField inputFileField, matchFileField, wormFileField, timingOffsetField,
+			synchronisationField;
+	JButton inputFileButton, matchFileButton, wormFileButton, cancelButton,
+			okButton;
+	MyFileChooser chooser;
+	
+	public WormLoadDialog(Worm w) {
+		super(w.theFrame, "Input Data", true);
+		worm = w;
+		worm.stop();
+		chooser = new MyFileChooser();
+		inputFileField = new MyTextField(worm.getInputFile(),new TextListener(){
+			public void textValueChanged(TextEvent e) {
+				worm.setInputFile(inputFileField.getText());
+				worm.clearWormFile();
+			}
+		});
+		matchFileField = new MyTextField(worm.getMatchFile(),new TextListener(){
+			public void textValueChanged(TextEvent e) {
+				worm.setMatchFile(matchFileField.getText());
+			}
+		});
+		wormFileField = new MyTextField(worm.getWormFileName(),
+			new TextListener() {
+				public void textValueChanged(TextEvent e) {
+					worm.setWormFile(wormFileField.getText());
+				}
+			}
+		);
+		timingOffsetField = new MyTextField(worm.getTimingOffsetString(),
+			new TextListener() {
+				public void textValueChanged(TextEvent e) {
+					worm.setTimingOffsetString(timingOffsetField.getText());
+				}
+			}
+		);
+		synchronisationField = new MyTextField(worm.getFileDelayString(),
+			new TextListener() {
+				public void textValueChanged(TextEvent e) {
+					worm.setFileDelayString(synchronisationField.getText());
+				}
+			}
+		);
+		inputFileButton = new MyButton("Browse", new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				worm.setInputFile(chooser.browseOpen(inputFileField,
+													MyFileFilter.waveFilter));
+			}
+		});
+		matchFileButton = new MyButton("Browse", new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				worm.setMatchFile(chooser.browseOpen(matchFileField,
+													MyFileFilter.matchFilter));
+			}
+		});
+		wormFileButton = new MyButton("Browse", new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				worm.setWormFile(chooser.browseOpen(wormFileField,
+													MyFileFilter.wormFilter));
+			}
+		});
+		cancelButton = new MyButton("OK", new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				setVisible(false);
+			}
+		});
+		okButton = new MyButton("OK", new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				setVisible(false);
+			}
+		});
+		Container cp = getContentPane();
+		cp.setLayout(null);
+		JPanel p1 = new JPanel(new GridLayout(5,1));
+		JPanel p2 = new JPanel(new GridLayout(5,1));
+		JPanel p3 = new JPanel(new GridLayout(5,1));
+		JLabel inputLabel = new MyLabel("Input file: ");
+		JLabel matchLabel = new MyLabel("Match file: ");
+		JLabel wormLabel = new MyLabel("Worm file: ");
+		JLabel timingOffsetLabel = new MyLabel("Match/Audio Offset: ");
+		JLabel synchronisationLabel = new MyLabel("Synchronisation: ");
+		cp.setBackground(WormConstants.buttonColor);
+		p1.setBackground(WormConstants.buttonColor);
+		p1.add(inputLabel);
+		p2.add(inputFileField);
+		p3.add(inputFileButton);
+		p1.add(matchLabel);
+		p2.add(matchFileField);
+		p3.add(matchFileButton);
+		p1.add(wormLabel);
+		p2.add(wormFileField);
+		p3.add(wormFileButton);
+		p1.add(timingOffsetLabel);
+		p2.add(timingOffsetField);
+		p3.add(cancelButton);
+		p1.add(synchronisationLabel);
+		p2.add(synchronisationField);
+		p3.add(okButton);
+		p1.setBounds(10,5,130,150);
+		p2.setBounds(150,5,500,150);
+		p3.setBounds(660,5,100,150);
+		cp.add(p1);
+		cp.add(p2);
+		cp.add(p3);
+		Dimension d = FrameMargins.get(false);
+		setSize(770 + d.width, 160 + d.height);
+		int x = w.getLocationOnScreen().x + (w.getWidth() - getWidth()) / 2;
+		int y = w.getLocationOnScreen().y + (w.getHeight() - getHeight()) / 2;
+		setLocation(x, y);
+		setVisible(true);
+	} // constructor
+	
+} // class WormLoadDialog
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/worm/WormParameters.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,232 @@
+/*  Performance Worm: Visualisation of Expressive Musical Performance
+	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.worm;
+
+import java.io.PrintStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import at.ofai.music.util.Parameters;
+
+public class WormParameters extends Parameters {
+
+	static final long serialVersionUID = 0;
+	public static final String VERSION = "WORM Version";
+	public static final String FRAMEPERIOD = "FrameLength";
+	public static final String COMPOSER = "Composer";
+	public static final String PIECE = "Piece";
+	public static final String PERFORMER = "Performer";
+	public static final String KEY = "Key";
+	public static final String YEAR = "YearOfRecording";
+	public static final String INDICATION = "Indication";
+	public static final String BEATLEVEL = "BeatLevel";
+	public static final String TRACKLEVEL = "TrackLevel";
+	public static final String STARTBAR = "StartBarNumber";
+	public static final String UPBEAT = "Upbeat";
+	public static final String BEATSPERBAR = "BeatsPerBar";
+	public static final String LENGTH = "Length";
+	public static final String AUDIOPATH = "AudioPath";
+	public static final String AUDIOFILE = "AudioFile";
+	public static final String SMOOTHING = "Smoothing";
+	public static final String AXIS = "Axis";
+	public static final String RESOLUTION = "Time Resolution";
+	public static final String UNITS = "LoudnessUnits";
+	public static final String TEMPOLATE = "TempoLate";
+	public static final String TITLE = "Edit Worm Parameters";
+	public static final String SEP = ":\t";
+	public static final char SEPCHAR = ':';
+
+	protected double framePeriod;
+	protected String trackLevel, loudnessUnits, version, composer, piece,
+				performer, key, year, indication, audioFile, audioPath,
+				smoothing, axis, beatLevel, upbeat, startBar, tempoLate;
+	protected int beatsPerBar, length;
+	
+	public WormParameters(java.awt.Frame f) {
+		super(f, TITLE);
+		composer = "Unknown composer";
+		piece = "unknown piece";
+		performer = "unknown performer";
+		key = "";
+		year = "";
+		indication = "";
+		beatLevel = "1/4";
+		trackLevel = "1.0";
+		upbeat = "0";
+		startBar = "1";
+		beatsPerBar = 4;
+		length = 0;
+		audioFile = "";
+		audioPath = "";
+		smoothing = "";
+		axis = "";
+		version = "1.0";
+		loudnessUnits = "dB";
+		tempoLate = "";
+		framePeriod = WormFile.defaultFramePeriod;
+	} // constructor
+
+	public void editParameters() {
+		editParameters(true);
+	} // editParameters()
+
+	public void editParameters(boolean doEdit) {
+		setString(COMPOSER, composer);
+		setString(PIECE, piece);
+		setString(PERFORMER, performer);
+		setString(KEY, key);
+		setString(YEAR, year);
+		setString(INDICATION, indication);
+		setString(BEATLEVEL, beatLevel);	// e.g. 3/8
+		setString(TRACKLEVEL, trackLevel);
+		setString(UPBEAT, upbeat);
+		setString(STARTBAR, startBar);
+		setInt(BEATSPERBAR, beatsPerBar);
+		setInt(LENGTH, length);
+		setString(AUDIOPATH, audioPath);
+		setString(AUDIOFILE, audioFile);
+		setString(SMOOTHING, smoothing);
+		setString(AXIS, axis);
+		setString(VERSION, version);
+		setDouble(RESOLUTION, framePeriod);
+		setString(UNITS, loudnessUnits);
+		setString(TEMPOLATE, tempoLate);
+		setVisible(doEdit);
+		composer = getString(COMPOSER);
+		piece = getString(PIECE);
+		performer = getString(PERFORMER);
+		key = getString(KEY);
+		year = getString(YEAR);
+		indication = getString(INDICATION);
+		beatLevel = getString(BEATLEVEL);	// e.g. 3/8
+		trackLevel = getString(TRACKLEVEL);
+		upbeat = getString(UPBEAT);
+		startBar = getString(STARTBAR);
+		beatsPerBar = getInt(BEATSPERBAR);
+		length = getInt(LENGTH);
+		audioPath = getString(AUDIOPATH);
+		audioFile = getString(AUDIOFILE);
+		smoothing = getString(SMOOTHING);
+		axis = getString(AXIS);
+		version = getString(VERSION);
+		framePeriod = getDouble(RESOLUTION);
+		loudnessUnits = getString(UNITS);
+		tempoLate = getString(TEMPOLATE);
+	} // editParameters()
+
+	public void write(PrintStream out, int length, double outFramePeriod) {
+		out.println(VERSION + SEP + version);
+		out.println(FRAMEPERIOD + SEP + outFramePeriod);
+		out.println(UNITS + SEP + loudnessUnits);
+		if ((audioPath.length() > 0) && !audioPath.endsWith("/"))
+			audioPath += "/";
+		out.println(AUDIOFILE + SEP + audioPath + audioFile);
+		out.println(SMOOTHING + SEP + smoothing);
+		out.println(COMPOSER + SEP + composer);
+		out.println(PIECE + SEP + piece);
+		out.println(PERFORMER + SEP + performer);
+		out.println(BEATLEVEL + SEP + beatLevel);
+		out.println(TRACKLEVEL + SEP + trackLevel);
+		out.println(UPBEAT + SEP + upbeat);
+		out.println(STARTBAR + SEP + startBar);
+		out.println(BEATSPERBAR + SEP + beatsPerBar);
+		out.println(AXIS + SEP + axis);
+		out.println(TEMPOLATE + SEP + tempoLate);
+		out.println(LENGTH + SEP + length);
+	} // write()
+
+	public String read(BufferedReader in) throws IOException {
+		String input = in.readLine();
+		if (input == null)
+			throw new RuntimeException("Empty input file");
+		if (!input.startsWith("WORM"))
+			throw new RuntimeException("Bad header format: not a WORM file");
+		int delimiter = input.indexOf(SEPCHAR);
+		while (delimiter >= 0) {
+			String attribute = input.substring(0,delimiter).trim();
+			String value = input.substring(delimiter+1).trim();
+			if (attribute.equalsIgnoreCase(VERSION))
+				version = value;
+			else if (attribute.equalsIgnoreCase(FRAMEPERIOD))
+				framePeriod = Double.parseDouble(value);
+			else if (attribute.equalsIgnoreCase(UNITS))
+				loudnessUnits = value;
+			else if (attribute.equalsIgnoreCase(LENGTH))
+				length = Integer.parseInt(value);
+			else if (attribute.equalsIgnoreCase(AUDIOFILE)) {
+				int index = value.lastIndexOf('/');
+				if (index >= 0)
+					audioPath = value.substring(0, index);
+				audioFile = value.substring(index + 1);
+			} else if (attribute.equalsIgnoreCase(SMOOTHING))
+				smoothing = value;
+			else if (attribute.equalsIgnoreCase(COMPOSER))
+				composer = value;
+			else if (attribute.equalsIgnoreCase(PIECE))
+				piece = value;
+			else if (attribute.equalsIgnoreCase(PERFORMER))
+				performer = value;
+			else if (attribute.equalsIgnoreCase(KEY))
+				key = value;
+			else if (attribute.equalsIgnoreCase(INDICATION))
+				indication = value;
+			else if (attribute.equalsIgnoreCase(YEAR))
+				year = value;
+			else if (attribute.equalsIgnoreCase(BEATLEVEL))
+				beatLevel = value;
+			else if (attribute.equalsIgnoreCase(TRACKLEVEL))
+				trackLevel = value;
+			else if (attribute.equalsIgnoreCase(STARTBAR))
+				startBar = value;
+			else if (attribute.equalsIgnoreCase(UPBEAT))
+				upbeat = value;
+			else if (attribute.equalsIgnoreCase(BEATSPERBAR))
+				beatsPerBar = Integer.parseInt(value);
+			else if (attribute.equalsIgnoreCase(AXIS))
+				axis = value;
+			else if (attribute.equalsIgnoreCase(TEMPOLATE))
+				tempoLate = value;
+			else
+				System.err.println("Warning: Unrecognised header data: " +
+									attribute + SEP + value);
+			input = in.readLine();
+			if (input != null)
+				delimiter = input.indexOf(SEPCHAR);
+			else
+				break;
+		}
+		return input;
+	} // read()
+
+	public double getTrackLevel() {
+		try {
+            int i = trackLevel.indexOf("/");
+			if (i >= 0)
+				return Double.parseDouble(trackLevel.substring(0,i)) /
+						Double.parseDouble(trackLevel.substring(i+1));
+			else
+				return Double.parseDouble(trackLevel);
+		} catch (Exception e) {
+			System.err.println("Error getting TrackLevel:\n" + e);
+			return 1;
+		}
+	} // getTrackLevel()
+
+} // WormParameters
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/worm/WormScrollBar.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,66 @@
+/*  Performance Worm: Visualisation of Expressive Musical Performance
+	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.worm;
+
+import javax.swing.JScrollBar;
+import java.awt.Adjustable;
+import java.awt.Dimension;
+
+class WormScrollBar extends JScrollBar {
+
+	static final long serialVersionUID = 0;
+	
+	protected Worm worm;
+
+	public WormScrollBar(Worm w) {
+		super(Adjustable.HORIZONTAL, 0, 10, 0, 1010);
+		// setOrientation(Adjustable.HORIZONTAL);
+		// setVisibleAmount(...);
+		// setMinimum(...);
+		// setMaximum(...);
+		// setValue(...);
+		setUnitIncrement(10);
+		setBlockIncrement(100);
+		worm = w;
+		setBackground(WormConstants.buttonColor);
+		setPreferredSize(new Dimension(w.getWidth(), 17));
+		worm.setScrollBar(this);
+	} // constructor
+
+	public void setValue(int value) {
+		if (worm.audio != null)
+			worm.audio.skipTo(value);
+		super.setValue(value);
+	} // setValue()
+
+	public void setValueNoFeedback(int value) { super.setValue(value); }
+
+	// // MouseListener Interface
+	// public void mouseClicked(MouseEvent e) {}
+	// public void mouseEntered(MouseEvent e) {}
+	// public void mouseExited(MouseEvent e) {}
+	// public void mousePressed(MouseEvent e) {}
+	// public void mouseReleased(MouseEvent e) {}
+	// 
+	// // AdjustmentListener interface
+	// public void adjustmentValueChanged(AdjustmentEvent e) {}
+
+} // class WormScrollBar
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/at/ofai/music/worm/WormSmoothDialog.java	Fri Oct 08 16:11:06 2010 +0100
@@ -0,0 +1,52 @@
+/*  Performance Worm: Visualisation of Expressive Musical Performance
+	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.worm;
+
+import at.ofai.music.util.Parameters;
+
+class WormSmoothDialog extends Parameters {
+
+	static final long serialVersionUID = 0;
+	
+	public WormSmoothDialog (Worm w, WormFile wf) {
+		super(w.theFrame, "Smoothing parameters");
+		setDouble("Before", 1);
+		setDouble("After", 1);
+		String[] labels = new String[]{"Tracks","Beats","Bars","Seconds"};
+		int[] levels = {WormFile.TRACK, WormFile.BEAT, WormFile.BAR, 0};
+		setChoice("Units", labels, 1);
+		setVisible(true);
+		try {
+			double before = getDouble("Before");
+			double after = getDouble("After");
+			String smoothLevel = getChoice("Units");
+			int smoothIndex = 0;
+			for (int i = 0; i < labels.length; i++)
+				if (smoothLevel.equals(labels[i]))
+					smoothIndex = i;
+			if ((after > 0) && (before > 0))
+				wf.smooth(Worm.FULL_GAUSS, before, after, levels[smoothIndex]);
+			else
+				wf.smooth(Worm.NONE, 0, 0, 0);
+		} catch (NumberFormatException e) {}
+	}
+
+} // class WormSmoothDialog