Jump To …

pitch.js

apps/jukebox/web/

lobal PitchAnalyzer:true, Float32Array:false, FFT:false

shint undef:true node:true browser:true

PitchAnalyzer = this.PitchAnalyzer = (function () {

var pi  = Math.PI,
  pi2 = pi * 2,
  cos = Math.cos,
  pow = Math.pow,
  log = Math.log,
  max = Math.max,
  min = Math.min,
  abs = Math.abs,
  LN10  = Math.LN10,
  sqrt  = Math.sqrt,
  atan2 = Math.atan2,
  round = Math.round,
  inf = 1/0,
  FFT_P = 10,
  FFT_N = 1 << FFT_P,
  BUF_N = FFT_N * 2;

function remainder (val, div) {
  return val - round(val/div) * div;
}

function extend (obj) {
  var args  = arguments,
    l = args.length,
    i, n;


  for (i=1; i<l; i++){
    for (n in args[i]){
      if (args[i].hasOwnProperty(n)){
        obj[n] = args[i][n];
      }
    }
  }

  return obj;
}

A class for tones.

Params
min:0.0 default:0.0- type:Number freq The frequency of the tone.
max:0.0 default:-Infinity- type:Number db The volume of the tone.
max:0.0 default:-Infinity- type:Number stabledb An average of the volume of the tone.
min:0 default:0- type:Integer age How many times the tone has been detected in a row.
function Tone () {
  this.harmonics = new Float32Array(Tone.MAX_HARM);
}

Tone.prototype = {
  freq: 0.0,
  db: -inf,
  stabledb: -inf,
  age: 0,

  toString: function () {
    return '{freq: ' + this.freq + ', db: ' + this.db + ', stabledb: ' + this.stabledb + ', age: ' + this.age + '}';
  },

Return an approximation of whether the tone has the same frequency as provided.

Returns
BooleanWhether it was a match.
  matches: function (freq) {
    return abs(this.freq / freq - 1.0) < 0.05;
  },

  harmonics: null
};

Tone.MIN_AGE = 2;
Tone.MAX_HARM = 48;

An internal class to manage the peak frequencies detected.

function Peak (freq, db) {
  this.freq = typeof freq === 'undefined' ? this.freq : freq;
  this.db = typeof db === 'undefined' ? this.db : db;

  this.harm = new Array(Tone.MAX_HARM);
}

Peak.prototype = {
  harm: null,

  freq: 0.0,
  db: -inf,

Resets the peak to default values.

  clear: function () {
    this.freq = Peak.prototype.freq;
    this.db   = Peak.prototype.db;
  }
};

Finds the best matching peak from a certain point in the array of peaks.

Returns
PeakThe best matching peak.
Peak.match = function (peaks, pos) {
  var best = pos;

  if (peaks[pos - 1].db > peaks[best].db) best = pos - 1;
  if (peaks[pos + 1].db > peaks[best].db) best = pos + 1;

  return peaks[best];
};

A class to analyze pitch from input data.

function Analyzer (options) {
  options = extend(this, options);

  this.data = new Float32Array(FFT_N);
  this.buffer = new Float32Array(BUF_N);
  this.fftLastPhase = new Float32Array(BUF_N);
  this.tones = [];

  if (this.wnd === null) this.wnd = Analyzer.calculateWindow();
  this.setupFFT();
}

Analyzer.prototype = {
  wnd: null,
  data: null,
  fft: null,
  tones: null,
  fftLastPhase: null,
  buffer: null,

  offset: 0,
  bufRead: 0,
  bufWrite: 0,

  MIN_FREQ: 45,
  MAX_FREQ: 5000,

  sampleRate: 44100,
  step: 200,
  oldFreq: 0.0,

  peak: 0.0,

Gets the current peak level in dB (negative value, 0.0 = clipping).

Returns
NumberThe current peak level (db).
  getPeak: function () {
    return 10.0 * log(this.peak) / LN10;
  },

  findTone: function (minFreq, maxFreq) {
    if (!this.tones.length) {
      this.oldFreq = 0.0;
      return null;
    }

    minFreq = typeof minFreq === 'undefined' ? 65.0 : minFreq;
    maxFreq = typeof maxFreq === 'undefined' ? 1000.0 : maxFreq;

    var db = max.apply(null, this.tones.map(Analyzer.mapdb));
    var best = null;
    var bestscore = 0;

    for (var i=0; i<this.tones.length; i++) {
      if (this.tones[i].db < db - 20.0 || this.tones[i].freq < minFreq || this.tones[i].age < Tone.MIN_AGE) continue;
      if (this.tones[i].freq > maxFreq) break;

      var score = this.tones[i].db - max(180.0, abs(this.tones[i].freq - 300)) / 10.0;

      if (this.oldFreq !== 0.0 && abs(this.tones[i].freq / this.oldFreq - 1.0) < 0.05) score += 10.0;
      if (best && bestscore > score) break;

      best = this.tones[i];
      bestscore = score;
    }

    this.oldFreq = (best ? best.freq : 0.0);
    return best;
  },

Copies data to the internal buffers for processing and calculates peak.
Note that if the buffer overflows, unprocessed data gets discarded.

  input: function (data) {
    var buf = this.buffer;
    var r = this.bufRead;
    var w = this.bufWrite;

    var overflow = false;

    for (var i=0; i<data.length; i++) {
      var s = data[i];
      var p = s * s;

      if (p > this.peak) this.peak = p; else this.peak *= 0.999;

      buf[w] = s;

      w = (w + 1) % BUF_N;

      if (w === r) overflow = true;
    }

    this.bufWrite = w;
    if (overflow) this.bufRead = (w + 1) % BUF_N;
  },

Processes available data and calculates tones.

  process: function () {
    while (this.calcFFT()) this.calcTones();
  },

Matches new tones against old ones, merging similar ones.

  mergeWithOld: function (tones) {
    var i, n;

    tones.sort(function (a, b) { return a.freq < b.freq ? -1 : a.freq > b.freq ? 1 : 0; });

    for (i=0, n=0; i<this.tones.length; i++) {
      while (n < tones.length && tones[n].freq < this.tones[i].freq) n++;

      if (n < tones.length && tones[n].matches(this.tones[i].freq)) {
        tones[n].age = this.tones[i].age + 1;
        tones[n].stabledb = 0.8 * this.tones[i].stabledb + 0.2 * tones[n].db;
        tones[n].freq = 0.5 * (this.tones[i].freq + tones[n].freq);
      } else if (this.tones[i].db > -80.0) {
        tones.splice(n, 0, this.tones[i]);
        tones[n].db -= 5.0;
        tones[n].stabledb -= 0.1;
      }

    }
  },

Calculates the tones from the FFT data.

  calcTones: function () {
    var freqPerBin  = this.sampleRate / FFT_N,
      phaseStep = pi2 * this.step / FFT_N,
      normCoeff = 1.0 / FFT_N,
      minMagnitude  = pow(10, -100.0 / 20.0) / normCoeff,
      kMin    = ~~max(1, this.MIN_FREQ / freqPerBin),
      kMax    = ~~min(FFT_N / 2, this.MAX_FREQ / freqPerBin),
      peaks   = [],
      tones   = [],
      k, k2, p, n, t, count, freq, magnitude, phase, delta, prevdb, db, bestDiv,
      bestScore, div, score;

    for (k=0; k <= kMax; k++) {
      peaks.push(new Peak());
    }

    for (k=1, k2=2; k<=kMax; k++, k2 += 2) {

complex absolute

      magnitude = sqrt(this.fft[k2] * this.fft[k2] + this.fft[k2+1] * this.fft[k2+1]);

complex arguscosine

      phase = atan2(this.fft[k2+1], this.fft[k2]);

      delta = phase - this.fftLastPhase[k];
      this.fftLastPhase[k] = phase;

      delta -= k * phaseStep;
      delta = remainder(delta, pi2);
      delta /= phaseStep;

      freq = (k + delta) * freqPerBin;

      if (freq > 1.0 && magnitude > minMagnitude) {
        peaks[k].freq = freq;
        peaks[k].db = 20.0 * log(normCoeff * magnitude) / LN10;
      }
    }

    prevdb = peaks[0].db;

    for (k=1; k<kMax; k++) {
      db = peaks[k].db;
      if (db > prevdb) peaks[k - 1].clear();
      if (db < prevdb) peaks[k].clear();
      prevdb = db;
    }

    for (k=kMax-1; k >= kMin; k--) {
      if (peaks[k].db < -70.0) continue;

      bestDiv = 1;
      bestScore = 0;

      for (div = 2; div <= Tone.MAX_HARM && k / div > 1; div++) {
        freq = peaks[k].freq / div;
        score = 0;

        for (n=1; n<div && n<8; n++) {
          p = Peak.match(peaks, ~~(k * n / div));
          score--;
          if (p.db < -90.0 || abs(p.freq / n / freq - 1.0) > 0.03) continue;
          if (n === 1) score += 4;
          score += 2;
        }

        if (score > bestScore) {
          bestScore = score;
          bestDiv = div;
        }
      }

      t = new Tone();

      count = 0;

      freq = peaks[k].freq / bestDiv;

      t.db = peaks[k].db;

      for (n=1; n<=bestDiv; n++) {
        p = Peak.match(peaks, ~~(k * n / bestDiv));

        if (abs(p.freq / n / freq - 1.0) > 0.03) continue;

        if (p.db > t.db - 10.0) {
          t.db = max(t.db, p.db);
          count++;
          t.freq += p.freq / n;
        }

        t.harmonics[n - 1] = p.db;
        p.clear();
      }

      t.freq /= count;

      if (t.db > -50.0 - 3.0 * count) {
        t.stabledb = t.db;
        tones.push(t);
      }
    }

    this.mergeWithOld(tones);

    this.tones = tones;
  },

Calculates the FFT for the input signal, if enough is available.

Returns
BooleanWhether there was enough data to process.
  calcFFT: function () {
    var r = this.bufRead;

    if ((BUF_N + this.bufWrite - r) % BUF_N <= FFT_N) return false;

    for (var i=0; i<FFT_N; i++) {
      this.data[i] = this.buffer[(r + i) % BUF_N];
    }

    this.bufRead = (r + this.step) % BUF_N;

    this.processFFT(this.data, this.wnd);

    return true;
  },

  setupFFT: function () {
    var RFFT = typeof FFT !== 'undefined' && FFT;

    if (!RFFT) {
      try {
        RFFT = require('fft');
      } catch (e) {
        throw Error("pitch.js requires fft.js");
      }
    }

    RFFT = RFFT.complex;

    this.rfft = new RFFT(FFT_N, false);
    this.fft = new Float32Array(FFT_N * 2);
    this.fftInput = new Float32Array(FFT_N);
  },

  processFFT: function (data, wnd) {
    var i;

    for (i=0; i<data.length; i++) {
      this.fftInput[i] = data[i] * wnd[i];
    }

    this.rfft.simple(this.fft, this.fftInput, 'real');
  }
};

Analyzer.mapdb = function (e) {
  return e.db;
};

Analyzer.Tone = Tone;

Calculates a Hamming window for the size FFT_N, scaled up with FFT_N.

Returns
Float32ArrayThe hamming window.
Analyzer.calculateWindow = function () {
  var i,
    w = new Float32Array(FFT_N);

  for (i=0; i<FFT_N; i++) {
    w[i] = 0.53836 - 0.46164 * cos(pi2 * i / (FFT_N - 1));
  }

  return w;
};

return Analyzer;

}());

if (typeof module !== 'undefined') {
  module.exports = PitchAnalyzer;
}

JukeboxJS