1 | package org.apollo.meldex;
|
---|
2 |
|
---|
3 | import java.util.ArrayList;
|
---|
4 |
|
---|
5 | @SuppressWarnings("unchecked") // code in java 1.4
|
---|
6 | public class PitchTracker
|
---|
7 | {
|
---|
8 | // The sample rate (frequency) of the loaded sample
|
---|
9 | int sampleRate;
|
---|
10 |
|
---|
11 | // The filtered data
|
---|
12 | byte[] filterData;
|
---|
13 | int filterLength;
|
---|
14 |
|
---|
15 | // The pitch data
|
---|
16 | ArrayList pitchData;
|
---|
17 |
|
---|
18 | // Our lovely Pitch Period Estimators
|
---|
19 | PitchEstimator[] ppe = new PitchEstimator[6];
|
---|
20 |
|
---|
21 | // Our lovely winner calculator
|
---|
22 | WinnerCalculator winnerCalc = new WinnerCalculator();
|
---|
23 |
|
---|
24 | int lastMinHeight, lastMaxHeight;
|
---|
25 | int lastPeriodStart;
|
---|
26 |
|
---|
27 |
|
---|
28 | public ArrayList process(byte[] rawData, int rawLength, int rawSampleRate)
|
---|
29 | {
|
---|
30 | // Save the sample rate of the loaded sample
|
---|
31 | sampleRate = rawSampleRate;
|
---|
32 |
|
---|
33 | // Allocate a new pitch data array to store our pitch values
|
---|
34 | pitchData = new ArrayList();
|
---|
35 |
|
---|
36 | // Reset our 6 pitch period estimators
|
---|
37 | for (int i = 0; i < 6; i++) {
|
---|
38 | ppe[i] = new PitchEstimator();
|
---|
39 | }
|
---|
40 |
|
---|
41 | // Reset our other variables
|
---|
42 | lastMinHeight = 0;
|
---|
43 | lastMaxHeight = 0;
|
---|
44 | lastPeriodStart = 0;
|
---|
45 |
|
---|
46 | // Perform a low pass filter on the raw data
|
---|
47 | lowPassFilter(rawData, rawLength);
|
---|
48 |
|
---|
49 | // Find the peaks that exist in the filtered data
|
---|
50 | findPeaks(filterData, filterLength);
|
---|
51 |
|
---|
52 | return pitchData;
|
---|
53 | }
|
---|
54 |
|
---|
55 |
|
---|
56 | public void lowPassFilter(byte[] rawData, int rawLength)
|
---|
57 | {
|
---|
58 | // I have absolutely no idea where these numbers come from - ask Rodger!!
|
---|
59 | int filters[] = { 26968, 16384, 19494, 21549, 22266, 21549, 19494, 16384, 26968 };
|
---|
60 |
|
---|
61 | // Allocate memory for the filtered data values
|
---|
62 | filterLength = rawLength;
|
---|
63 | filterData = new byte[filterLength];
|
---|
64 |
|
---|
65 | // Perform a low pass filter on the note data
|
---|
66 | for (int rawPos = 0; rawPos < rawLength; rawPos++) {
|
---|
67 |
|
---|
68 | int value = 0;
|
---|
69 | for (int filterNum = 0; filterNum < 9; filterNum++) {
|
---|
70 |
|
---|
71 | // Remember to convert note data value to an unsigned value
|
---|
72 | if ((rawPos - filterNum) >= 0) {
|
---|
73 | value += ((rawData[rawPos - filterNum] & 255) * filters[filterNum]);
|
---|
74 | }
|
---|
75 | // We are out of the array bounds, so use a zero value (unsigned 127)
|
---|
76 | else {
|
---|
77 | value += (127 * filters[filterNum]);
|
---|
78 | }
|
---|
79 | }
|
---|
80 |
|
---|
81 | value = (value + 500888) / 195000;
|
---|
82 | filterData[rawPos] = (byte) value;
|
---|
83 | }
|
---|
84 | }
|
---|
85 |
|
---|
86 |
|
---|
87 | public void findPeaks(byte[] data, int length)
|
---|
88 | {
|
---|
89 | int maxPos = 0, maxHeight = 0;
|
---|
90 | int minPos = 0, minHeight = 0;
|
---|
91 | int peakPos = 0, peakHeight = 0;
|
---|
92 | boolean trackingMaximum = true;
|
---|
93 | boolean lookingAtPeak = false;
|
---|
94 |
|
---|
95 | // Find the peaks that exist in the data
|
---|
96 | for (int pos = 0; pos < length; pos++) {
|
---|
97 |
|
---|
98 | int value = 0;
|
---|
99 |
|
---|
100 | // Are we currently tracking a maximum??
|
---|
101 | if (trackingMaximum == true) {
|
---|
102 | value = (data[pos] & 255) - 127;
|
---|
103 |
|
---|
104 | // Have we found our maximum??
|
---|
105 | if (value < 0) {
|
---|
106 | maxPos = peakPos;
|
---|
107 | maxHeight = peakHeight;
|
---|
108 | peakPos = pos;
|
---|
109 | peakHeight = 0;
|
---|
110 | trackingMaximum = false;
|
---|
111 | lookingAtPeak = false;
|
---|
112 | value = -value;
|
---|
113 | }
|
---|
114 | }
|
---|
115 | // No, so we must be currently tracking a minimum...
|
---|
116 | else {
|
---|
117 | value = 127 - (data[pos] & 255);
|
---|
118 |
|
---|
119 | // Have we found our minimum??
|
---|
120 | if (value < 0) {
|
---|
121 | minPos = peakPos;
|
---|
122 | minHeight = peakHeight;
|
---|
123 | peakPos = pos;
|
---|
124 | peakHeight = 0;
|
---|
125 | trackingMaximum = true;
|
---|
126 | lookingAtPeak = false;
|
---|
127 | value = -value;
|
---|
128 |
|
---|
129 | // Process the Max/Min value that we have found
|
---|
130 | processMaxMin(maxPos, maxHeight, minPos, minHeight);
|
---|
131 | }
|
---|
132 | }
|
---|
133 |
|
---|
134 | // Deal with this sample
|
---|
135 | if (value > peakHeight) {
|
---|
136 | peakHeight = value;
|
---|
137 | peakPos = pos;
|
---|
138 | lookingAtPeak = true;
|
---|
139 | }
|
---|
140 | else if (value < peakHeight && lookingAtPeak == true) {
|
---|
141 | // We have found a maximum
|
---|
142 | peakPos = (peakPos + pos - 1) / 2;
|
---|
143 | lookingAtPeak = false;
|
---|
144 | }
|
---|
145 | }
|
---|
146 | }
|
---|
147 |
|
---|
148 | public void processMaxMin(int maxPos, int maxHeight, int minPos, int minHeight)
|
---|
149 | {
|
---|
150 | // int mil13 = (int) (0.0125 * sampleRate); // !! PURE-ROG !!
|
---|
151 | int eightieth = (int) (sampleRate / 80);
|
---|
152 | int period;
|
---|
153 | boolean newEstimate = false;
|
---|
154 |
|
---|
155 | // Calculate the next pitch estimate
|
---|
156 | period = ppe[0].nextEstimate(maxPos, maxHeight);
|
---|
157 | if (period >= 0) {
|
---|
158 | newEstimate = true;
|
---|
159 | ppe[0].setEstimate(period);
|
---|
160 | }
|
---|
161 |
|
---|
162 | // Calculate the next pitch estimate
|
---|
163 | period = ppe[1].nextEstimate(maxPos, maxHeight + lastMinHeight);
|
---|
164 | if (period >= 0) {
|
---|
165 | newEstimate = true;
|
---|
166 | ppe[1].setEstimate(period);
|
---|
167 | }
|
---|
168 |
|
---|
169 | // Calculate the next pitch estimate
|
---|
170 | if (maxHeight > lastMaxHeight) {
|
---|
171 | period = ppe[2].nextEstimate(maxPos, maxHeight - lastMaxHeight);
|
---|
172 | }
|
---|
173 | else {
|
---|
174 | period = ppe[2].nextEstimate(maxPos, 0);
|
---|
175 | }
|
---|
176 | if (period >= 0) {
|
---|
177 | newEstimate = true;
|
---|
178 | ppe[2].setEstimate(period);
|
---|
179 | }
|
---|
180 |
|
---|
181 | // Calculate the next pitch estimate
|
---|
182 | period = ppe[3].nextEstimate(minPos, minHeight);
|
---|
183 | if (period >= 0) {
|
---|
184 | newEstimate = true;
|
---|
185 | ppe[3].setEstimate(period);
|
---|
186 | }
|
---|
187 |
|
---|
188 | // Calculate the next pitch estimate
|
---|
189 | period = ppe[4].nextEstimate(minPos, minHeight + maxHeight);
|
---|
190 | if (period >= 0) {
|
---|
191 | newEstimate = true;
|
---|
192 | ppe[4].setEstimate(period);
|
---|
193 | }
|
---|
194 |
|
---|
195 | // Calculate the next pitch estimate
|
---|
196 | if (minHeight > lastMinHeight) {
|
---|
197 | period = ppe[5].nextEstimate(minPos, minHeight - lastMinHeight);
|
---|
198 | }
|
---|
199 | else {
|
---|
200 | period = ppe[5].nextEstimate(minPos, 0);
|
---|
201 | }
|
---|
202 | if (period >= 0) {
|
---|
203 | newEstimate = true;
|
---|
204 | ppe[5].setEstimate(period);
|
---|
205 | }
|
---|
206 |
|
---|
207 | // If we have a new estimate, calculate the winner...
|
---|
208 | if (newEstimate == true) {
|
---|
209 | PitchValue winner = winnerCalc.calculateWinner();
|
---|
210 |
|
---|
211 | // Period starts should be strictly increasing...
|
---|
212 | int periodStart = winner.position;
|
---|
213 | if (periodStart <= lastPeriodStart) {
|
---|
214 | periodStart = lastPeriodStart + 1;
|
---|
215 | }
|
---|
216 |
|
---|
217 | // If there has been a gap of more than 13 milliseconds, fill it with zero
|
---|
218 | // if ((lastPeriodStart + mil13) < periodStart) { // !! PURE-ROG !!
|
---|
219 | if ((lastPeriodStart + eightieth) < periodStart) {
|
---|
220 | // pitchData.add(new PitchValue(0, lastPeriodStart + mil13)); // !! PURE-ROG !!
|
---|
221 | pitchData.add(new PitchValue(0, lastPeriodStart + eightieth));
|
---|
222 | }
|
---|
223 |
|
---|
224 | // Save the pitch data
|
---|
225 | pitchData.add(new PitchValue(winner.period, periodStart));
|
---|
226 |
|
---|
227 | lastPeriodStart = periodStart;
|
---|
228 | }
|
---|
229 |
|
---|
230 | // Save the height values for next time
|
---|
231 | lastMaxHeight = maxHeight;
|
---|
232 | lastMinHeight = minHeight;
|
---|
233 | }
|
---|
234 |
|
---|
235 |
|
---|
236 | // ----------------------------------------
|
---|
237 | // Class : PitchEstimator
|
---|
238 | // ----------------------------------------
|
---|
239 | class PitchEstimator
|
---|
240 | {
|
---|
241 | int minSmoothPeriod = (int) (0.0012 * sampleRate);
|
---|
242 | int maxSmoothPeriod = (int) (0.0012 * sampleRate * 10.0);
|
---|
243 | int timeEndBlanking = 0;
|
---|
244 | int timeLastEstimate = 0;
|
---|
245 | int lnLast = 0;
|
---|
246 | int smoothPeriod = minSmoothPeriod;
|
---|
247 | int currentPeriod = 0;
|
---|
248 | int currentPeriodStart = 0;
|
---|
249 | int pitchEstimate[] = { 0, 0, 0, 0, 0, 0 };
|
---|
250 |
|
---|
251 |
|
---|
252 | public int nextEstimate(int time, int height)
|
---|
253 | {
|
---|
254 | // No change in the estimate
|
---|
255 | if (time < timeEndBlanking) {
|
---|
256 | return -1;
|
---|
257 | }
|
---|
258 |
|
---|
259 | int lnValue = 0;
|
---|
260 | if (height != 0) {
|
---|
261 | lnValue = (int) (256 * Math.log((double) height));
|
---|
262 | }
|
---|
263 |
|
---|
264 | // Absolutely no idea at all what this stuff is doing... :-)
|
---|
265 | if (lnValue >= (lnLast - ((178 * (time - timeEndBlanking)) / smoothPeriod))) {
|
---|
266 | currentPeriod = time - timeLastEstimate;
|
---|
267 | currentPeriodStart = timeLastEstimate;
|
---|
268 |
|
---|
269 | smoothPeriod = (smoothPeriod + currentPeriod) / 2;
|
---|
270 | if (smoothPeriod < minSmoothPeriod) {
|
---|
271 | smoothPeriod = minSmoothPeriod;
|
---|
272 | }
|
---|
273 | if (smoothPeriod > maxSmoothPeriod) {
|
---|
274 | smoothPeriod = maxSmoothPeriod;
|
---|
275 | }
|
---|
276 |
|
---|
277 | timeLastEstimate = time;
|
---|
278 | timeEndBlanking = time + (smoothPeriod / 3);
|
---|
279 | lnLast = lnValue;
|
---|
280 |
|
---|
281 | // Return the calculated period estimate
|
---|
282 | return currentPeriod;
|
---|
283 | }
|
---|
284 |
|
---|
285 | // No change in the estimate
|
---|
286 | return -1;
|
---|
287 | }
|
---|
288 |
|
---|
289 |
|
---|
290 | public void setEstimate(int estimate)
|
---|
291 | {
|
---|
292 | pitchEstimate[2] = pitchEstimate[1];
|
---|
293 | pitchEstimate[1] = pitchEstimate[0];
|
---|
294 | pitchEstimate[0] = estimate;
|
---|
295 | pitchEstimate[3] = pitchEstimate[0] + pitchEstimate[1];
|
---|
296 | pitchEstimate[4] = pitchEstimate[1] + pitchEstimate[2];
|
---|
297 | pitchEstimate[5] = pitchEstimate[0] + pitchEstimate[1] + pitchEstimate[2];
|
---|
298 | }
|
---|
299 |
|
---|
300 |
|
---|
301 | public int getEstimate()
|
---|
302 | {
|
---|
303 | return pitchEstimate[0];
|
---|
304 | }
|
---|
305 |
|
---|
306 |
|
---|
307 | public int getEstimatePosition()
|
---|
308 | {
|
---|
309 | return currentPeriodStart;
|
---|
310 | }
|
---|
311 |
|
---|
312 |
|
---|
313 | public void compare(int period, int[] serr, int[] coin, int startNum)
|
---|
314 | {
|
---|
315 | int error;
|
---|
316 | for (int i = startNum; i < 6; i++) {
|
---|
317 | if (period > pitchEstimate[i]) {
|
---|
318 | error = period - pitchEstimate[i];
|
---|
319 | }
|
---|
320 | else {
|
---|
321 | error = pitchEstimate[i] - period;
|
---|
322 | }
|
---|
323 |
|
---|
324 | if (error <= serr[0]) {
|
---|
325 | coin[0]++;
|
---|
326 | }
|
---|
327 | else if (error <= serr[1]) {
|
---|
328 | coin[1]++;
|
---|
329 | }
|
---|
330 | else if (error <= serr[2]) {
|
---|
331 | coin[2]++;
|
---|
332 | }
|
---|
333 | else if (error <= serr[3]) {
|
---|
334 | coin[3]++;
|
---|
335 | }
|
---|
336 | }
|
---|
337 | }
|
---|
338 | }
|
---|
339 |
|
---|
340 |
|
---|
341 | // ----------------------------------------
|
---|
342 | // Class : WinnerCalculator
|
---|
343 | // ----------------------------------------
|
---|
344 | class WinnerCalculator
|
---|
345 | {
|
---|
346 | public PitchValue calculateWinner()
|
---|
347 | {
|
---|
348 | // int mil1 = (int) (0.001 * sampleRate); // !! PURE-ROG !!
|
---|
349 | int thousandth = (int) (sampleRate / 1000);
|
---|
350 | // int mil13 = (int) (0.0125 * sampleRate); // !! PURE-ROG !!
|
---|
351 | int eightieth = (int) (sampleRate / 80);
|
---|
352 | int winningEstimateNum = 0;
|
---|
353 | int winningCoincidence = 0;
|
---|
354 | int winningPeriod = 0;
|
---|
355 |
|
---|
356 | // Create a new PitchData object to store the winning pitch
|
---|
357 | PitchValue winner = new PitchValue(0, 0);
|
---|
358 |
|
---|
359 | // Calculate the winner of the 6 estimates...
|
---|
360 | for (int estNum = 0; estNum < 6; estNum++) {
|
---|
361 |
|
---|
362 | int period = ppe[estNum].getEstimate();
|
---|
363 |
|
---|
364 | // Check that we are within the boundaries of the pitch tracker
|
---|
365 | // if (period > mil1 && period < mil13) { // !! PURE-ROG !!
|
---|
366 | if (period > thousandth && period < eightieth) {
|
---|
367 |
|
---|
368 | // Make sure we haven't calculated coincidences for this period before
|
---|
369 | int checkNum;
|
---|
370 | for (checkNum = 0; checkNum < estNum; checkNum++) {
|
---|
371 | // Check if it has been calculated before
|
---|
372 | if (period == ppe[checkNum].getEstimate()) {
|
---|
373 | break;
|
---|
374 | }
|
---|
375 | }
|
---|
376 |
|
---|
377 | // We need to calculate the coincidences for this period
|
---|
378 | if (checkNum == estNum) {
|
---|
379 | int value = calculateCoincidences(estNum, period);
|
---|
380 | if (value > winningCoincidence) {
|
---|
381 | winningCoincidence = value;
|
---|
382 | winningEstimateNum = estNum;
|
---|
383 | winningPeriod = period;
|
---|
384 | }
|
---|
385 | }
|
---|
386 | }
|
---|
387 | }
|
---|
388 |
|
---|
389 | // Return the winning pitch
|
---|
390 | winner.period = winningPeriod;
|
---|
391 | winner.position = ppe[winningEstimateNum].getEstimatePosition();
|
---|
392 | return winner;
|
---|
393 | }
|
---|
394 |
|
---|
395 |
|
---|
396 | public int calculateCoincidences(int estNum, int period)
|
---|
397 | {
|
---|
398 | // Allocate memory for the coincidence array
|
---|
399 | int[] coin = { 0, 0, 0, 0 };
|
---|
400 |
|
---|
401 | // Allocate memory for some strange array
|
---|
402 | int[] serr = new int[4];
|
---|
403 | for (int i = 0; i < 4; i++) {
|
---|
404 | serr[i] = ((i+1) * period) / 32;
|
---|
405 | if (serr[i] <= 0) {
|
---|
406 | serr[i] = 1;
|
---|
407 | }
|
---|
408 | }
|
---|
409 |
|
---|
410 | // Do something weird...
|
---|
411 | for (int i = 0; i < 6; i++) {
|
---|
412 | if (i == estNum) {
|
---|
413 | ppe[i].compare(period, serr, coin, 1);
|
---|
414 | }
|
---|
415 | else {
|
---|
416 | ppe[i].compare(period, serr, coin, 0);
|
---|
417 | }
|
---|
418 | }
|
---|
419 |
|
---|
420 | // More weird stuff to follow...
|
---|
421 | coin[1] += coin[0];
|
---|
422 | coin[2] += coin[1];
|
---|
423 | coin[3] += coin[2];
|
---|
424 |
|
---|
425 | int maxCoin = (coin[0] - 1);
|
---|
426 | if ((coin[1] - 2) > maxCoin) {
|
---|
427 | maxCoin = (coin[1] - 2);
|
---|
428 | }
|
---|
429 | if ((coin[2] - 5) > maxCoin) {
|
---|
430 | maxCoin = (coin[2] - 5);
|
---|
431 | }
|
---|
432 | if ((coin[3] - 7) > maxCoin) {
|
---|
433 | maxCoin = (coin[3] - 7);
|
---|
434 | }
|
---|
435 |
|
---|
436 | // Return the maximum coincidence value
|
---|
437 | return maxCoin;
|
---|
438 | }
|
---|
439 | }
|
---|
440 | }
|
---|