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