changeset 1:88f50ba37174 tip

* Import MATCH v0.9.4
author Chris Cannam
date Fri, 08 Oct 2010 16:03:47 +0100
parents 9feddf959b6b
children
files at/ofai/music/match/AudioFile.java at/ofai/music/match/GUI.java at/ofai/music/match/MatrixFrame.java at/ofai/music/match/PerformanceMatcher.java at/ofai/music/match/ScrollingMatrix.java
diffstat 5 files changed, 351 insertions(+), 106 deletions(-) [+]
line wrap: on
line diff
--- a/at/ofai/music/match/AudioFile.java	Fri Oct 08 16:02:41 2010 +0100
+++ b/at/ofai/music/match/AudioFile.java	Fri Oct 08 16:03:47 2010 +0100
@@ -85,13 +85,23 @@
 	public int search(int[] arr, int val) {
 		int max = pathLength - 1;
 		int min = 0;
-		while (max > min) {
-			int mid = (max + min) / 2;
-			if (val > arr[mid])
-				min = mid + 1;
-			else
-				max = mid;
-		} // max = MIN_j (arr[j] >= val)   i.e. the first equal or next highest
+		if ((max > min) && (arr[max] > arr[min])) {
+			while (max > min) {
+				int mid = (max + min) / 2;
+				if (val > arr[mid])
+					min = mid + 1;
+				else
+					max = mid;
+			} // max = MIN_j (arr[j] >= val)   i.e. the first equal or next highest
+		} else {	// elements in reverse order (bPath)
+			while (max > min) {
+				int mid = (max + min) / 2;
+				if (val < arr[mid])
+					min = mid + 1;
+				else
+					max = mid;
+			} // max = MIN_j (arr[j] <= val)   i.e. the first equal or next lowest
+		}
 		while ((max + 1 < pathLength) && (arr[max + 1] == val))
 			max++;
 		return (min + max) / 2;
--- a/at/ofai/music/match/GUI.java	Fri Oct 08 16:02:41 2010 +0100
+++ b/at/ofai/music/match/GUI.java	Fri Oct 08 16:03:47 2010 +0100
@@ -30,7 +30,7 @@
 										   MouseMotionListener,
 										   KeyListener {
 
-	public static final String version = "0.9.2";
+	public static final String version = "0.9.4";
 	public static final String title = "MATCH " + version;
 	protected static final int xSize = 230;
 	protected static final int ySize = 150;
--- a/at/ofai/music/match/MatrixFrame.java	Fri Oct 08 16:02:41 2010 +0100
+++ b/at/ofai/music/match/MatrixFrame.java	Fri Oct 08 16:03:47 2010 +0100
@@ -17,6 +17,7 @@
 import at.ofai.music.util.Event;
 import at.ofai.music.util.WormEvent;
 import at.ofai.music.util.PSPrinter;
+import at.ofai.music.worm.WormFile;
 
 class MatrixFrame extends JFrame implements MouseListener,
 											MouseMotionListener,
@@ -34,6 +35,7 @@
 	int bottom;		// size of margin
 	int left;		// size of margin
 	int right;		// size of margin
+	public static boolean useSmoothPath = true;
 	public final Color BACKGROUND = Color.black;
 	public final Color FOREGROUND = Color.green;
 	public final int[] flagsToLength = { 0, 5,10,10,15,15,15,15,
@@ -46,11 +48,13 @@
 		sm = s;
 		bottom = defaultMargin;
 		left = defaultMargin;
-		if (sm.pm2.hasMatchFile)
+		if ((sm.pm2.metadata == PerformanceMatcher.MetaType.MATCH) ||
+				(sm.pm2.metadata == PerformanceMatcher.MetaType.MIDI)) // hasMatchFile)
 			top = matchFileMargin;
 		else
 			top = defaultMargin;
-		if (sm.pm1.hasMatchFile)
+		if ((sm.pm1.metadata == PerformanceMatcher.MetaType.MATCH) ||
+				(sm.pm1.metadata == PerformanceMatcher.MetaType.MIDI)) // hasMatchFile)
 			right = matchFileMargin;
 		else
 			right = defaultMargin;
@@ -62,17 +66,23 @@
 		sm.parent = this;
 		if (sm.pm1.events != null) {
 			eventIterator1 = sm.pm1.events.listIterator();
-			if (sm.pm1.hasWormFile)
+			//if (sm.pm1.hasWormFile)
+			if (sm.pm1.metadata == PerformanceMatcher.MetaType.WORM)
 				generateLabels(eventIterator1);
-			if (sm.pm1.hasMatchFile)
+			//if (sm.pm1.hasMatchFile)
+			if ((sm.pm1.metadata == PerformanceMatcher.MetaType.MATCH) ||
+					(sm.pm1.metadata == PerformanceMatcher.MetaType.MIDI))
 				correctTiming(eventIterator1, sm.pm1.matchFileOffset);
 		} else
 			eventIterator1 = null;
 		if (sm.pm2.events != null) {
 			eventIterator2 = sm.pm2.events.listIterator();
-			if (sm.pm2.hasWormFile)
+			//if (sm.pm2.hasWormFile)
+			if (sm.pm2.metadata == PerformanceMatcher.MetaType.WORM)
 				generateLabels(eventIterator2);
-			if (sm.pm2.hasMatchFile)
+			//if (sm.pm2.hasMatchFile)
+			if ((sm.pm2.metadata == PerformanceMatcher.MetaType.MATCH) ||
+					(sm.pm2.metadata == PerformanceMatcher.MetaType.MIDI))
 				correctTiming(eventIterator2, sm.pm2.matchFileOffset);
 		} else
 			eventIterator2 = null;
@@ -101,13 +111,15 @@
 		int bar = 0;
 		while (it.hasNext()) {
 			WormEvent w = (WormEvent) it.next();
-			if ((w.flags & 4) != 0) {
+			if ((w.flags & WormFile.BAR) != 0) {
 				bar++;
-				if ((w.flags & 8) != 0)
+				if (w.label != null)	// don't overwrite existing labels e.g. sapp data
+					continue;
+				if ((w.flags & WormFile.SEG1) != 0)
 					w.label = "*" + bar;
 				else
 					w.label = "" + bar;
-				if ((w.flags & 16) != 0)
+				if ((w.flags & WormFile.SEG2) != 0)
 					w.label += "*";
 			}
 		}
@@ -180,14 +192,20 @@
 					continue;
 				else if (x > wd - left - right)
 					break;
-				addHMarker(g, e.label, x + left, flagsToLength[e.flags&15]);
+				if ((e.flags & WormFile.BAR) != 0)
+					addHMarker(g, e.label, x + left, flagsToLength[e.flags&15]);
+				else
+					addHMarker(g, null, x + left, flagsToLength[e.flags&15]);
 			}
 		} // addHWorm()
 
 		/** Displays tempo curve based on the OTHER file's WormFile. */
 		protected void addHTempoCurve(Graphics g) {
-			map.setMatch(sm.sPathY, sm.hop1, sm.sPathX, sm.hop2,
+			if (useSmoothPath)
+				map.setMatch(sm.sPathY, sm.hop1, sm.sPathX, sm.hop2,
 						 sm.sPathLength);	// x is reference
+			else	// note bPath is in the opposite order
+				map.setMatch(sm.bPathY, sm.hop1, sm.bPathX, sm.hop2, sm.bPathLength);
 			double t1 = 0;
 			while (eventIterator1.hasPrevious()) {
 				Event e = eventIterator1.previous();
@@ -198,8 +216,11 @@
 			int x1 = (int) Math.round(t1 / sm.hop1) + sm.x0;
 			int y1 = -1;
 			double stop = 0;
-			if (sm.sPathLength > 0)
-				stop = sm.sPathY[sm.sPathLength - 1] * sm.hop1;
+			if (useSmoothPath) {
+				if (sm.sPathLength > 0)
+					stop = sm.sPathY[sm.sPathLength - 1] * sm.hop1;
+			} else if (sm.bPathLength > 0)
+				stop = sm.bPathY[0] * sm.hop1;	// bPath is in reverse order
 			while (eventIterator1.hasNext()) {
 				WormEvent e = (WormEvent) eventIterator1.next();
 				double t2 = map.toReferenceTimeD(e.keyDown);
@@ -216,7 +237,10 @@
 					continue;
 				else if (e.keyDown > stop)
 					break;
-				addHMarker(g, e.label, x2 + left, flagsToLength[e.flags&15]);
+				if ((e.flags & WormFile.BAR) != 0)
+					addHMarker(g, e.label, x2 + left, flagsToLength[e.flags&15]);
+				else
+					addHMarker(g, null, x2 + left, flagsToLength[e.flags&15]);
 				if (y1 >= 0)
 					g.drawLine(x1 + left, y1, x2 + left, y2);
 				t1 = t2;
@@ -298,7 +322,10 @@
 					continue;
 				else if (y < 0)
 					break;
-				addVMarker(g, e.label, y + top, flagsToLength[e.flags&15]);
+				if ((e.flags & WormFile.BAR) != 0)
+					addVMarker(g, e.label, y + top, flagsToLength[e.flags&15]);
+				else
+					addVMarker(g, null, y + top, flagsToLength[e.flags&15]);
 			}
 		} // addVWorm()
 
@@ -361,11 +388,16 @@
 				x += dx;
 			}
 			if (eventIterator2 != null) {
-				if (sm.pm2.hasWormFile)
+				//if (sm.pm2.hasWormFile)
+				if (sm.pm2.metadata == PerformanceMatcher.MetaType.WORM)
 					addHWorm(g);
-				if (sm.pm2.hasMatchFile)
+				//if (sm.pm2.hasMatchFile)
+				if ((sm.pm2.metadata == PerformanceMatcher.MetaType.MATCH) ||
+						(sm.pm2.metadata == PerformanceMatcher.MetaType.MIDI))
 					addHMatch(g);
-			} else if ((eventIterator1 != null) && sm.pm1.hasWormFile) {
+			//} else if ((eventIterator1 != null) && sm.pm1.hasWormFile) {
+			} else if ((eventIterator1 != null) &&
+					(sm.pm1.metadata == PerformanceMatcher.MetaType.WORM)) {
 				// TODO: implement addHTempoCurve and equiv for V
 				// adds tempo curve based on worm
 				addHTempoCurve(g);
@@ -386,9 +418,12 @@
 				y += dy;
 			}
 			if (eventIterator1 != null) {
-				if (sm.pm1.hasWormFile)
+				//if (sm.pm1.hasWormFile)
+				if (sm.pm1.metadata == PerformanceMatcher.MetaType.WORM)
 					addVWorm(g2);
-				if (sm.pm1.hasMatchFile)
+				//if (sm.pm1.hasMatchFile)
+				if ((sm.pm1.metadata == PerformanceMatcher.MetaType.MATCH) ||
+						(sm.pm1.metadata == PerformanceMatcher.MetaType.MIDI))
 					addVMatch(g2);
 			}
 			g2.rotate(Math.PI / 2);
--- a/at/ofai/music/match/PerformanceMatcher.java	Fri Oct 08 16:02:41 2010 +0100
+++ b/at/ofai/music/match/PerformanceMatcher.java	Fri Oct 08 16:03:47 2010 +0100
@@ -1,5 +1,6 @@
 package at.ofai.music.match;
 
+import java.awt.Color;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -19,11 +20,14 @@
 import at.ofai.music.util.Event;
 import at.ofai.music.util.EventList;
 import at.ofai.music.util.Profile;
+import at.ofai.music.util.WormEvent;
+import at.ofai.music.worm.Plot;
 
 /** Represents an audio stream that can be matched to another audio stream of
  *  the same piece of music.  The matching algorithm uses dynamic time warping.
- *  The distance metric is a Euclidean metric on the FFT data with the higher
- *  frequencies mapped onto a linear scale.
+ *  The distance metric is a Euclidean metric on the first difference of the
+ *  magnitude spectrum with the lower frequencies on a linear scale and the
+ *  higher frequencies mapped onto a logarithmic scale.
  */
 public class PerformanceMatcher {
 
@@ -31,7 +35,7 @@
 	 *  The data for the distance metric and the dynamic time warping is shared
 	 *  between the two matchers. In the original version, only one of the two
 	 *  performance matchers contained the distance metric. (See
-	 *  <code>first</code>) */
+	 *  <code>firstPM</code>) */
 	protected PerformanceMatcher otherMatcher;
 
 	/** Indicates which performance is considered primary (the score). This is
@@ -75,11 +79,18 @@
 	 *  the times and velocities of all notes (as recorded by the Boesendorfer
 	 *  SE290). */
 	protected String matchFileName;
-	protected boolean hasWormFile;
-	protected boolean hasMatchFile;
+	protected String outputFileName;
+	protected enum PathType {BACKWARD, FORWARD, SMOOTHED};
+	protected PathType outputType;
+	protected enum MetaType {NONE, MATCH, MIDI, WORM, LABEL};
+	protected MetaType metadata;
+	//protected boolean hasWormFile;
+	//protected boolean hasMatchFile;
+	//protected boolean hasLabelFile;
 	protected EventList events;
 	protected WormHandler wormHandler;
 	protected boolean liveWorm;
+	protected double referenceFrequency;
 	
 	/** Onset time of the first note in the audio file, in order to establish
 	 *  synchronisation between the match file and the audio data. */
@@ -87,7 +98,7 @@
 
 	/** Flag (command line options <b>-n1</b> and <b>-N1</b>) indicating whether
 	 *  or not each frame of audio should be normalised to have a sum of 1.
-	 *  (Default = false). */
+	 *  (Default = true). */
 	protected boolean normalise1;
 	
 	/** Flag (command line options <b>-n2</b> and <b>-N2</b>) indicating whether
@@ -105,9 +116,15 @@
 	/** Flag (command line options <b>-n4</b> and <b>-N4</b>) indicating whether
 	 *  or not the distance metric for pairs of audio frames should be
 	 *  normalised by the log of the sum of the frames.
-	 *  (Default = false; assumes normalise2 == false). */
+	 *  (Default = true; assumes normalise2 == false). */
 	protected boolean normalise4;
 
+	/** Flag (command line options <b>-n5</b> and <b>-N5</b>) indicating whether
+	 *  or not the distance metric for pairs of audio frames should be
+	 *  set to zero between annotated positions.
+	 *  (Default = false). */
+	protected boolean normalise5;
+
 	/** Flag (command line options <b>-d</b> and <b>-D</b>) indicating whether
 	 *  or not the half-wave rectified spectral difference should be used in
 	 *  calculating the distance metric for pairs of audio frames, instead of
@@ -129,7 +146,7 @@
 	/** The size of an FFT frame in seconds, as set by the command line option
 	 *  <b>-f FFTTime</b>. (Default = 0.04644s).  Note that the value is not
 	 *  taken to be precise; it is adjusted so that <code>fftSize</code> is
-	 *  always power of 2. */
+	 *  always a power of 2. */
 	protected double fftTime;
 
 	/** The width of the search band (error margin) around the current match
@@ -223,6 +240,9 @@
 
 	/** The bounds of each row of data in the distance and path cost matrices.*/
 	protected int[] first, last;
+	
+	/** The frames to ignore because they are not annotated. */
+	protected boolean[] ignore;
 
 	/** GUI component which shows progress of alignment. */
 	protected GUI.FileNameSelection progressCallback;
@@ -277,17 +297,23 @@
 		normalise2 = false;
 		normalise3 = false;
 		normalise4 = true;
+		normalise5 = false;
 		useSpectralDifference = true;
 		useChromaFrequencyMap = false;
 		audioOutputRequested = false;
 		scale = 90;
 		maxFrames = 0;	// stop at EOF
 		progressCallback = null;
-		hasMatchFile = false;
-		hasWormFile = false;
+		metadata = MetaType.NONE;
+		//hasMatchFile = false;
+		//hasWormFile = false;
+		//hasLabelFile = false;
 		liveWorm = false;
 		matchFileName = null;
+		outputFileName = null;
+		outputType = PathType.BACKWARD;
 		events = null;
+		referenceFrequency = 440.0;
 	} // default constructor
 
 	/** For debugging, outputs information about the PerformanceMatcher to
@@ -332,14 +358,19 @@
 	public void setMatchFile(String fileName, double tStart, boolean isWorm) {
 		matchFileName = fileName;
 		matchFileOffset = tStart;
-		hasWormFile = isWorm;
-		hasMatchFile = !isWorm;
+		metadata = isWorm? MetaType.WORM: MetaType.MATCH;
+		//hasWormFile = isWorm;
+		//hasMatchFile = !isWorm;
 		try {
 			if (isWorm) {
 				setInputFile(EventList.getAudioFileFromWormFile(matchFileName));
 				events = EventList.readWormFile(matchFileName);
 				if (otherMatcher.wormHandler != null)
 					otherMatcher.wormHandler.init();
+			} else if (fileName.endsWith(".mid")) {
+				events = EventList.readMidiFile(matchFileName);
+				metadata = MetaType.MIDI;
+				//hasMatchFile = false;
 			} else
 				events = EventList.readMatchFile(matchFileName);
 		} catch (Exception e) {
@@ -352,6 +383,59 @@
 		setMatchFile(fileName, tStart, false);
 	} // setMatchFile()
 
+	public void setLabelFile(String fileName) {
+		//hasMatchFile = false;
+		//hasWormFile = true;
+		//hasLabelFile = true;
+		matchFileName = fileName;
+		metadata = MetaType.LABEL;
+		try {
+			events = EventList.readLabelFile(fileName);
+		} catch (Exception e) {
+			System.err.println("Error reading labelFile: " + fileName + "\n"+e);
+			events = null;
+		}
+	} // setLabelFile()
+	
+	public void writeLabelFile(ScrollingMatrix sm) {
+		EventList el = new EventList();
+		AudioFile af = new AudioFile();
+		if (MatrixFrame.useSmoothPath)
+			af.setMatch(sm.sPathX, sm.hop2, sm.sPathY, sm.hop1, sm.sPathLength);
+		else
+			af.setMatch(sm.bPathX, sm.hop2, sm.bPathY, sm.hop1, sm.bPathLength);
+		for (Event ev: otherMatcher.events.l) {
+			WormEvent we = new WormEvent(af.fromReferenceTimeD(ev.keyDown),0,0,0,0);
+			we.label = ((WormEvent)ev).label;
+			el.add(we);
+		}
+		try {
+			el.writeLabelFile(matchFileName);
+		} catch (Exception e) {
+			System.err.println("Unable to write output file: " + e);
+		}
+	} // writeLabelFile()
+	
+	public void writeMidiFile(ScrollingMatrix sm) {
+		EventList el = new EventList();
+		AudioFile af = new AudioFile();
+		if (MatrixFrame.useSmoothPath)
+			af.setMatch(sm.sPathX, sm.hop2, sm.sPathY, sm.hop1, sm.sPathLength);
+		else
+			af.setMatch(sm.bPathX, sm.hop2, sm.bPathY, sm.hop1, sm.bPathLength);
+		for (Event ev: otherMatcher.events.l) {
+			Event copy = ev.clone();
+			copy.keyDown = af.fromReferenceTimeD(ev.keyDown);
+			copy.keyUp = af.fromReferenceTimeD(ev.keyUp);
+			el.add(copy);
+		}
+		try {
+			el.writeMIDI(matchFileName);
+		} catch (Exception e) {
+			System.err.println("Unable to write output file: " + e);
+		}
+	} // writeMidiFile()
+
 	/** Sets up the streams and buffers for live audio input (CD quality).
 	 *  If any Exception is thrown within this method, it is caught, and any
 	 *  opened streams are closed, and <code>pcmInputStream</code> is set to
@@ -426,7 +510,7 @@
 		fftSize = (int) Math.round(Math.pow(2,
 				Math.round( Math.log(fftTime * sampleRate) / Math.log(2))));
 		blockSize = (int) Math.round(blockTime / hopTime);
-		makeFreqMap(fftSize, sampleRate);
+		makeFreqMap(fftSize, sampleRate, referenceFrequency);
 		int buffSize = hopSize * channels * 2;
 		if ((inputBuffer == null) || (inputBuffer.length != buffSize))
 			inputBuffer = new byte[buffSize];
@@ -449,6 +533,12 @@
 		bestPathCost = new int[len][];
 		first = new int[len];
 		last = new int[len];
+		if (normalise5 && (events != null)) { // skip frames without onsets
+			ignore = new boolean[len];
+			Arrays.fill(ignore, true);
+			for (Event e: events.l)			//TODO: check if should be corrected
+				ignore[(int)Math.round(e.keyDown / hopTime)] = false;
+		}
 		for (int i = 0; i < blockSize; i++) {
 			distance[i] = new byte[(MAX_RUN_COUNT+1) * blockSize];
 			bestPathCost[i] = new int[(MAX_RUN_COUNT+1) * blockSize];
@@ -499,12 +589,12 @@
 		}
 	} // closeStreams()
 
-	protected void makeFreqMap(int fftSize, float sampleRate) {
+	protected void makeFreqMap(int fftSize, float sampleRate, double refFreq) {
 		freqMap = new int[fftSize/2+1];
 		if (useChromaFrequencyMap)
-			makeChromaFrequencyMap(fftSize, sampleRate);
+			makeChromaFrequencyMap(fftSize, sampleRate, refFreq);
 		else
-			makeStandardFrequencyMap(fftSize, sampleRate);
+			makeStandardFrequencyMap(fftSize, sampleRate, refFreq);
 	} // makeFreqMap()
 
 	/** Creates a map of FFT frequency bins to comparison bins.
@@ -514,50 +604,53 @@
 	 *  is the energy is summed into the comparison bins. See also
 	 *  processFrame()
 	 */
-	protected void makeStandardFrequencyMap(int fftSize, float sampleRate) {
+	protected void makeStandardFrequencyMap(int fftSize, float sampleRate,
+											double refFreq) {
 		double binWidth = sampleRate / fftSize;
 		int crossoverBin = (int)(2 / (Math.pow(2, 1/12.0) - 1));
-		int crossoverMidi = (int)Math.round(Math.log(crossoverBin*binWidth/440)/
-														Math.log(2) * 12 + 69);
-		// freq = 440 * Math.pow(2, (midi-69)/12.0) / binWidth;
+		int crossoverMidi = (int)Math.round(Math.log(
+					crossoverBin * binWidth / refFreq) / Math.log(2) * 12 + 69);
+		// freq = refFreq * Math.pow(2, (midi-69)/12.0) / binWidth;
 		int i = 0;
 		while (i <= crossoverBin)
-			freqMap[i++] = i;
+			freqMap[i++] = (int)Math.round(i*440/refFreq);
 		while (i <= fftSize/2) {
-			double midi = Math.log(i*binWidth/440) / Math.log(2) * 12 + 69;
+			double midi = Math.log(i*binWidth/refFreq) / Math.log(2) * 12 + 69;
 			if (midi > 127)
 				midi = 127;
 			freqMap[i++] = crossoverBin + (int)Math.round(midi) - crossoverMidi;
 		}
 		freqMapSize = freqMap[i-1] + 1;
-		// System.err.println("Map size: " + freqMapSize +
-		//				   ";  Crossover at: " + crossoverBin);
+		if (!silent) {
+			System.err.println("Map size: " + freqMapSize +
+							   ";  Crossover at: " + crossoverBin);
+			int stopAt = Math.min(freqMap.length, 500);
+			for (i = 0; i < stopAt; i++) // fftSize / 2; i++)
+				System.err.println("freqMap[" + i + "] = " + freqMap[i]);
+			System.err.println("Reference frequency: " + refFreq);
+		}
 	} // makeStandardFrequencyMap()
 
 	// Test whether chroma is better or worse
-	protected void makeChromaFrequencyMap(int fftSize, float sampleRate) {
-		double binWidth = sampleRate / fftSize;
+	protected void makeChromaFrequencyMap(int fftSize,float sR,double refFreq) {
+		double binWidth = sR / fftSize;
 		int crossoverBin = (int)(1 / (Math.pow(2, 1/12.0) - 1));
-		int crossoverMidi = (int)Math.round(Math.log(crossoverBin*binWidth/440)/
-														Math.log(2) * 12 + 69);
-		// freq = 440 * Math.pow(2, (midi-69)/12.0) / binWidth;
+	//	int crossoverMidi = (int)Math.round(Math.log(crossoverBin*binWidth/440)/
+	//													Math.log(2) * 12 + 69);
+		// freq = refFreq * Math.pow(2, (midi-69)/12.0) / binWidth;
 		int i = 0;
 		while (i <= crossoverBin)
 			freqMap[i++] = 0;
 		while (i <= fftSize/2) {
-			double midi = Math.log(i*binWidth/440) / Math.log(2) * 12 + 69;
+			double midi = Math.log(i*binWidth/refFreq) / Math.log(2) * 12 + 69;
 			freqMap[i++] = ((int)Math.round(midi)) % 12 + 1;
 		}
 		freqMapSize = 13;
-	//	System.err.println("Map size: " + freqMapSize +
-	//					   ";  Crossover at: " + crossoverBin);
-	//	for (i = 0; i < fftSize / 2; i++)
-	//		System.err.println("freqMap[" + i + "] = " + freqMap[i]);
 	} // makeChromaFrequencyMap()
 
 	/** Reads a frame of input data, averages the channels to mono, scales
 	 *  to a maximum possible absolute value of 1, and stores the audio data
-	 *  in a circular input buffer.
+	 *  in a circular input buffer. Assumes 16 bit PCM, any number of channels.
 	 *  @return true if a frame (or part of a frame, if it is the final frame)
 	 *  is read. If a complete frame cannot be read, the InputStream is set
 	 *  to null.
@@ -621,9 +714,12 @@
 		return true;
 	} // getFrame()
 
+	Plot plot = null;
+	double[] plotX, plotY;
+
 	/** Processes a frame of audio data by first computing the STFT with a
-	 *  Hamming window, then mapping the frequency bins into a part-linear
-	 *  part-logarithmic array, then (optionally) computing the half-wave
+	 *  Hamming window, then scaling the frequency axis with an arbitrary
+	 *  mapping, then (optionally) computing the half-wave
 	 *  rectified spectral difference from the previous frame, then (optionally)
 	 *  normalising to a sum of 1, then calculating the distance to all frames
 	 *  stored in the otherMatcher and storing them in the distance matrix, and
@@ -661,13 +757,12 @@
 				bestPathCost[frameCount] = bpcOld;
 				bestPathCost[frameCount - blockSize] = bpcNew;
 			}
-			double totalEnergy = 0;
+			double totalEnergy = 0;		// for normalisation
 			if (useSpectralDifference) {
 				for (int i = 0; i < freqMapSize; i++) {
 					totalEnergy += newFrame[i];
 					if (newFrame[i] > prevFrame[i]) {
 						frames[frameIndex][i] = newFrame[i] - prevFrame[i];
-//						totalEnergy += frames[frameIndex][i];
 					} else
 						frames[frameIndex][i] = 0;
 				}
@@ -677,21 +772,28 @@
 					totalEnergy += frames[frameIndex][i];
 				}
 			}
+			if (plot != null) {
+				if (plotX == null) {
+					plotX = new double[freqMapSize];
+					plotY = new double[freqMapSize];
+					for (int j=0; j < freqMapSize; j++)
+						plotX[j] = j;
+					plot.addPlot(plotX, plotY);
+				}
+				for (int i = 0; i < freqMapSize; i++)
+					plotY[i] = frames[frameIndex][i];
+				plot.update();
+			}
 			frames[frameIndex][freqMapSize] = totalEnergy;
 			if (wormHandler != null)
 				wormHandler.addPoint(totalEnergy);
-//	System.err.print(Format.d(max(newFrame),3) + " " +
-//						Format.d(totalEnergy,3));
 			double decay = frameCount >= 200? 0.99:
 						(frameCount < 100? 0: (frameCount - 100) / 100.0);
 			if (ltAverage == 0)
 				ltAverage = totalEnergy;
 			else
 				ltAverage = ltAverage * decay + totalEnergy * (1.0 - decay);
-// System.err.println(Format.d(ltAverage,4) + " " +
-//					Format.d(totalEnergy) + " " +
-//					Format.d(frameRMS));
-			if (frameRMS <= silenceThreshold)
+			if (totalEnergy <= silenceThreshold)		// was frameRMS
 				for (int i = 0; i < freqMapSize; i++)
 					frames[frameIndex][i] = 0;
 			else if (normalise1)
@@ -710,9 +812,13 @@
 			int mn=-1;
 			int mx=-1;
 			for ( ; index < stop; index++) {
+//				tmp = stop - index;
 				int dMN = calcDistance(frames[frameIndex],
 										otherMatcher.frames[index % blockSize]);
-				if (mx<0)
+				if (((ignore != null) && ignore[frameCount]) ||
+						((otherMatcher.ignore != null) && otherMatcher.ignore[index]))
+					dMN /= 4;
+				if (mx < 0)
 					mx = mn = dMN;
 				else if (dMN > mx)
 					mx = dMN;
@@ -789,8 +895,6 @@
 		}
 	} // processFrame()
 
-	// protected double mins = 100, maxs = -100;
-
 	/** Calculates the Manhattan distance between two vectors, with an optional
 	 *  normalisation by the combined values in the vectors. Since the
 	 *  vectors contain energy, this could be considered as a squared Euclidean
@@ -807,27 +911,32 @@
 			d += Math.abs(f1[i] - f2[i]);
 			sum += f1[i] + f2[i];
 		}
-		// System.err.print("   " + Format.d(d,3));
 		if (sum == 0)
 			return 0;
 		if (normalise2)
 			return (int)(scale * d / sum);	// 0 <= d/sum <= 2
 		if (!normalise4)
 			return (int)(scale * d);
-	//	double weight = (5 + Math.log(f1[freqMapSize] + f2[freqMapSize]))/10.0;
 		double weight = (8 + Math.log(sum)) / 10.0;
-		// if (weight < mins) {
-		// 	mins = weight;
-		//	System.err.println(Format.d(mins,3) + " " + Format.d(maxs));
-		// }
-		// if (weight > maxs) {
-		// 	maxs = weight;
-		//	System.err.println(Format.d(mins,3) + " " + Format.d(maxs));
-		// }
 		if (weight < 0)
 			weight = 0;
 		else if (weight > 1)
 			weight = 1;
+//		if ((frameCount > 1000) && (tmp < 50) && (d/sum < 0.8)) {
+//			double[] x = new double[f1.length];
+//			for (int i=0; i < x.length; i++)
+//				x[i] = i;
+//			Plot p = new Plot();
+//			p.addPlot(x, f1, Color.blue);
+//			p.setLength(0, freqMapSize-2);
+//			p.addPlot(x, f2, Color.red);
+//			p.setLength(1, freqMapSize-2);
+//			p.fitAxes();
+//			p.setTitle(String.format("%5d %5d  %5.3f %5.3f %5.3f  %5.3f  %5.3f\n",
+//					frameCount, tmp, d, sum, weight, d/sum, d/sum*weight));
+//			try{System.in.read();}catch(Exception e){}
+//			p.close();
+//		}
 		return (int)(scale * d / sum * weight);
 	} // calcDistance()
 
@@ -922,8 +1031,9 @@
 		for (Iterator<Event> i = events.iterator(); i.hasNext(); ) {
 			current = i.next();
 			if (count == 0) {
-				sum = current.keyDown;
-				if (hasMatchFile)
+				sum = current.keyDown;	// will average "simultaneous" onsets
+				//if (hasMatchFile)
+				if ((metadata == MetaType.MATCH) || (metadata == MetaType.MIDI))
 					correction = matchFileOffset - current.keyDown;
 				count = 1;
 				prevBeat = current.scoreBeat;
@@ -951,14 +1061,16 @@
 			current = i.next();
 			if (count == 0) {
 				sum = current.keyDown;
-				if (pm.hasMatchFile)
+				//if (pm.hasMatchFile)
+				if ((pm.metadata == MetaType.MATCH) ||
+						(pm.metadata == MetaType.MIDI))
 					correction = pm.matchFileOffset - current.keyDown;
 				count = 1;
 				prevBeat = current.scoreBeat;
 			} else if (current.scoreBeat == prevBeat) {
 				sum += current.keyDown;
 				count++;
-			} else {
+			} else {	// insert in list sorted (keyed) by beat number
 				while ((onset.beat < prevBeat) && li.hasNext())
 					onset = li.next();
 				while ((onset.beat > prevBeat) && li.hasPrevious())
@@ -973,7 +1085,7 @@
 				prevBeat = current.scoreBeat;
 			}
 		}
-		if (count != 0) {
+		if (count != 0) {	// insert last event
 			while ((onset.beat < prevBeat) && li.hasNext())
 				onset = li.next();
 			while ((onset.beat > prevBeat) && li.hasPrevious())
@@ -989,7 +1101,7 @@
 			onset = li.next();
 			String s = String.format("%8.3f %8.3f %8.3f",
 									onset.beat, onset.time1, onset.time2);
-			if ((onset.time1 < 0) || (onset.time2 < 0)) {
+			if ((onset.time1 < 0) || (onset.time2 < 0) || (onset.beat < 0)) {
 				System.err.println("Match Error: " + s);
 				li.remove();	// notes must exist in both performances
 			}
@@ -1075,12 +1187,15 @@
 	 *  <li><b>-m2 matchFile2 offset2</b> matchFile + start time for inputFile2
 	 *  <li><b>-w1 wormFile1</b> wormFile for inputFile1
 	 *  <li><b>-w2 wormFile2</b> wormFile for inputFile2
-	 *  <li><b>-n1</b> normalise FFT frames before comparison
-	 *  <li><b>-N1</b> do not normalise FFT frames before comparison (default)
-	 *  <li><b>-n2</b> normalise distance metric
-	 *  <li><b>-N2</b> do not normalise distance metric (default)
-	 *  <li><b>-n3</b> normalise .........
-	 *  <li><b>-N3</b> do not normalise (default)
+	 *  <li><b>-[nN]<i>k</i></b> Various options for normalisation, where
+	 *    <b>n</b> switches on, <b>N</b> switches off each option.
+	 *    <b><i>k</i></b> can have the following values:<ul>
+	 *    <li>1: normalise each FFT frame (sum of energy = 1) before comparison (default=true)
+	 *    <li>2: normalise distance metric by sum of energies of both frames (default=false)
+	 *    <li>3: normalise each FFT frame by the medium-term average energy (default=false)
+	 *    <li>4: normalise distance metric by thresholded log of sum of frames (default=true)
+	 *    <li>5: set distance to zero for non-annotated positions in either file (default=false)
+	 *    </ul>
 	 *  <li><b>-d</b> use half-wave rectified spectral difference (default)
 	 *  <li><b>-D</b> do not use half-wave rectified spectral difference
 	 *  <li><b>-s scale</b> set scaling factor for distance metric
@@ -1095,6 +1210,11 @@
 	 *  <li><b>-W</b> live worm output (file 2)
 	 *  <li><b>-z fileName</b> worm output (map file 1 to 2)
 	 *  <li><b>-Z fileName</b> worm output (map file 2 to 1)
+	 *  <li><b>-ob fileName</b> output backward match path
+	 *  <li><b>-of fileName</b> output forward match path
+	 *  <li><b>-os fileName</b> output smoothed match path
+	 *  <li><b>-rf1 value</b> set A4 reference frequency (default 440) for pm1
+	 *  <li><b>-rf2 value</b> set A4 reference frequency for pm2
 	 *  </ul>
 	 */
 	public static int processArgs(PerformanceMatcher pm1,
@@ -1148,6 +1268,12 @@
 				pm1.setMatchFile(args[++i], 0, true);
 			} else if (args[i].equals("-w2")) {
 				pm2.setMatchFile(args[++i], 0, true);
+			} else if (args[i].equals("-M1")) {
+				pm1.setLabelFile(args[++i]);
+				MatrixFrame.useSmoothPath = false;
+			} else if (args[i].equals("-M2")) {
+				pm2.setLabelFile(args[++i]);
+				MatrixFrame.useSmoothPath = false;
 			} else if (args[i].equals("-d")) {
 				pm1.useSpectralDifference = pm2.useSpectralDifference = true;
 			} else if (args[i].equals("-D")) {
@@ -1168,6 +1294,16 @@
 				pm1.normalise4 = pm2.normalise4 = true;
 			} else if (args[i].equals("-N4")) {
 				pm1.normalise4 = pm2.normalise4 = false;
+			} else if (args[i].equals("-k1")) {
+				pm1.normalise5 = true;
+				pm2.normalise5 = false;
+			} else if (args[i].equals("-k2")) {
+				pm1.normalise5 = false;
+				pm2.normalise5 = true;
+			} else if (args[i].equals("--plot1")) {
+				pm1.plot = new Plot();
+			} else if (args[i].equals("--plot2")) {
+				pm2.plot = new Plot();
 			} else if (args[i].equals("--use-chroma-map")) {
 				pm1.useChromaFrequencyMap = pm2.useChromaFrequencyMap = true;
 			} else if (args[i].equals("-b")) {
@@ -1215,13 +1351,30 @@
 				pm2.wormHandler = new WormHandler(pm2);
 				pm2.liveWorm = true;
 			} else if (args[i].equals("-z")) {
-				batchMode = true;
-				pm1.wormHandler = new WormHandler(pm1);
+				//batchMode = true;			//TODO check if scripts are broken
+				//if (!pm2.hasLabelFile)	//TODO why !hLabel instead of hWorm
+				if (pm2.metadata == MetaType.WORM)
+					pm1.wormHandler = new WormHandler(pm1);
 				pm1.matchFileName = args[++i];
 			} else if (args[i].equals("-Z")) {
-				batchMode = true;
-				pm2.wormHandler = new WormHandler(pm2);
+				//batchMode = true;			//TODO check if scripts are broken
+				//if (!pm1.hasLabelFile)
+				if (pm1.metadata == MetaType.WORM)
+					pm2.wormHandler = new WormHandler(pm2);
 				pm2.matchFileName = args[++i];
+			} else if (args[i].equals("-ob")) {
+				pm1.outputFileName = args[++i];
+				pm1.outputType = PathType.BACKWARD;
+			} else if (args[i].equals("-of")) {
+				pm1.outputFileName = args[++i];
+				pm1.outputType = PathType.FORWARD;
+			} else if (args[i].equals("-os")) {
+				pm1.outputFileName = args[++i];
+				pm1.outputType = PathType.SMOOTHED;
+			} else if (args[i].equals("-rf1")) {
+				pm1.referenceFrequency = Double.parseDouble(args[++i]);
+			} else if (args[i].equals("-rf2")) {
+				pm2.referenceFrequency = Double.parseDouble(args[++i]);
 			} else
 				return i;
 			//	System.err.println("WARNING: Ignoring argument: " + args[i]);
@@ -1252,14 +1405,37 @@
 			if (pm2.pcmInputStream != null) {
 				doMatch(pm1, pm2, s);
 				s.updatePaths(false);
-				if ((pm1.hasMatchFile && pm2.hasMatchFile) ||
-						(pm1.hasWormFile && pm2.hasWormFile))
+				//if ((pm1.hasMatchFile && pm2.hasMatchFile) ||
+				//		(pm1.hasWormFile && pm2.hasWormFile))
+				if ((pm1.metadata == pm2.metadata) &&
+						((pm1.metadata == MetaType.MATCH) ||
+							(pm1.metadata == MetaType.WORM)))
 					s.evaluatePaths();
-				else if (pm1.hasWormFile && !pm2.hasWormFile) {
-					if ((pm2.matchFileName != null) && !pm2.hasMatchFile)
+				//else if (pm1.hasWormFile && !pm2.hasWormFile) {
+				else if ((pm1.metadata != MetaType.NONE) &&
+						(pm2.metadata == MetaType.NONE)) {
+					//if (pm1.hasLabelFile)
+					if (pm1.metadata == MetaType.LABEL)
+						pm2.writeLabelFile(s);
+					else if (pm1.metadata == MetaType.MIDI)
+						pm2.writeMidiFile(s);
+					//else if ((pm2.matchFileName != null) && !pm2.hasMatchFile)
+					else if (pm2.matchFileName != null)
 						s.wormHandler.write(new File(pm2.matchFileName), false);
 					else
 						g.saveWormFile();
+				} else if (pm1.outputFileName != null) {
+					switch (pm1.outputType) {
+						case BACKWARD:
+							s.saveBackwardPath(pm1.outputFileName);
+							break;
+						case FORWARD:
+							s.saveForwardPath(pm1.outputFileName);
+							break;
+						case SMOOTHED:
+							s.saveSmoothedPath(pm1.outputFileName);
+							break;
+					}
 				}
 			}
 			if (!silent)
--- a/at/ofai/music/match/ScrollingMatrix.java	Fri Oct 08 16:02:41 2010 +0100
+++ b/at/ofai/music/match/ScrollingMatrix.java	Fri Oct 08 16:03:47 2010 +0100
@@ -528,6 +528,7 @@
 		sPathLength = Path.smooth(sPathX, sPathY, bPathLength);
 		if ((pm1.audioFile == null) || (pm2.audioFile == null))
 			return;
+		//if (MatrixFrame.useSmoothPath) {
 		if (pm1.audioFile.isReference) {
 			pm2.audioFile.setMatch(sPathX, pm2.hopTime, sPathY, pm1.hopTime,
 									sPathLength);
@@ -538,8 +539,31 @@
 			pm2.audioFile.setFixedPoints(lastPoint, false);
 			System.err.println("Warning: pm1 is not reference file");
 		}
+		//}
 	} // updateSmoothedPath()
 
+	public void saveBackwardPath(String fileName) {
+		savePath(fileName, bPathX, bPathY, bPathLength);
+	} // saveBackwardPath()
+
+	public void saveForwardPath(String fileName) {
+		savePath(fileName, fPathX, fPathY, fPathLength);
+	} // saveForwardPath()
+
+	public void saveSmoothedPath(String fileName) {
+		savePath(fileName, sPathX, sPathY, sPathLength);
+	} // saveSmoothedPath()
+
+	protected void savePath(String fileName, int[] pathX, int[] pathY, int len){
+		try {
+			java.io.PrintStream out = new java.io.PrintStream(fileName);
+			for (int i = len-1; i >= 0; i--)
+				out.printf(" %7.3f %7.3f\n", hop1 * pathY[i], hop2 * pathX[i]);
+		} catch (Exception e) {
+			System.err.println("Error saving path: " + e);
+		}
+	} // savePath()
+
 	public int[] forwardPathX() {
 		return replace(fPathX, fPathLength, fPathLength);
 	} // forwardPathX()