Chris@2
|
1 /* Performance Worm: Visualisation of Expressive Musical Performance
|
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.worm;
|
Chris@2
|
22
|
Chris@2
|
23 import java.io.File;
|
Chris@2
|
24 import java.io.FileOutputStream;
|
Chris@2
|
25 import java.io.PrintStream;
|
Chris@2
|
26 import java.io.FileReader;
|
Chris@2
|
27 import java.io.BufferedReader;
|
Chris@2
|
28 import java.io.FileNotFoundException;
|
Chris@2
|
29 import java.io.IOException;
|
Chris@2
|
30 import java.util.StringTokenizer;
|
Chris@2
|
31 import java.util.Iterator;
|
Chris@2
|
32 import java.awt.Frame;
|
Chris@2
|
33 import at.ofai.music.util.Event;
|
Chris@2
|
34 import at.ofai.music.util.EventList;
|
Chris@2
|
35 import at.ofai.music.util.Format;
|
Chris@2
|
36 import at.ofai.music.util.MatchTempoMap;
|
Chris@2
|
37
|
Chris@2
|
38 // Read/write performance worm data
|
Chris@2
|
39 public class WormFile {
|
Chris@2
|
40
|
Chris@2
|
41 Worm worm;
|
Chris@2
|
42 double outFramePeriod, inFramePeriod;
|
Chris@2
|
43 int length;
|
Chris@2
|
44 double[] time;
|
Chris@2
|
45 double[] inTempo, outTempo;
|
Chris@2
|
46 double[] inIntensity, outIntensity;
|
Chris@2
|
47 int[] inFlags, outFlags;
|
Chris@2
|
48 String[] label;
|
Chris@2
|
49 public static final int
|
Chris@2
|
50 TRACK=1, BEAT=2, BAR=4, SEG1=8, SEG2=16, SEG3=32, SEG4=64;
|
Chris@2
|
51 public static final double defaultFramePeriod = 0.1; // 10 FPS
|
Chris@2
|
52 WormParameters info;
|
Chris@2
|
53
|
Chris@2
|
54 private WormFile(Frame f) {
|
Chris@2
|
55 info = new WormParameters(f);
|
Chris@2
|
56 inFramePeriod = defaultFramePeriod;
|
Chris@2
|
57 outFramePeriod = defaultFramePeriod;
|
Chris@2
|
58 } // shared constructor
|
Chris@2
|
59
|
Chris@2
|
60 public WormFile(int size) {
|
Chris@2
|
61 this(null);
|
Chris@2
|
62 length = size;
|
Chris@2
|
63 init();
|
Chris@2
|
64 } // constructor
|
Chris@2
|
65
|
Chris@2
|
66 public WormFile(int size, double step) {
|
Chris@2
|
67 this(size);
|
Chris@2
|
68 inFramePeriod = step;
|
Chris@2
|
69 } // constructor
|
Chris@2
|
70
|
Chris@2
|
71 public WormFile(EventList el, double step) {
|
Chris@2
|
72 this(null);
|
Chris@2
|
73 inFramePeriod = step;
|
Chris@2
|
74 convertList(el);
|
Chris@2
|
75 } // constructor
|
Chris@2
|
76
|
Chris@2
|
77 public WormFile(Worm w, EventList el) {
|
Chris@2
|
78 this(w == null? null: w.theFrame);
|
Chris@2
|
79 worm = w;
|
Chris@2
|
80 convertList(el);
|
Chris@2
|
81 } // constructor
|
Chris@2
|
82
|
Chris@2
|
83 public WormFile(Worm w, String fileName) {
|
Chris@2
|
84 this(w.theFrame);
|
Chris@2
|
85 worm = w;
|
Chris@2
|
86 read(fileName);
|
Chris@2
|
87 } // constructor
|
Chris@2
|
88
|
Chris@2
|
89 public void init() {
|
Chris@2
|
90 inTempo = new double[length];
|
Chris@2
|
91 inIntensity = new double[length];
|
Chris@2
|
92 inFlags = new int[length];
|
Chris@2
|
93 time = new double[length];
|
Chris@2
|
94 } // init()
|
Chris@2
|
95
|
Chris@2
|
96 public void smooth(int mode, double left, double right, int smoothLevel) {
|
Chris@2
|
97 if (worm != null)
|
Chris@2
|
98 worm.setSmoothMode(Worm.NONE);
|
Chris@2
|
99 info.smoothing = "None";
|
Chris@2
|
100 if ((outFramePeriod == 0) || ((inFramePeriod == 0) && (time == null))) {
|
Chris@2
|
101 System.err.println("Error: smooth() frameLength unspecified");
|
Chris@2
|
102 return;
|
Chris@2
|
103 }
|
Chris@2
|
104 if (inFramePeriod != 0) {
|
Chris@2
|
105 for (int i = 0; i < length; i++)
|
Chris@2
|
106 time[i] = inFramePeriod * i;
|
Chris@2
|
107 }
|
Chris@2
|
108 int outLength = 1+(int) Math.ceil(time[time.length-1] / outFramePeriod);
|
Chris@2
|
109 if ((outTempo == null) || (outTempo.length != outLength)) {
|
Chris@2
|
110 outTempo = new double[outLength];
|
Chris@2
|
111 outIntensity = new double[outLength];
|
Chris@2
|
112 outFlags = new int[outLength];
|
Chris@2
|
113 label = new String[outLength];
|
Chris@2
|
114 }
|
Chris@2
|
115 if (mode == Worm.NONE) {
|
Chris@2
|
116 int i = 0, o = 0;
|
Chris@2
|
117 while (o * outFramePeriod < time[0]) {
|
Chris@2
|
118 outTempo[o] = inTempo[0];
|
Chris@2
|
119 outIntensity[o] = inIntensity[0];
|
Chris@2
|
120 o++;
|
Chris@2
|
121 }
|
Chris@2
|
122 for ( ; i < time.length - 1; i++) {
|
Chris@2
|
123 while (o * outFramePeriod < time[i+1]) {
|
Chris@2
|
124 outTempo[o] = inTempo[i];
|
Chris@2
|
125 outIntensity[o] = inIntensity[i];
|
Chris@2
|
126 o++;
|
Chris@2
|
127 }
|
Chris@2
|
128 }
|
Chris@2
|
129 while (o < outLength) {
|
Chris@2
|
130 outTempo[o] = inTempo[i];
|
Chris@2
|
131 outIntensity[o] = inIntensity[i];
|
Chris@2
|
132 o++;
|
Chris@2
|
133 }
|
Chris@2
|
134 } else {
|
Chris@2
|
135 info.smoothing = "Gaussian" + "\t" + Format.d(left, 4) +
|
Chris@2
|
136 "\t" + Format.d(right, 4);
|
Chris@2
|
137 if (smoothLevel != 0) {
|
Chris@2
|
138 int count = 0;
|
Chris@2
|
139 double first = 0, last = 0;
|
Chris@2
|
140 for (int i = 0; i < time.length; i++)
|
Chris@2
|
141 if ((inFlags[i] & smoothLevel) != 0) {
|
Chris@2
|
142 if (count == 0)
|
Chris@2
|
143 first = time[i];
|
Chris@2
|
144 else
|
Chris@2
|
145 last = time[i];
|
Chris@2
|
146 count++;
|
Chris@2
|
147 }
|
Chris@2
|
148 if (count < 2)
|
Chris@2
|
149 System.err.println("Warning: Beat data not available");
|
Chris@2
|
150 else {
|
Chris@2
|
151 double IBI = (last - first) / (count - 1);
|
Chris@2
|
152 left *= IBI;
|
Chris@2
|
153 right *= IBI;
|
Chris@2
|
154 info.smoothing += "\t" +Format.d(IBI,4) + "\t" +smoothLevel;
|
Chris@2
|
155 System.out.println("Smoothing parameters (seconds): pre=" +
|
Chris@2
|
156 Format.d(left,3) + " post=" + Format.d(right,3));
|
Chris@2
|
157 }
|
Chris@2
|
158 }
|
Chris@2
|
159 int start = 0;
|
Chris@2
|
160 for (int o = 0; o < outLength; o++) {
|
Chris@2
|
161 double sum = 0, val = 0, tempo = 0, intensity = 0;
|
Chris@2
|
162 for (int i = start; i < time.length; i++) {
|
Chris@2
|
163 double d = o * outFramePeriod - time[i];
|
Chris@2
|
164 if (d > 4 * left) { // average over 4 stddevs
|
Chris@2
|
165 start++;
|
Chris@2
|
166 continue;
|
Chris@2
|
167 }
|
Chris@2
|
168 if (d < -4 * right)
|
Chris@2
|
169 break;
|
Chris@2
|
170 if (d < 0)
|
Chris@2
|
171 val = Math.exp(-d*d/(left*left*2));
|
Chris@2
|
172 else
|
Chris@2
|
173 val = Math.exp(-d*d/(right*right*2));
|
Chris@2
|
174 sum += val;
|
Chris@2
|
175 tempo += val * inTempo[i];
|
Chris@2
|
176 intensity += val * inIntensity[i];
|
Chris@2
|
177 }
|
Chris@2
|
178 if (sum == 0) { // assume this only occurs at beginning
|
Chris@2
|
179 outTempo[o] = inTempo[0];
|
Chris@2
|
180 outIntensity[o] = inIntensity[0];
|
Chris@2
|
181 } else {
|
Chris@2
|
182 outTempo[o] = tempo / sum;
|
Chris@2
|
183 outIntensity[o] = intensity / sum;
|
Chris@2
|
184 }
|
Chris@2
|
185 }
|
Chris@2
|
186 }
|
Chris@2
|
187 for (int i = 0; i < outFlags.length; i++)
|
Chris@2
|
188 outFlags[i] = 0;
|
Chris@2
|
189 for (int i = 0; i < inFlags.length; i++)
|
Chris@2
|
190 outFlags[(int)Math.round(time[i] / outFramePeriod)] |= inFlags[i];
|
Chris@2
|
191 int bar = 0;
|
Chris@2
|
192 int beat = 0;
|
Chris@2
|
193 int track = 0;
|
Chris@2
|
194 for (int i = 0; i < outFlags.length; i++) {
|
Chris@2
|
195 if ((outFlags[i] & BAR) != 0)
|
Chris@2
|
196 bar++;
|
Chris@2
|
197 if ((outFlags[i] & BEAT) != 0)
|
Chris@2
|
198 beat++;
|
Chris@2
|
199 if ((outFlags[i] & TRACK) != 0)
|
Chris@2
|
200 track++;
|
Chris@2
|
201 label[i] = bar + ":" + beat + ":" + track + ":" +
|
Chris@2
|
202 Format.d(i * outFramePeriod, 1);
|
Chris@2
|
203 }
|
Chris@2
|
204 } // smooth()
|
Chris@2
|
205
|
Chris@2
|
206 public void editParameters() {
|
Chris@2
|
207 info.editParameters();
|
Chris@2
|
208 update();
|
Chris@2
|
209 } // editParameters()
|
Chris@2
|
210
|
Chris@2
|
211 public void update() {
|
Chris@2
|
212 length = info.length;
|
Chris@2
|
213 inFramePeriod = info.framePeriod;
|
Chris@2
|
214 worm.setTitle(info.composer + ", " + info.piece +
|
Chris@2
|
215 ", played by " + info.performer);
|
Chris@2
|
216 // not used (?) : beatLevel trackLevel upbeat beatsPerBar
|
Chris@2
|
217 if ((inTempo == null) || (inTempo.length != length))
|
Chris@2
|
218 init();
|
Chris@2
|
219 worm.setInputFile(info.audioPath, info.audioFile);
|
Chris@2
|
220 worm.setSmoothMode(Worm.NONE);
|
Chris@2
|
221 if (info.axis.length() > 0)
|
Chris@2
|
222 worm.setAxis(info.axis);
|
Chris@2
|
223 worm.setFramePeriod(outFramePeriod);
|
Chris@2
|
224 worm.setLoudnessUnits(info.loudnessUnits);
|
Chris@2
|
225 } // update()
|
Chris@2
|
226
|
Chris@2
|
227 public void convertList(EventList el) {
|
Chris@2
|
228 double tMax = 0;
|
Chris@2
|
229 int count = 0;
|
Chris@2
|
230 for (Iterator<Event> i = el.iterator(); i.hasNext(); ) {
|
Chris@2
|
231 double pedalUpTime = i.next().pedalUp;
|
Chris@2
|
232 if (pedalUpTime > tMax)
|
Chris@2
|
233 tMax = pedalUpTime;
|
Chris@2
|
234 count++;
|
Chris@2
|
235 }
|
Chris@2
|
236 length = (int)Math.ceil(tMax / inFramePeriod);
|
Chris@2
|
237 init();
|
Chris@2
|
238 // double[] decayFactor = new double[128];
|
Chris@2
|
239 // for (int i = 0; i < 128; i++)
|
Chris@2
|
240 // decayFactor[i] = Math.max(5.0, (i - 6.0) / 3.0) * inFramePeriod;
|
Chris@2
|
241 // // was Math.pow(0.1, inFramePeriod); // modify for pitch?
|
Chris@2
|
242 for (Iterator<Event> i = el.l.iterator(); i.hasNext(); ) {
|
Chris@2
|
243 Event e = i.next();
|
Chris@2
|
244 double loudness = 30.29 * Math.pow(e.midiVelocity, 0.2609);
|
Chris@2
|
245 loudness += (e.midiPitch - 66.0) / 12.0; // +1dB / oct
|
Chris@2
|
246 int start = (int)Math.floor(e.keyDown / inFramePeriod);
|
Chris@2
|
247 if (start < 0)
|
Chris@2
|
248 start = 0;
|
Chris@2
|
249 int stop = (int)Math.ceil((e.pedalUp + 0.5) / inFramePeriod);
|
Chris@2
|
250 if (stop > inIntensity.length)
|
Chris@2
|
251 stop = inIntensity.length;
|
Chris@2
|
252 for (int t = start; t < stop; t++) {
|
Chris@2
|
253 if (loudness > inIntensity[t])
|
Chris@2
|
254 inIntensity[t] = loudness;
|
Chris@2
|
255 loudness -= Math.max(5.0, (e.midiPitch - 6.0) / 3.0) *
|
Chris@2
|
256 inFramePeriod;
|
Chris@2
|
257 // was: mult by decay factor. But since vals are dB, we subtract
|
Chris@2
|
258 }
|
Chris@2
|
259 }
|
Chris@2
|
260 MatchTempoMap tMap = new MatchTempoMap(count);
|
Chris@2
|
261 for (Iterator<Event> i = el.l.iterator(); i.hasNext(); ) {
|
Chris@2
|
262 Event e = i.next();
|
Chris@2
|
263 tMap.add(e.keyDown, e.scoreBeat);
|
Chris@2
|
264 }
|
Chris@2
|
265 // el.print();
|
Chris@2
|
266 // tMap.print(); // for debugging
|
Chris@2
|
267 tMap.dump(inTempo, inFramePeriod);
|
Chris@2
|
268 } // convertList()
|
Chris@2
|
269
|
Chris@2
|
270 public void write(String fileName) {
|
Chris@2
|
271 PrintStream out;
|
Chris@2
|
272 try {
|
Chris@2
|
273 out = new PrintStream(new FileOutputStream(fileName));
|
Chris@2
|
274 } catch (FileNotFoundException e) {
|
Chris@2
|
275 System.err.println("Unable to open output file " + fileName);
|
Chris@2
|
276 return;
|
Chris@2
|
277 }
|
Chris@2
|
278 info.write(out, outTempo.length, outFramePeriod);
|
Chris@2
|
279 for (int i = 0; i < outTempo.length; i++) {
|
Chris@2
|
280 if (outFramePeriod == 0)
|
Chris@2
|
281 out.print(Format.d(time[i],3) + " ");
|
Chris@2
|
282 out.println(Format.d(outTempo[i],4) +" "+
|
Chris@2
|
283 Format.d(outIntensity[i],4) +" "+outFlags[i]);
|
Chris@2
|
284 }
|
Chris@2
|
285 out.close();
|
Chris@2
|
286 } // write()
|
Chris@2
|
287
|
Chris@2
|
288 public void read(String fileName) {
|
Chris@2
|
289 try {
|
Chris@2
|
290 File f = new File(fileName);
|
Chris@2
|
291 if (!f.isFile()) // a local hack for UNC file names under Windows
|
Chris@2
|
292 f = new File("//fichte" + fileName);
|
Chris@2
|
293 if (!f.isFile())
|
Chris@2
|
294 throw(new FileNotFoundException("Could not open " + fileName));
|
Chris@2
|
295 BufferedReader in = new BufferedReader(new FileReader(f));
|
Chris@2
|
296 String input = info.read(in);
|
Chris@2
|
297 update();
|
Chris@2
|
298 int index = 0;
|
Chris@2
|
299 int bar = 0;
|
Chris@2
|
300 while ((input != null) && (index < length)) {
|
Chris@2
|
301 StringTokenizer tk = new StringTokenizer(input);
|
Chris@2
|
302 if (inFramePeriod != 0)
|
Chris@2
|
303 time[index] = Double.parseDouble(tk.nextToken());
|
Chris@2
|
304 inTempo[index] = Double.parseDouble(tk.nextToken());
|
Chris@2
|
305 inIntensity[index] = Double.parseDouble(tk.nextToken());
|
Chris@2
|
306 if (tk.hasMoreTokens())
|
Chris@2
|
307 inFlags[index] = Integer.parseInt(tk.nextToken());
|
Chris@2
|
308 else
|
Chris@2
|
309 inFlags[index] = 0;
|
Chris@2
|
310 input = in.readLine();
|
Chris@2
|
311 index++;
|
Chris@2
|
312 }
|
Chris@2
|
313 in.close();
|
Chris@2
|
314 smooth(Worm.NONE, 0, 0, 0);
|
Chris@2
|
315 } catch (FileNotFoundException e) {
|
Chris@2
|
316 System.err.println(e);
|
Chris@2
|
317 } catch (IOException e) {
|
Chris@2
|
318 System.err.println("IOException reading " + fileName);
|
Chris@2
|
319 } catch (Exception e) {
|
Chris@2
|
320 System.err.println("Error parsing file " + fileName + ": " + e);
|
Chris@2
|
321 e.printStackTrace();
|
Chris@2
|
322 }
|
Chris@2
|
323 } // read()
|
Chris@2
|
324
|
Chris@2
|
325 } // class WormFile
|