Chris@2
|
1 /*
|
Chris@2
|
2 Copyright (C) 2001, 2006 by Simon Dixon
|
Chris@2
|
3
|
Chris@2
|
4 This program is free software; you can redistribute it and/or modify
|
Chris@2
|
5 it under the terms of the GNU General Public License as published by
|
Chris@2
|
6 the Free Software Foundation; either version 2 of the License, or
|
Chris@2
|
7 (at your option) any later version.
|
Chris@2
|
8
|
Chris@2
|
9 This program is distributed in the hope that it will be useful,
|
Chris@2
|
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
|
Chris@2
|
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
Chris@2
|
12 GNU General Public License for more details.
|
Chris@2
|
13
|
Chris@2
|
14 You should have received a copy of the GNU General Public License along
|
Chris@2
|
15 with this program (the file gpl.txt); if not, download it from
|
Chris@2
|
16 http://www.gnu.org/licenses/gpl.txt or write to the
|
Chris@2
|
17 Free Software Foundation, Inc.,
|
Chris@2
|
18 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
Chris@2
|
19 */
|
Chris@2
|
20
|
Chris@2
|
21 package at.ofai.music.util;
|
Chris@2
|
22
|
Chris@2
|
23 import at.ofai.music.util.Format;
|
Chris@2
|
24
|
Chris@2
|
25 // Stores a list of corresponding performance (real) times and
|
Chris@2
|
26 // notated (score or MIDI) times. Automatically resizes when necessary.
|
Chris@2
|
27 // Very inefficient if the list is edited after being used.
|
Chris@2
|
28 // Assumes a monotonic mapping (t1 > t2 <=> s1 > s2)
|
Chris@2
|
29 public class MatchTempoMap implements TempoMap {
|
Chris@2
|
30
|
Chris@2
|
31 protected double[] realTime; // in seconds
|
Chris@2
|
32 protected double[] scoreTime; // in beats or MIDI units
|
Chris@2
|
33 protected int[] repeats; // for calculating the average if needed
|
Chris@2
|
34 protected int size; // number of entries being used
|
Chris@2
|
35
|
Chris@2
|
36 public MatchTempoMap() {
|
Chris@2
|
37 this(5000); // Mozart files are up to 3251 notes
|
Chris@2
|
38 } // default constructor
|
Chris@2
|
39
|
Chris@2
|
40 public MatchTempoMap(int sz) {
|
Chris@2
|
41 realTime = new double[sz];
|
Chris@2
|
42 scoreTime = new double[sz];
|
Chris@2
|
43 repeats = new int[sz];
|
Chris@2
|
44 size = 0;
|
Chris@2
|
45 } // constructor
|
Chris@2
|
46
|
Chris@2
|
47 protected void makeSpace() {
|
Chris@2
|
48 if (size == realTime.length)
|
Chris@2
|
49 resize(new MatchTempoMap(2 * size));
|
Chris@2
|
50 } // makeSpace()
|
Chris@2
|
51
|
Chris@2
|
52 protected void closeList() {
|
Chris@2
|
53 if (size != realTime.length)
|
Chris@2
|
54 resize(new MatchTempoMap(size));
|
Chris@2
|
55 } // closeList()
|
Chris@2
|
56
|
Chris@2
|
57 protected void resize(MatchTempoMap newList) {
|
Chris@2
|
58 for (int i = 0; i < size; i++) {
|
Chris@2
|
59 newList.realTime[i] = realTime[i];
|
Chris@2
|
60 newList.scoreTime[i] = scoreTime[i];
|
Chris@2
|
61 newList.repeats[i] = repeats[i];
|
Chris@2
|
62 }
|
Chris@2
|
63 realTime = newList.realTime;
|
Chris@2
|
64 scoreTime = newList.scoreTime;
|
Chris@2
|
65 repeats = newList.repeats;
|
Chris@2
|
66 } // resize()
|
Chris@2
|
67
|
Chris@2
|
68 public double toRealTime(double sTime) {
|
Chris@2
|
69 closeList();
|
Chris@2
|
70 return lookup(sTime, scoreTime, realTime);
|
Chris@2
|
71 } // toRealTime()
|
Chris@2
|
72
|
Chris@2
|
73 public double toScoreTime(double rTime) {
|
Chris@2
|
74 closeList();
|
Chris@2
|
75 return lookup(rTime, realTime, scoreTime);
|
Chris@2
|
76 } // toScoreTime()
|
Chris@2
|
77
|
Chris@2
|
78 public double lookup(double value, double[] domain, double[] range) {
|
Chris@2
|
79 int index = java.util.Arrays.binarySearch(domain, value);
|
Chris@2
|
80 if (index >= 0)
|
Chris@2
|
81 return range[index];
|
Chris@2
|
82 if ((size == 0) || ((size == 1) &&
|
Chris@2
|
83 ((range[0] == 0) || (domain[0] == 0))))
|
Chris@2
|
84 throw new RuntimeException("Insufficient entries in tempo map");
|
Chris@2
|
85 if (size == 1)
|
Chris@2
|
86 return value * range[0] / domain[0];
|
Chris@2
|
87 index = -1 - index; // do linear interpolation
|
Chris@2
|
88 if (index == 0) // unless at ends, where it is extrapolation
|
Chris@2
|
89 index++;
|
Chris@2
|
90 else if (index == size)
|
Chris@2
|
91 index--;
|
Chris@2
|
92 return (range[index] * (value - domain[index - 1]) +
|
Chris@2
|
93 range[index - 1] * (domain[index] - value)) /
|
Chris@2
|
94 (domain[index] - domain[index - 1]);
|
Chris@2
|
95 } // lookup()
|
Chris@2
|
96
|
Chris@2
|
97 public void add(double rTime, double sTime) {
|
Chris@2
|
98 if (Double.isNaN(sTime))
|
Chris@2
|
99 return;
|
Chris@2
|
100 makeSpace();
|
Chris@2
|
101 int index;
|
Chris@2
|
102 for (index = 0; index < size; index++)
|
Chris@2
|
103 if (sTime <= scoreTime[index])
|
Chris@2
|
104 break;
|
Chris@2
|
105 if ((index == size) || (sTime != scoreTime[index])) {
|
Chris@2
|
106 for (int j = size; j > index; j--) {
|
Chris@2
|
107 scoreTime[j] = scoreTime[j-1];
|
Chris@2
|
108 realTime[j] = realTime[j-1];
|
Chris@2
|
109 repeats[j] = repeats[j-1];
|
Chris@2
|
110 }
|
Chris@2
|
111 size++;
|
Chris@2
|
112 scoreTime[index] = sTime;
|
Chris@2
|
113 realTime[index] = rTime;
|
Chris@2
|
114 repeats[index] = 1;
|
Chris@2
|
115 } else { // average time of multiple nominally simultaneous notes
|
Chris@2
|
116 realTime[index] = (repeats[index] * realTime[index] + rTime) /
|
Chris@2
|
117 (repeats[index] + 1);
|
Chris@2
|
118 repeats[index]++;
|
Chris@2
|
119 }
|
Chris@2
|
120 } // add()
|
Chris@2
|
121
|
Chris@2
|
122 public void dump(double[] tempo, double step) {
|
Chris@2
|
123 if (size < 2) {
|
Chris@2
|
124 System.err.println("dump() failed: Empty tempo map");
|
Chris@2
|
125 return;
|
Chris@2
|
126 }
|
Chris@2
|
127 double[] tmp = new double[tempo.length];
|
Chris@2
|
128 int i = 0;
|
Chris@2
|
129 for (int j = 1; j < size; j++)
|
Chris@2
|
130 for ( ; i * step < realTime[j]; i++)
|
Chris@2
|
131 tmp[i] = (realTime[j] - realTime[j - 1]) /
|
Chris@2
|
132 (scoreTime[j] - scoreTime[j - 1]);
|
Chris@2
|
133 for ( ; i < tmp.length; i++)
|
Chris@2
|
134 tmp[i] = (realTime[size - 1] - realTime[size - 2]) /
|
Chris@2
|
135 (scoreTime[size - 1] - scoreTime[size - 2]);
|
Chris@2
|
136 int window = (int)(0.1 / step); // smooth over 2.0 second window
|
Chris@2
|
137 double sum = 0;
|
Chris@2
|
138 for (i = 0; i < tmp.length; i++) {
|
Chris@2
|
139 sum += tmp[i];
|
Chris@2
|
140 if (i >= window) {
|
Chris@2
|
141 sum -= tmp[i - window];
|
Chris@2
|
142 tempo[i] = sum / window;
|
Chris@2
|
143 } else
|
Chris@2
|
144 tempo[i] = sum / (i + 1);
|
Chris@2
|
145 // System.out.println(i + " " + Format.d(tmp[i],3) +
|
Chris@2
|
146 // " " + Format.d(tempo[i], 3));
|
Chris@2
|
147 if (tempo[i] != 0)
|
Chris@2
|
148 tempo[i] = 60.0 / tempo[i];
|
Chris@2
|
149 }
|
Chris@2
|
150 } // dump
|
Chris@2
|
151
|
Chris@2
|
152 public void print() {
|
Chris@2
|
153 System.out.println("Score | Perf.\n-------+-------");
|
Chris@2
|
154 for (int i = 0; i < size; i++)
|
Chris@2
|
155 System.out.println(Format.d(scoreTime[i], 3) + " | " +
|
Chris@2
|
156 Format.d(realTime[i], 3));
|
Chris@2
|
157 } // print()
|
Chris@2
|
158
|
Chris@2
|
159 public static void main(String[] args) { // unit test
|
Chris@2
|
160 TempoMap mtm = new MatchTempoMap();
|
Chris@2
|
161 mtm.add(0.6, 1);
|
Chris@2
|
162 mtm.add(0.8, 2);
|
Chris@2
|
163 mtm.add(0.95, 2.5);
|
Chris@2
|
164 mtm.add(1.0, 3);
|
Chris@2
|
165 double[] st = {0, 1, 2, 3, 4};
|
Chris@2
|
166 for (int i = 0 ; i < st.length; i++)
|
Chris@2
|
167 System.out.println(st[i] + " -> " + mtm.toRealTime(st[i]) +
|
Chris@2
|
168 " -> " + mtm.toScoreTime(mtm.toRealTime(st[i])));
|
Chris@2
|
169 double[] rt = {0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1};
|
Chris@2
|
170 for (int i = 0 ; i < rt.length; i++)
|
Chris@2
|
171 System.out.println(rt[i] + " => " + mtm.toScoreTime(rt[i]) +
|
Chris@2
|
172 " => " + mtm.toRealTime(mtm.toScoreTime(rt[i])));
|
Chris@2
|
173 } // main()
|
Chris@2
|
174
|
Chris@2
|
175 } // class MatchTempoMap
|