1/*global PitchAnalyzer:true, Float32Array:false, FFT:false */ 2/*jshint undef:true node:true browser:true */ 3 4PitchAnalyzer = this.PitchAnalyzer = (function () { 5 6var pi = Math.PI, 7 pi2 = pi * 2, 8 cos = Math.cos, 9 pow = Math.pow, 10 log = Math.log, 11 max = Math.max, 12 min = Math.min, 13 abs = Math.abs, 14 LN10 = Math.LN10, 15 sqrt = Math.sqrt, 16 atan2 = Math.atan2, 17 round = Math.round, 18 inf = 1/0, 19 FFT_P = 10, 20 FFT_N = 1 << FFT_P, 21 BUF_N = FFT_N * 2; 22 23function remainder (val, div) { 24 return val - round(val/div) * div; 25} 26 27function extend (obj) { 28 var args = arguments, 29 l = args.length, 30 i, n; 31 32 33 for (i=1; i<l; i++){ 34 for (n in args[i]){ 35 if (args[i].hasOwnProperty(n)){ 36 obj[n] = args[i][n]; 37 } 38 } 39 } 40 41 return obj; 42} 43 44/** 45 * A class for tones. 46 * 47 * @class 48 * @static PitchAnalyzer 49 * @param default:0.0 min:0.0 type:Number freq The frequency of the tone. 50 * @param default:-Infinity max:0.0 type:Number db The volume of the tone. 51 * @param default:-Infinity max:0.0 type:Number stabledb An average of the volume of the tone.
52 * @param default:0 min:0 type:Integer age How many times the tone has been detected in a row. 53*/ 54function Tone () { 55 this.harmonics = new Float32Array(Tone.MAX_HARM); 56} 57 58Tone.prototype = { 59 freq: 0.0, 60 db: -inf, 61 stabledb: -inf, 62 age: 0, 63 64 toString: function () { 65 return '{freq: ' + this.freq + ', db: ' + this.db + ', stabledb: ' + this.stabledb + ', age: ' + this.age + '}'; 66 }, 67 68/** 69 * Return an approximation of whether the tone has the same frequency as provided. 70 * 71 * @method Tone 72 * @private 73 * @arg {Number} freq The frequency to compare to. 74 * @return {Boolean} Whether it was a match. 75*/ 76 matches: function (freq) { 77 return abs(this.freq / freq - 1.0) < 0.05; 78 }, 79 80 harmonics: null 81}; 82 83Tone.MIN_AGE = 2; 84Tone.MAX_HARM = 48; 85 86/** 87 * An internal class to manage the peak frequencies detected. 88 * 89 * @private 90 * @class 91 * @static PitchAnalyzer 92 * @arg default:0.0 min:0.0 type:Number !freq The frequency of the peak. 93 * @arg default:-Infinity max:0.0 type:Number !db The volume of the peak. 94*/ 95function Peak (freq, db) { 96 this.freq = typeof freq === 'undefined' ? this.freq : freq; 97 this.db = typeof db === 'undefined' ? this.db : db; 98 99 this.harm = new Array(Tone.MAX_HARM); 100} 101 102Peak.prototype = { 103 harm: null, 104 105 freq: 0.0, 106 db: -inf, 107 108/** 109 * Resets the peak to default values. 110 * 111 * @method Peak 112 * @private 113*/ 114 clear: function () { 115 this.freq = Peak.prototype.freq; 116 this.db = Peak.prototype.db; 117 } 118}; 119 120/** 121 * Finds the best matching peak from a certain point in the array of peaks. 122 * 123 * @name match 124 * @static Peak 125 * @private 126 * @arg {Array} peaks The peaks to search from. 127 * @arg {Integer} pos The position to find the match for. 128 * @return {Peak} The best matching peak. 129*/ 130Peak.match = function (peaks, pos) { 131 var best = pos; 132 133 if (peaks[pos - 1].db > peaks[best].db) best = pos - 1; 134 if (peaks[pos + 1].db > peaks[best].db) best = pos + 1; 135 136 return peaks[best]; 137}; 138 139/** 140 * A class to analyze pitch from input data. 141 * 142 * @class PitchAnalyzer 143 * @arg {Object} !options Options to override default values. 144*/ 145function Analyzer (options) { 146 options = extend(this, options); 147 148 this.data = new Float32Array(FFT_N); 149 this.buffer = new Float32Array(BUF_N); 150 this.fftLastPhase = new Float32Array(BUF_N); 151 this.tones = []; 152 153 if (this.wnd === null) this.wnd = Analyzer.calculateWindow(); 154 this.setupFFT(); 155} 156 157Analyzer.prototype = { 158 wnd: null, 159 data: null, 160 fft: null, 161 tones: null, 162 fftLastPhase: null, 163 buffer: null, 164 165 offset: 0, 166 bufRead: 0, 167 bufWrite: 0, 168 169 MIN_FREQ: 45, 170 MAX_FREQ: 5000, 171 172 sampleRate: 44100, 173 step: 200, 174 oldFreq: 0.0, 175 176 peak: 0.0, 177 178/** 179 * Gets the current peak level in dB (negative value, 0.0 = clipping). 180 * 181 * @method PitchAnalyzer 182 * @return {Number} The current peak level (db). 183*/ 184 getPeak: function () { 185 return 10.0 * log(this.peak) / LN10; 186 }, 187 188 findTone: function (minFreq, maxFreq) { 189 if (!this.tones.length) { 190 this.oldFreq = 0.0; 191 return null; 192 } 193 194 minFreq = typeof minFreq === 'undefined' ? 65.0 : minFreq; 195 maxFreq = typeof maxFreq === 'undefined' ? 1000.0 : maxFreq; 196 197 var db = max.apply(null, this.tones.map(Analyzer.mapdb)); 198 var best = null; 199 var bestscore = 0; 200 201 for (var i=0; i<this.tones.length; i++) { 202 if (this.tones[i].db < db - 20.0 || this.tones[i].freq < minFreq || this.tones[i].age < Tone.MIN_AGE) continue; 203 if (this.tones[i].freq > maxFreq) break; 204 205 var score = this.tones[i].db - max(180.0, abs(this.tones[i].freq - 300)) / 10.0; 206 207 if (this.oldFreq !== 0.0 && abs(this.tones[i].freq / this.oldFreq - 1.0) < 0.05) score += 10.0; 208 if (best && bestscore > score) break; 209 210 best = this.tones[i]; 211 bestscore = score; 212 } 213 214 this.oldFreq = (best ? best.freq : 0.0); 215 return best; 216 }, 217 218/** 219 * Copies data to the internal buffers for processing and calculates peak. 220 * Note that if the buffer overflows, unprocessed data gets discarded. 221 * 222 * @method PitchAnalyzer 223 * @arg {Float32Array} data The input data. 224*/ 225 input: function (data) { 226 var buf = this.buffer; 227 var r = this.bufRead; 228 var w = this.bufWrite; 229 230 var overflow = false; 231 232 for (var i=0; i<data.length; i++) { 233 var s = data[i]; 234 var p = s * s; 235 236 if (p > this.peak) this.peak = p; else this.peak *= 0.999; 237 238 buf[w] = s; 239
240 w = (w + 1) % BUF_N; 241 242 if (w === r) overflow = true; 243 } 244 245 this.bufWrite = w; 246 if (overflow) this.bufRead = (w + 1) % BUF_N; 247 }, 248 249/** 250 * Processes available data and calculates tones. 251 * 252 * @method PitchAnalyzer 253*/ 254 process: function () { 255 while (this.calcFFT()) this.calcTones(); 256 }, 257 258/** 259 * Matches new tones against old ones, merging similar ones. 260 * 261 * @method PitchAnalyzer 262 * @private 263*/ 264 mergeWithOld: function (tones) { 265 var i, n; 266 267 tones.sort(function (a, b) { return a.freq < b.freq ? -1 : a.freq > b.freq ? 1 : 0; }); 268 269 for (i=0, n=0; i<this.tones.length; i++) { 270 while (n < tones.length && tones[n].freq < this.tones[i].freq) n++; 271 272 if (n < tones.length && tones[n].matches(this.tones[i].freq)) { 273 tones[n].age = this.tones[i].age + 1; 274 tones[n].stabledb = 0.8 * this.tones[i].stabledb + 0.2 * tones[n].db; 275 tones[n].freq = 0.5 * (this.tones[i].freq + tones[n].freq); 276 } else if (this.tones[i].db > -80.0) { 277 tones.splice(n, 0, this.tones[i]); 278 tones[n].db -= 5.0; 279 tones[n].stabledb -= 0.1; 280 } 281 282 } 283 }, 284 285/** 286 * Calculates the tones from the FFT data. 287 * 288 * @method PitchAnalyzer 289 * @private 290*/ 291 calcTones: function () { 292 var freqPerBin = this.sampleRate / FFT_N, 293 phaseStep = pi2 * this.step / FFT_N, 294 normCoeff = 1.0 / FFT_N, 295 minMagnitude = pow(10, -100.0 / 20.0) / normCoeff, 296 kMin = ~~max(1, this.MIN_FREQ / freqPerBin), 297 kMax = ~~min(FFT_N / 2, this.MAX_FREQ / freqPerBin), 298 peaks = [], 299 tones = [], 300 k, k2, p, n, t, count, freq, magnitude, phase, delta, prevdb, db, bestDiv, 301 bestScore, div, score; 302 303 for (k=0; k <= kMax; k++) { 304 peaks.push(new Peak()); 305 } 306 307 for (k=1, k2=2; k<=kMax; k++, k2 += 2) { 308 /* complex absolute */ 309 magnitude = sqrt(this.fft[k2] * this.fft[k2] + this.fft[k2+1] * this.fft[k2+1]); 310 /* complex arguscosine */ 311 phase = atan2(this.fft[k2+1], this.fft[k2]);
312 313 delta = phase - this.fftLastPhase[k]; 314 this.fftLastPhase[k] = phase; 315 316 delta -= k * phaseStep; 317 delta = remainder(delta, pi2); 318 delta /= phaseStep; 319 320 freq = (k + delta) * freqPerBin; 321 322 if (freq > 1.0 && magnitude > minMagnitude) { 323 peaks[k].freq = freq; 324 peaks[k].db = 20.0 * log(normCoeff * magnitude) / LN10; 325 } 326 } 327 328 prevdb = peaks[0].db; 329 330 for (k=1; k<kMax; k++) { 331 db = peaks[k].db; 332 if (db > prevdb) peaks[k - 1].clear(); 333 if (db < prevdb) peaks[k].clear(); 334 prevdb = db; 335 } 336 337 for (k=kMax-1; k >= kMin; k--) { 338 if (peaks[k].db < -70.0) continue; 339 340 bestDiv = 1; 341 bestScore = 0; 342 343 for (div = 2; div <= Tone.MAX_HARM && k / div > 1; div++) { 344 freq = peaks[k].freq / div; 345 score = 0; 346 347 for (n=1; n<div && n<8; n++) { 348 p = Peak.match(peaks, ~~(k * n / div)); 349 score--; 350 if (p.db < -90.0 || abs(p.freq / n / freq - 1.0) > 0.03) continue; 351 if (n === 1) score += 4; 352 score += 2; 353 } 354 355 if (score > bestScore) { 356 bestScore = score; 357 bestDiv = div; 358 } 359 } 360 361 t = new Tone(); 362 363 count = 0; 364 365 freq = peaks[k].freq / bestDiv; 366 367 t.db = peaks[k].db; 368 369 for (n=1; n<=bestDiv; n++) { 370 p = Peak.match(peaks, ~~(k * n / bestDiv)); 371 372 if (abs(p.freq / n / freq - 1.0) > 0.03) continue; 373 374 if (p.db > t.db - 10.0) { 375 t.db = max(t.db, p.db); 376 count++; 377 t.freq += p.freq / n; 378 } 379 380 t.harmonics[n - 1] = p.db; 381 p.clear(); 382 } 383 384 t.freq /= count; 385 386 if (t.db > -50.0 - 3.0 * count) { 387 t.stabledb = t.db; 388 tones.push(t); 389 } 390 } 391 392 this.mergeWithOld(tones); 393 394 this.tones = tones; 395 }, 396 397/** 398 * Calculates the FFT for the input signal, if enough is available. 399 * 400 * @method PitchAnalyzer 401 * @private 402 * @return {Boolean} Whether there was enough data to process. 403*/ 404 calcFFT: function () { 405 var r = this.bufRead; 406 407 if ((BUF_N + this.bufWrite - r) % BUF_N <= FFT_N) return false; 408 409 for (var i=0; i<FFT_N; i++) { 410 this.data[i] = this.buffer[(r + i) % BUF_N]; 411 } 412 413 this.bufRead = (r + this.step) % BUF_N; 414 415 this.processFFT(this.data, this.wnd); 416 417 return true; 418 }, 419 420 setupFFT: function () { 421 var RFFT = typeof FFT !== 'undefined' && FFT; 422 423 if (!RFFT) { 424 try { 425 RFFT = require('fft'); 426 } catch (e) { 427 throw Error("pitch.js requires fft.js"); 428 } 429 } 430 431 RFFT = RFFT.complex; 432 433 this.rfft = new RFFT(FFT_N, false); 434 this.fft = new Float32Array(FFT_N * 2); 435 this.fftInput = new Float32Array(FFT_N); 436 }, 437 438 processFFT: function (data, wnd) { 439 var i; 440 441 for (i=0; i<data.length; i++) { 442 this.fftInput[i] = data[i] * wnd[i]; 443 } 444 445 this.rfft.simple(this.fft, this.fftInput, 'real'); 446 } 447}; 448 449Analyzer.mapdb = function (e) { 450 return e.db; 451}; 452 453Analyzer.Tone = Tone; 454 455/** 456 * Calculates a Hamming window for the size FFT_N, scaled up with FFT_N. 457 * 458 * @static PitchAnalyzer 459 * @return {Float32Array} The hamming window. 460*/ 461Analyzer.calculateWindow = function () { 462 var i, 463 w = new Float32Array(FFT_N); 464 465 for (i=0; i<FFT_N; i++) { 466 w[i] = 0.53836 - 0.46164 * cos(pi2 * i / (FFT_N - 1)); 467 } 468 469 return w; 470}; 471 472return Analyzer; 473 474}()); 475 476if (typeof module !== 'undefined') { 477 module.exports = PitchAnalyzer; 478}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.