source: trunk/src/org/apollo/meldex/PitchTracker.java@ 1006

Last change on this file since 1006 was 315, checked in by bjn8, 16 years ago

Apollo spin-off added

File size: 11.3 KB
Line 
1package org.apollo.meldex;
2
3import java.util.ArrayList;
4
5@SuppressWarnings("unchecked") // code in java 1.4
6public 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}
Note: See TracBrowser for help on using the repository browser.