[315] | 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
|
---|
[1102] | 15 | ArrayList<PitchValue> pitchData;
|
---|
[315] | 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 |
|
---|
[1102] | 27 | public ArrayList<PitchValue> process(byte[] rawData, int rawLength, int rawSampleRate)
|
---|
[315] | 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
|
---|
[1102] | 33 | pitchData = new ArrayList<PitchValue>();
|
---|
[315] | 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 | }
|
---|