Chris@2: /* Chris@2: Copyright (C) 2001, 2006 by Simon Dixon Chris@2: Chris@2: This program is free software; you can redistribute it and/or modify Chris@2: it under the terms of the GNU General Public License as published by Chris@2: the Free Software Foundation; either version 2 of the License, or Chris@2: (at your option) any later version. Chris@2: Chris@2: This program is distributed in the hope that it will be useful, Chris@2: but WITHOUT ANY WARRANTY; without even the implied warranty of Chris@2: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Chris@2: GNU General Public License for more details. Chris@2: Chris@2: You should have received a copy of the GNU General Public License along Chris@2: with this program (the file gpl.txt); if not, download it from Chris@2: http://www.gnu.org/licenses/gpl.txt or write to the Chris@2: Free Software Foundation, Inc., Chris@2: 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Chris@2: */ Chris@2: Chris@2: package at.ofai.music.util; Chris@2: Chris@2: import at.ofai.music.util.Format; Chris@2: Chris@2: // Stores a list of corresponding performance (real) times and Chris@2: // notated (score or MIDI) times. Automatically resizes when necessary. Chris@2: // Very inefficient if the list is edited after being used. Chris@2: // Assumes a monotonic mapping (t1 > t2 <=> s1 > s2) Chris@2: public class MatchTempoMap implements TempoMap { Chris@2: Chris@2: protected double[] realTime; // in seconds Chris@2: protected double[] scoreTime; // in beats or MIDI units Chris@2: protected int[] repeats; // for calculating the average if needed Chris@2: protected int size; // number of entries being used Chris@2: Chris@2: public MatchTempoMap() { Chris@2: this(5000); // Mozart files are up to 3251 notes Chris@2: } // default constructor Chris@2: Chris@2: public MatchTempoMap(int sz) { Chris@2: realTime = new double[sz]; Chris@2: scoreTime = new double[sz]; Chris@2: repeats = new int[sz]; Chris@2: size = 0; Chris@2: } // constructor Chris@2: Chris@2: protected void makeSpace() { Chris@2: if (size == realTime.length) Chris@2: resize(new MatchTempoMap(2 * size)); Chris@2: } // makeSpace() Chris@2: Chris@2: protected void closeList() { Chris@2: if (size != realTime.length) Chris@2: resize(new MatchTempoMap(size)); Chris@2: } // closeList() Chris@2: Chris@2: protected void resize(MatchTempoMap newList) { Chris@2: for (int i = 0; i < size; i++) { Chris@2: newList.realTime[i] = realTime[i]; Chris@2: newList.scoreTime[i] = scoreTime[i]; Chris@2: newList.repeats[i] = repeats[i]; Chris@2: } Chris@2: realTime = newList.realTime; Chris@2: scoreTime = newList.scoreTime; Chris@2: repeats = newList.repeats; Chris@2: } // resize() Chris@2: Chris@2: public double toRealTime(double sTime) { Chris@2: closeList(); Chris@2: return lookup(sTime, scoreTime, realTime); Chris@2: } // toRealTime() Chris@2: Chris@2: public double toScoreTime(double rTime) { Chris@2: closeList(); Chris@2: return lookup(rTime, realTime, scoreTime); Chris@2: } // toScoreTime() Chris@2: Chris@2: public double lookup(double value, double[] domain, double[] range) { Chris@2: int index = java.util.Arrays.binarySearch(domain, value); Chris@2: if (index >= 0) Chris@2: return range[index]; Chris@2: if ((size == 0) || ((size == 1) && Chris@2: ((range[0] == 0) || (domain[0] == 0)))) Chris@2: throw new RuntimeException("Insufficient entries in tempo map"); Chris@2: if (size == 1) Chris@2: return value * range[0] / domain[0]; Chris@2: index = -1 - index; // do linear interpolation Chris@2: if (index == 0) // unless at ends, where it is extrapolation Chris@2: index++; Chris@2: else if (index == size) Chris@2: index--; Chris@2: return (range[index] * (value - domain[index - 1]) + Chris@2: range[index - 1] * (domain[index] - value)) / Chris@2: (domain[index] - domain[index - 1]); Chris@2: } // lookup() Chris@2: Chris@2: public void add(double rTime, double sTime) { Chris@2: if (Double.isNaN(sTime)) Chris@2: return; Chris@2: makeSpace(); Chris@2: int index; Chris@2: for (index = 0; index < size; index++) Chris@2: if (sTime <= scoreTime[index]) Chris@2: break; Chris@2: if ((index == size) || (sTime != scoreTime[index])) { Chris@2: for (int j = size; j > index; j--) { Chris@2: scoreTime[j] = scoreTime[j-1]; Chris@2: realTime[j] = realTime[j-1]; Chris@2: repeats[j] = repeats[j-1]; Chris@2: } Chris@2: size++; Chris@2: scoreTime[index] = sTime; Chris@2: realTime[index] = rTime; Chris@2: repeats[index] = 1; Chris@2: } else { // average time of multiple nominally simultaneous notes Chris@2: realTime[index] = (repeats[index] * realTime[index] + rTime) / Chris@2: (repeats[index] + 1); Chris@2: repeats[index]++; Chris@2: } Chris@2: } // add() Chris@2: Chris@2: public void dump(double[] tempo, double step) { Chris@2: if (size < 2) { Chris@2: System.err.println("dump() failed: Empty tempo map"); Chris@2: return; Chris@2: } Chris@2: double[] tmp = new double[tempo.length]; Chris@2: int i = 0; Chris@2: for (int j = 1; j < size; j++) Chris@2: for ( ; i * step < realTime[j]; i++) Chris@2: tmp[i] = (realTime[j] - realTime[j - 1]) / Chris@2: (scoreTime[j] - scoreTime[j - 1]); Chris@2: for ( ; i < tmp.length; i++) Chris@2: tmp[i] = (realTime[size - 1] - realTime[size - 2]) / Chris@2: (scoreTime[size - 1] - scoreTime[size - 2]); Chris@2: int window = (int)(0.1 / step); // smooth over 2.0 second window Chris@2: double sum = 0; Chris@2: for (i = 0; i < tmp.length; i++) { Chris@2: sum += tmp[i]; Chris@2: if (i >= window) { Chris@2: sum -= tmp[i - window]; Chris@2: tempo[i] = sum / window; Chris@2: } else Chris@2: tempo[i] = sum / (i + 1); Chris@2: // System.out.println(i + " " + Format.d(tmp[i],3) + Chris@2: // " " + Format.d(tempo[i], 3)); Chris@2: if (tempo[i] != 0) Chris@2: tempo[i] = 60.0 / tempo[i]; Chris@2: } Chris@2: } // dump Chris@2: Chris@2: public void print() { Chris@2: System.out.println("Score | Perf.\n-------+-------"); Chris@2: for (int i = 0; i < size; i++) Chris@2: System.out.println(Format.d(scoreTime[i], 3) + " | " + Chris@2: Format.d(realTime[i], 3)); Chris@2: } // print() Chris@2: Chris@2: public static void main(String[] args) { // unit test Chris@2: TempoMap mtm = new MatchTempoMap(); Chris@2: mtm.add(0.6, 1); Chris@2: mtm.add(0.8, 2); Chris@2: mtm.add(0.95, 2.5); Chris@2: mtm.add(1.0, 3); Chris@2: double[] st = {0, 1, 2, 3, 4}; Chris@2: for (int i = 0 ; i < st.length; i++) Chris@2: System.out.println(st[i] + " -> " + mtm.toRealTime(st[i]) + Chris@2: " -> " + mtm.toScoreTime(mtm.toRealTime(st[i]))); Chris@2: double[] rt = {0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1}; Chris@2: for (int i = 0 ; i < rt.length; i++) Chris@2: System.out.println(rt[i] + " => " + mtm.toScoreTime(rt[i]) + Chris@2: " => " + mtm.toRealTime(mtm.toScoreTime(rt[i]))); Chris@2: } // main() Chris@2: Chris@2: } // class MatchTempoMap