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

Last change on this file was 1102, checked in by davidb, 6 years ago

Reworking of the code-base to separate logic from graphics. This version of Expeditee now supports a JFX graphics as an alternative to SWING

File size: 11.3 KB
Line 
1package org.apollo.meldex;
2
3import java.util.ArrayList;
4
5public 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}
Note: See TracBrowser for help on using the repository browser.