Source: vowelworm.js

(function(numeric){
"use strict";

/**
 * @namespace
 * @const
 * @ignore
 */
var VowelWorm = {};

/**
 * @namespace
 * @name VowelWorm
 */
window.VowelWorm = VowelWorm;

/**
 * @const
 */
var CONTEXT = new window.AudioContext();

/**
 * A collection of all vowel worm instances. Used for attaching modules.
 * @see {@link VowelWorm.module}
 * @type {Array.<window.VowelWorm.instance>}
 */
var instances = [];

/**
 * A collection of modules to add to instances, whenever they are created
 * @type {Object.<string, Function>}
 */
var modules = {};

/**
 * The sample rate used when one cannot be found.
 */
var DEFAULT_SAMPLE_RATE = 44100;

/**
 * From both Wikipedia (http://en.wikipedia.org/wiki/Formant; retrieved 23 Jun.
 * 2014, 2:52 PM UTC) and Cory Robinson's chart (personal email)
 *
 * These indicate the minimum values in Hz in which we should find our formants
 */

/**
 * @const
 * @type number
 */
var F1_MIN = 100;
/**
 * @const
 * @type number
 */
var F1_MAX = 1000;
/**
 * @const
 * @type number
 */
var F2_MIN = 600;
/**
 * @const
 * @type number
 */
var F2_MAX = 3000;
/**
 * @const
 * @type number
 */
var F3_MIN = 1500;
/**
 * @const
 * @type number
 */
var F3_MAX = 5000;

/**
 * Represent the minimum differences between formants, to ensure they are
 * properly spaced
 *
 * TODO ensure accuracy; find official source
 */

/**
 * @const
 * @type number
 */
var MIN_DIFF_F1_F2 = 150;
/**
 * @const
 * @type number
 */
var MIN_DIFF_F2_F3 = 500;

/**
 * Specifies that a peak must be this many decibels higher than the closest
 * valleys to be considered a formant
 *
 * TODO ensure accuracy; find official source
 * @constant
 * @type number
 */
var MIN_PEAK_HEIGHT = 0.1;

/**
 * All window sizes to try when pulling data, in the order they should
 * be tried
 * @see {@link VowelWorm._HANNING_WINDOW}
 * @constant
 * @type Array.<number>
 */
var WINDOW_SIZES = [
  75,
  61
];


/***
 * Contains precomputed values for the Hanning function at specific window
 * lengths.
 *
 * From Python's numpy.hanning(x) method.
 *
 * @see {@link WINDOW_SIZES}
 *
 * @constant
 * @type {Object.<number, Array.<number>>}
 * @private
 * @memberof VowelWorm
 */
VowelWorm._HANNING_WINDOW = {
  61: new Float32Array([ 0.        ,  0.00273905,  0.0109262 ,  0.02447174,  0.04322727,
        0.0669873 ,  0.0954915 ,  0.12842759,  0.1654347 ,  0.20610737,
        0.25      ,  0.29663168,  0.3454915 ,  0.39604415,  0.44773577,
        0.5       ,  0.55226423,  0.60395585,  0.6545085 ,  0.70336832,
        0.75      ,  0.79389263,  0.8345653 ,  0.87157241,  0.9045085 ,
        0.9330127 ,  0.95677273,  0.97552826,  0.9890738 ,  0.99726095,
        1.        ,  0.99726095,  0.9890738 ,  0.97552826,  0.95677273,
        0.9330127 ,  0.9045085 ,  0.87157241,  0.8345653 ,  0.79389263,
        0.75      ,  0.70336832,  0.6545085 ,  0.60395585,  0.55226423,
        0.5       ,  0.44773577,  0.39604415,  0.3454915 ,  0.29663168,
        0.25      ,  0.20610737,  0.1654347 ,  0.12842759,  0.0954915 ,
        0.0669873 ,  0.04322727,  0.02447174,  0.0109262 ,  0.00273905,  0.        ]),

  75: new Float32Array([ 0.        ,  0.00180126,  0.00719204,  0.01613353,  0.02856128,
          0.04438575,  0.06349294,  0.08574518,  0.11098212,  0.13902195,
          0.16966264,  0.20268341,  0.23784636,  0.27489813,  0.31357176,
          0.35358861,  0.39466037,  0.43649109,  0.4787794 ,  0.5212206 ,
          0.56350891,  0.60533963,  0.64641139,  0.68642824,  0.72510187,
          0.76215364,  0.79731659,  0.83033736,  0.86097805,  0.88901788,
          0.91425482,  0.93650706,  0.95561425,  0.97143872,  0.98386647,
          0.99280796,  0.99819874,  1.        ,  0.99819874,  0.99280796,
          0.98386647,  0.97143872,  0.95561425,  0.93650706,  0.91425482,
          0.88901788,  0.86097805,  0.83033736,  0.79731659,  0.76215364,
          0.72510187,  0.68642824,  0.64641139,  0.60533963,  0.56350891,
          0.5212206 ,  0.4787794 ,  0.43649109,  0.39466037,  0.35358861,
          0.31357176,  0.27489813,  0.23784636,  0.20268341,  0.16966264,
          0.13902195,  0.11098212,  0.08574518,  0.06349294,  0.04438575,
          0.02856128,  0.01613353,  0.00719204,  0.00180126,  0.        ])
};

/**
 * Contains methods for normalizing Hz values
 * @const
 * @namespace
 * @memberof VowelWorm
 * @name VowelWorm.Normalization
 */
VowelWorm.Normalization = {
  /**
   * Uses the Traunmüller conversion to conver the formant to the Bark Scale
   * @param {number} formant The formant (in Hz) to convert to the Bark Scale
   * @return {number} The formant converted to the Bark Scale
   */
  barkScale: function barkScale(formant) {
    if(formant == 0) {
      formant = 1;
    }
    return 26.81/(1+(1960/formant)) - 0.53;
  }
};

/**
 * @license
 *
 * VowelWorm.decibelsToLinear licensed under the following:
 * 
 * Copyright (C) 2010, Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1.  Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2.  Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * END VowelWorm.decibelsToLinear LICENSE
 */

/**
 * Returns the linear magnitude of the given decibels value.
 * @param {number} dB the value in dB to convert
 * @return {number} the linear magnitude
 * 
 * @TODO — If we can find a generic representation somewhere of this algorithm,
 * we can remove this license
 * @public
 * @memberof VowelWorm
 */
VowelWorm.decibelsToLinear = function(dB) {
  return Math.pow(10, 0.05 * dB);
};

/**
 * @license
 *
 * Hanning window code taken from http://wiki.scipy.org/Cookbook/SignalSmooth
 * both constant values and code preparing data for convolution
 *
 */
/**
 * Applies a hanning window to the given dataset, returning a new array.
 * You  may want to shift the values to get them to line up with the FFT.
 * @example
 *  VowelWorm.hann([...], 75).shift(VowelWorm.HANNING_SHIFT);
 * @see {@link VowelWorm.HANNING_SHIFT}
 * @param {Array.<number>} vals The values to change
 * @param {number} window_size the size of the window
 * @return {Array.<number>} the new values 
 * @memberof VowelWorm
 */
VowelWorm.hann = function hann(vals, window_size) {
  if(typeof VowelWorm._HANNING_WINDOW[window_size] === 'undefined') {
    throw new Error('No precomputed Hanning Window values found for ' +
        window_size);
  }

  var s = [];

  for(var i = window_size-1; i > 0; i--) {
    s.push(vals[i]);
  }
  for(var i = 0; i<vals.length; i++) {
    s.push(vals[i]);
  }
  for(var i = vals.length-1; i>vals.length-window_size; i--) {
    s.push(vals[i]);
  }

  var w = VowelWorm._HANNING_WINDOW[window_size];

  var sum = 0;
  var wMorph = [];
  for(var i = 0; i<w.length; i++) {
    sum += w[i];
  }
  for(var i = 0; i<w.length; i++) {
    wMorph[i] = w[i]/sum;
  }
  return VowelWorm.convolve(wMorph, s);
};

/**
 * @license
 *
 * Savitsky-Golay filter (VowelWorm.savitzkyGolay)
 * adapted from http://wiki.scipy.org/Cookbook/SavitzkyGolay
 *
 */
/**
 * Applies the Savitsky-Golay filter to the given array
 * uses numeric javascript
 * Adapted from http://wiki.scipy.org/Cookbook/SavitzkyGolay
 * @param {Array.<number>} y The values to smooth
 * @param {number} window_size The window size.
 * @param {number} order The...? TODO
 * @return {Array.<number>} if plotted gives you a smooth curve version of an parameter array
 * @memberof VowelWorm
 */
VowelWorm.savitzkyGolay = function savitzkyGolay(y, window_size, order) {
  //probably we don't need to parseInt anything or take the absolute value if we always make sure that our windown size and order are positive.  "golay.py" gave a window size of 55 and said that anything higuer will make a flatter graph
  //window size must be positive and an odd number for this to work better
  var windowSize = Math.abs(parseInt(window_size, 10));
  order = Math.abs(parseInt(order, 10));
  var order_range = order + 1;

  var half_window = (windowSize - 1)/2;
  var b = new Array();

  for(var k = -half_window; k < half_window+1; k++) {
    var row = new Array();
    for(var i = 0; i < order_range; i++) {
      row.push(Math.pow(k,i));
    }
    b.push(row);
  }
  //This line needs to be changed if you use something other than 0 for derivative
  var temp = pinv(b);
  var m = temp[0];
  //if you take a look at firstvals in the python code, and then at this code you'll see that I've only broken firstvals down into different parts such as first taking a sub array, flipping it, and so on
  var yTemp = new Array();
  yTemp = y.subarray ? y.subarray(1, half_window+1) :  y.slice(1, half_window+1);
  yTemp = flipArray(yTemp);
  yTemp = addToArray(yTemp, -y[0]);
  yTemp = arrayAbs(yTemp);
  yTemp = negArrayAddValue(yTemp, y[0]);
  var firstvals = yTemp;

  //Same thing was done for lastvals
  var yTemp2 = new Array();
  yTemp2 = y.subarray ? y.subarray(-half_window -1, -1) : y.slice(-half_window -1, -1);
  yTemp2 = flipArray(yTemp2);
  yTemp2 = addToArray(yTemp2, -y[y.length-1]);
  yTemp2 = arrayAbs(yTemp2);
  yTemp2 = addToArray(yTemp2, y[y.length-1]);
  var lastvals = yTemp2;

  y = concatenate(firstvals, y, lastvals);
  m = flipArray(m);
  var result = new Array();
  result = VowelWorm.convolve(m,y);
  return result;
};

/**
 * Performs a convolution on two arrays
 * @param {Array.<number>} m
 * @param {Array.<number>} y
 * @return {Array.<number>}
 * @memberof VowelWorm
 * 
 */
VowelWorm.convolve = function convolve(m, y) {
  var result = new Array(),
      first  = null,
      second = null;

  if(m.length > y.length) {
    first  = y;
    second = m;
  }
  else
  {
    first  = m;
    second = y;
  }
  var size = second.length - first.length + 1;
  for(var i = 0; i < size; i++) {
    var newNum = 0,
        len = first.length;

    for(var j = 0; j < first.length; j++) {
      newNum = newNum + first[len-1-j]*second[j+i];
    }
    result.push(newNum);
  }
  return result;
};

/**
 * Representative of the current mode VowelWorm is in.
 * In this case, an audio element
 * @const
 * @memberof VowelWorm
 */
VowelWorm.AUDIO = 1;

/**
 * Representative of the current mode VowelWorm is in.
 * In this case, a video element
 * @const
 * @memberof VowelWorm
 */
VowelWorm.VIDEO = 2;

/**
 * Representative of the current mode VowelWorm is in.
 * In this case, a media stream
 * @const
 * @memberof VowelWorm
 */
VowelWorm.STREAM = 3;

/**
 * Representative of the current mode VowelWorm is in.
 * In this case, a remote URL turned into a source node
 * @const
 * @memberof VowelWorm
 */
VowelWorm.REMOTE_URL = 4;

/*******************
 * HELPER FUNCTIONS
 * Most of these are attached to VowelWorm so they can be easily tested
 *******************/

/**
 * @license
 * 
 * VowelWorm._toFrequency method developed with help from kr1 at
 * {@link http://stackoverflow.com/questions/14789283/what-does-the-fft-data-in-the-web-audio-api-correspond-to}
 */
/**
 * Gets the frequency at the given index
 * @param {number} position the position of the data to get the frequency of
 * @param {number} sampleRate the sample rate of the data, in Hz
 * @param {number} fftSize the FFT size
 * @return {number} the frequency at the given index
 * @private
 * @memberof VowelWorm
 */
VowelWorm._toFrequency = function toFrequency(position, sampleRate, fftSize) {
  /**
   * I am dividing by two because both Praat and WaveSurfer correlate the
   * final FFT bin with the Hz value of only half of the sample rate.
   *
   * This halving creates what is called the Nyquist Frequency (see
   * http://www.fon.hum.uva.nl/praat/manual/Nyquist_frequency.html and
   * http://en.wikipedia.org/wiki/Nyquist_frequency).
   *
   * For example, an FFT of size 2048 will have 1024 bins. With a sample rate
   * of 16000 (16kHz), the final position, 1023 (the 1024th bin) should return
   * 8000 (8kHz). Position 511 (512th bin) should return 4000 (4kHz), and so
   * on.
   *
   * Kudos to Derrick Craven for discovering that we needed to divide this.
   */
  var nyquist = sampleRate/2;

  var totalBins = fftSize/2;

  return position*(nyquist/totalBins);
};

/**
 * Returns the smallest side of a given peak, based on its valleys
 * to the left and to the right. If a peak occurs in index 0 or
 * values.length -1 (i.e., the leftmost or rightmost values of the array),
 * then this just returns the height of the peak from the only available side.
 * @param {number} index The index of the array, where the peak can be found
 * @param {Array.<number>} values The values of the array
 * @return {number} The height of the peak, or 0 if it is not a peak
 * @private
 * @memberof VowelWorm
 */
VowelWorm._peakHeight = function peakHeight(index, values) {
  var peak = values[index],
      lheight = null,
      rheight = null;

  var prev = null;
  // check the left
  for(var i = index-1; i >= 0; i--) {
    if(prev !== null && values[i] > prev) {
      break;
    }
    prev = values[i];
    lheight = peak - prev;
  }

  prev = null;
  // check the right
  for(var i = index+1; i < values.length; i++) {
    if(prev !== null && values[i] > prev) {
      break;
    }
    prev = values[i];
    rheight = peak - prev;
  }

  var result;
  if(lheight === null) {
    result = +rheight;
  }
  else if(rheight === null) {
    result = +lheight;
  }
  else
  {
    result = lheight < rheight ? lheight : rheight;
  }

  if(result < 0) {
    return 0;
  }
  return result;
};

/**
 * Iterates through an array, applying the absolute value to each item
 * TODO: do we EVER get any negative values here? maybe we can ditch this.
 * @param {Array.<number>} y The array to map
 * @return {Array.<number>} the original array, transformed
 */
function arrayAbs(y) {
  for(var i = 0; i < y.length; i++) {
    y[i] = Math.abs(y[i]);
  }
  return y;
};

/**
 * Iterates through an array, inverting each item and adding a given number
 * @param {Array.<number>} y The array to map
 * @param {number} value the amount to add to each inverted item of the array
 * @return {Array.<number>} the original array, transformed
 */
function negArrayAddValue(y, value) {
  for(var i =0; i < y.length; i++) {
    y[i] = -y[i] + value;
  }
  return y;
};

/**
 * Iterates through an array, adding the given value to each item
 * @param {Array.<number>} y The array to map
 * @param {number} value the amount to add to each each item in the array
 * @return {Array.<number>} the original array, transformed
 */
function addToArray(y, value) {
  for(var i = 0; i < y.length; i++) {
    y[i] = y[i] + value;
  }
  return y;
};

/**
 * Combines numeric arrays together
 * @param {...Array.<number>} args any number of arrays to join together
 * @return {Array.<number>} a new array combining all submitted values
 */
function concatenate(args) {
 var p = new Array();
 for(var i = 0; i<arguments.length; i++) {
   for(var j = 0; j<arguments[i].length; j++) {
     p.push(arguments[i][j]);
   }
 }
 return p;
};

/**
 * Reverses an array
 * @param {Array.<number>} y The array to reverse
 * @return {Array.<number>} p A copy of the passed-in array, reversed
 */
function flipArray(y) {
 var p = new Array();
 for(var i = y.length-1; i > -1; i--) {
   p.push(y[i]); 
 }
 return p;
};

/**
 * @license
 *
 * Psuedo-inverse function from Sébastien Loisel, found in a Google Groups
 * discussion {@link https://groups.google.com/d/msg/numericjs/spFVVp1Fy60/6wuN3-vl1IkJ}
 * Sébastien linked to work he had done in the NumericJS Workshop, found here
 * {@link http://www.numericjs.com/workshop.php?link=aacea378e9958c51af91f9eadd5bc7446e0c4616fc7161b384e5ca6d4ec036c7}
 *
 */
/**
 * Finds the pseudo-inverse of the given array
 * Requires NumericJS to be loaded
 * @param {Array.<Array.<number>>} A The array to apply the psuedo-inverse to
 * @return {Array.<Array.<number>>} The psuedo-inverse applied to the array
 */
function pinv(A) {
  var z = numeric.svd(A), foo = z.S[0];
  var U = z.U, S = z.S, V = z.V;
  var m = A.length, n = A[0].length, tol = Math.max(m,n)*numeric.epsilon*foo,M = S.length;
  var i,Sinv = new Array(M);
  for(i=M-1;i!==-1;i--) { if(S[i]>tol) Sinv[i] = 1/S[i]; else Sinv[i] = 0; }
  return numeric.dot(numeric.dot(V,numeric.diag(Sinv)),numeric.transpose(U))
};

/**
 * Contains methods used in the analysis of vowel audio data
 * @param {*} stream The audio stream to analyze OR a string representing the URL for an audio file
 * @constructor
 * //@struct (attaching modules breaks this as a struct; is there a better way?)
 * @final
 * @name VowelWorm.instance
 * @memberof VowelWorm
 */
window.VowelWorm.instance = function(stream) {
  var that = this;

  this._context    = CONTEXT;
  this._analyzer   = this._context.createAnalyser();
  this._sourceNode = null; // for analysis with files rather than mic input
  this._analyzer.fftSize = 2048;
  this._buffer = new Float32Array(this._analyzer.fftSize/2);
  this._audioBuffer = null; // comes from downloading an audio file

  if(stream) {
    this.setStream(stream);
  }
 
  for(var name in modules) { 
    if(modules.hasOwnProperty(name)) {
      attachModuleToInstance(name, that);
    }
  }
  instances.push(this);
};
VowelWorm.instance = window.VowelWorm.instance;

/**
 * The amount the Hanning window needs to be shifted to line up correctly.
 * 
 * @TODO This should be proportional to the window size.
 *
 * @see {@link VowelWorm.hann}
 * @type number
 * @const
 * @memberof VowelWorm
 */
VowelWorm.HANNING_SHIFT = 32;


/**
 * The maximum formant expected to be found for a male speaker
 * @see {@link http://www.fon.hum.uva.nl/praat/manual/Sound__To_Formant__burg____.html}
 * @see {@link http://www.sfu.ca/sonic-studio/handbook/Formant.html}
 * @const
 * @type number
 * @memberof VowelWorm
 */
VowelWorm.DEFAULT_MAX_FORMANT_MALE = 5000;
/**
 * The maximum formant expected to be found for a female speaker
 * @see {@link http://www.fon.hum.uva.nl/praat/manual/Sound__To_Formant__burg____.html}
 * @see {@link http://www.sfu.ca/sonic-studio/handbook/Formant.html}
 * @const
 * @type number
 * @memberof VowelWorm
 */
VowelWorm.DEFAULT_MAX_FORMANT_FEMALE = 5500;
/**
 * The maximum formant expected to be found for a female speaker
 * @see {@link http://www.fon.hum.uva.nl/praat/manual/Sound__To_Formant__burg____.html}
 * @see {@link http://www.sfu.ca/sonic-studio/handbook/Formant.html}
 * @const
 * @type number
 * @memberof VowelWorm
 */
VowelWorm.DEFAULT_MAX_FORMANT_CHILD = 8000;

VowelWorm.instance.prototype = Object.create(VowelWorm);
VowelWorm.instance.constructor = VowelWorm.instance;

var proto = VowelWorm.instance.prototype;

/**
 * Attaches a module to the given instance, with the given name
 * @param {string} name The name of the module to attach. Should be present in
 * {@link modules} to work
 * @param {window.VowelWorm.instance} instance The instance to affix a module to
 */
function attachModuleToInstance(name, instance) {
  instance[name] = {};
  modules[name].call(instance[name], instance);
};

/**
 * Callback used by {@link VowelWorm.module}
 * @callback VowelWorm~createModule
 * @param {window.VowelWorm.instance} instance
 */

/**
 * Adds a module to instances of {@link VowelWorm.instance}, as called by
 * `new VowelWorm.instance(...);`
 * @param {string} name the name of module to add
 * @param {VowelWorm~createModule} callback - Called if successful.
 * `this` references the module, so you can add properties to it. The
 * instance itself is passed as the only argument, for easy access to core
 * functions.
 * @throws An Error when trying to create a module with a pre-existing
 * property name
 *
 * @see {@link attachModuleToInstance}
 * @see {@link modules}
 * @see {@link instances}
 * @memberof VowelWorm
 */
VowelWorm.module = function(name, callback) {
  if(proto[name] !== undefined || modules[name] !== undefined) {
    throw new Error("Cannot define a VowelWorm module with the name \"" +name+
        "\": a property with that name already exists. May I suggest \"" +name+
        "_kewl_sk8brdr_98\" instead?");
  }
  if(typeof callback !== 'function') {
    throw new Error("No callback function submitted.");
  }
  modules[name] = callback;
  instances.forEach(function(instance) {
    attachModuleToInstance(name, instance);
  });
};

/**
 * Removes a module from all current and future VowelWorm instances. Used
 * primarily for testing purposes.
 * @param {string} name - The name of the module to remove
 * @memberof VowelWorm
 */
VowelWorm.removeModule = function(name) {
  if(modules[name] === undefined) {
    return;
  }
  delete modules[name];
  instances.forEach(function(instance) {
    delete instance[name];
  });
};

/**
 * The current mode the vowel worm is in (e.g., stream, audio element, etc.)
 * @type {?number}
 *
 * @see VowelWorm.AUDIO
 * @see VowelWorm.VIDEO
 * @see VowelWorm.STREAM
 * @see VowelWorm.REMOTE_URL
 * @member
 * @memberof VowelWorm.instance
 */
VowelWorm.instance.prototype.mode = null;

/**
 * Retrieves the current FFT data of the audio source. NOTE: this returns a
 * reference to the internal buffer used to store this information. It WILL
 * change. If you need to store it, iterate through it and make a copy.
 *
 * The reason this doesn't return a new Float32Array is because Google Chrome
 * (as of v. 36) does not adequately garbage collect new Float32Arrays. Best
 * to keep them to a minimum.
 *
 * @return Float32Array
 * @memberof VowelWorm.instance
 * @example
 *  var w = new VowelWorm.instance(audioelement),
 *      fft = w.getFFT();
 *
 *  // store a copy for later
 *  var saved_data = [];
 *  for(var i = 0; i<fft.length; i++) {
 *    saved_data[i] = fft[i];
 *  } 
 */
VowelWorm.instance.prototype.getFFT = function(){
  this._analyzer.getFloatFrequencyData(this._buffer);
  return this._buffer;
};

/**
 * @license
 *
 * VowelWorm.instance.prototype.setStream helper functions borrow heavily from
 * Chris Wilson's pitch detector, under the MIT license.
 * See https://github.com/cwilso/pitchdetect
 *
 */
/**
 * Specifies the audio source for the instance. Can be a video or audio element,
 * a URL, or a MediaStream
 * @memberof VowelWorm.instance
 * @param {MediaStream|string|HTMLAudioElement|HTMLVideoElement} stream The audio stream to analyze OR a string representing the URL for an audio file OR an Audio file
 * @throws An error if stream is neither a Mediastream, Audio or Video Element, or a string
 */
VowelWorm.instance.prototype.setStream = function(stream) {
  if(typeof stream === 'string') {
    this._loadFromURL(stream);
  }
  else if(typeof stream === 'object' && stream['constructor']['name'] === 'MediaStream')
  {
    this._loadFromStream(stream);
  }
  else if(stream && (stream instanceof window.Audio || stream.tagName === 'AUDIO'))
  {
    this._loadFromAudio(stream);
  }
  else if(stream && stream.tagName === 'VIDEO')
  {
    this._loadFromVideo(stream);
  }
  else
  {
    throw new Error("VowelWorm.instance.setStream only accepts URL strings, "+
                     "instances of MediaStream (as from getUserMedia), or " +
                     "<audio> elements");
  }
};

/**
 * @param {MediaStream} stream
 * @memberof VowelWorm.instance
 * @private
 */
VowelWorm.instance.prototype._loadFromStream = function(stream) {
  this.mode = this.STREAM;
  var streamSource = this._context.createMediaStreamSource(stream);
  streamSource.connect(this._analyzer);
};

/**
 * Finds the first three peaks of the curve, representative of the first three formants
 * Use this file only after you have passed your array through a smoothing filter
 * @param {Array.<number>} smoothedArray data, expected to have been smoothed, to extract peaks from
 * @param {number} sampleRate the sample rate of the data
 * @param {number} fftSize the FFT size
 * @return {Array.<number>} the positions of all the peaks found, in Hz
 * @memberof VowelWorm.instance
 * @private
 */
VowelWorm.instance.prototype._getPeaks = function(smoothedArray, sampleRate, fftSize) {
  var peaks = new Array();
  var previousNum;
  var currentNum;
  var nextNum;

  for(var i = 0; i < smoothedArray.length; i++) {
    var hz = this._toFrequency(i, sampleRate, fftSize);
    var formant = peaks.length+1;

    switch(formant) {
      case 1:
        if(hz < F1_MIN) { continue; }
        break;
      case 2:
        if(hz < F2_MIN || hz - peaks[0] < MIN_DIFF_F1_F2) { continue; }
        break;
      case 3:
        if(hz < F3_MIN || hz - peaks[1] < MIN_DIFF_F2_F3) { continue; }
        break;
      default:
        return null;
    }

    previousNum = smoothedArray[i-1] || 0;
    currentNum = smoothedArray[i] || 0;
    nextNum = smoothedArray[i+1] || 0;

    if(currentNum > previousNum && currentNum > nextNum) {
      if(this._peakHeight(i, smoothedArray) >= MIN_PEAK_HEIGHT) {
        peaks.push(hz);
        if(formant === 3) {
          return peaks;
        }
      }
    }
  }
  return peaks;
};

/**
 * The sample rate of the attached audio source
 * @return {number}
 * @memberof VowelWorm.instance
 */
VowelWorm.instance.prototype.getSampleRate = function() {
  switch(this.mode) {
    case this.REMOTE_URL:
      return this._sourceNode.buffer.sampleRate;
      break;
    case this.AUDIO:
    case this.VIDEO:
      return DEFAULT_SAMPLE_RATE; // this cannot be retrieved from the element
      break;
    case this.STREAM:
      return this._context.sampleRate;
      break;
    default:
      throw new Error("Current mode has no method for sample rate");
  };
};

/**
 * The size of the FFT, in bins
 * @return {number}
 * @memberof VowelWorm.instance
 */
VowelWorm.instance.prototype.getFFTSize = function() {
  return this._analyzer.fftSize;
};

/**
 * @license
 *
 * VowelWorm.instance.prototype.getMFCCs derived from David Ireland's code at
 * https://github.com/Maxwell79/mfccExtractor under Version 2 (1991) of the GNU
 * General Public License.
 *
 */

/**
 * @typedef mfccsOptions
 * @property {number} minFreq The minimum frequency to expect (TODO: create default val)
 * @property {number} maxFreq The maximum frequency to expect (TODO: create default val)
 * @property {number} filterBanks The number of filter banks to retrieve (TODO: create default val)
 * @property {Array.<number>=} fft FFT transformation data. If null, pulls from the analyzer
 * @property {number=} sampleRate sampleRate the sample rate of the data. Required if data is not null
 * @property {boolean=} [toLinearMagnitude=true] Whether or not to convert
 *   the data to a linear magnitude scale (e.g., if the data being passed in is
 *   in decibels—as is the default data that comes back from {@link VowelWorm.instance#getFFT}).
 *   If this is set to false, the data will be mapped to Math.abs. Since this
 *   calls Math.log on the data, negative values will mess everything up.
 *   Granted, converting these to absolute values might _also_ mess everything
 *   up, but at least it will avoid NaN values. :-)
 */

/**
 * Retrieves Mel Frequency Cepstrum Coefficients (MFCCs). For best results,
 * if using preexisting webaudio FFT data (from getFloatFrequencyData), pass
 * your values through {@link VowelWorm.decibelsToLinear} first. If you do not
 * pass in specific FFT data, the default data will be converted to a linear 
 * magnitude scale anyway.
 *
 * @param {{minFreq: number, maxFreq: number, filterBanks: number, fft: Array.<number>, sampleRate: number, toLinearMagnitude: boolean}} options {@link mfccsOptions}
 * @return {Array.<number>} The MFFCs. Probably relevant are the second and
 * third values (i.e., a[1] and a[2])
 * @memberof VowelWorm.instance
 */
VowelWorm.instance.prototype.getMFCCs = function(options) {
  var fft = null;
  var toLM = options.toLinearMagnitude === undefined ? true : !!options.toLinearMagnitude;

  if(!options.fft) {
    fft = this.getFFT();
  }
  else
  {
    fft = options.fft;
  }

  if(toLM) {
    for(var j = 0; j<fft.length; j++) {
      fft[j] = VowelWorm.decibelsToLinear(fft[j]);
    }
  }
  else
  {
    // we need to ensure that these are all positive values
    var tmpFFT = [];
    for(var i = 0; i<fft.length; i++) {
      tmpFFT[i] = Math.abs(fft[i]);
    }
    fft = tmpFFT;
  }

  var filterBanks = [],
      noFilterBanks = options.filterBanks,
      NFFT = fft.length*2,
      minFreq = options.minFreq,
      maxFreq = options.maxFreq,
      sampleRate = options.sampleRate || this.getSampleRate();

  // initialize filter banks
  var maxMel = 1125 * Math.log(1.0 + maxFreq/700);
  var minMel = 1125 * Math.log(1.0 + minFreq/700);
  var dMel = (maxMel - minMel) / (noFilterBanks+1);

  var bins = [];
  for (var n = 0; n < noFilterBanks + 2; n++) {
    var mel = minMel + n * dMel;
    var Hz = 700  * (Math.exp(mel / 1125) - 1);
    var bin = Math.floor( (NFFT)*Hz / sampleRate);
    bins.push(bin);
  }

  for(var i = 1; i<bins.length-1; i++) {
    var fBank = [];

    var fBelow = VowelWorm._toFrequency(bins[i-1], sampleRate, NFFT);
    var fCentre = VowelWorm._toFrequency(bins[i], sampleRate, NFFT);
    var fAbove = VowelWorm._toFrequency(bins[i+1], sampleRate, NFFT);

    for(var n = 0; n < 1 + NFFT / 2; n++) {
      var freq = VowelWorm._toFrequency(n, sampleRate, NFFT);
      var val = null;

      if ((freq <= fCentre) && (freq >= fBelow)) {
        val = ((freq - fBelow) / (fCentre - fBelow));
      } else if ((freq > fCentre) && (freq <= fAbove)) {
        val = ((fAbove - freq) / (fAbove - fCentre));
      } else {
        val = 0.0;
      }
      fBank.push(val);
    }

    filterBanks.push(fBank);
  }
  // end initialize filterBanks

  // get log coefficients
  var preDCT = []; // Initialise pre-discrete cosine transformation vetor array
  var postDCT = [];// Initialise post-discrete cosine transformation vetor array / MFCC Coefficents

  for(var i = 0; i<filterBanks.length; i++) {
    var cel = 0;
    var n = 0;
    for(var j = 0; j < filterBanks[i].length-1; j++) {
      cel += (filterBanks[i][j]) * fft[n++];
    }
    preDCT.push(Math.log(cel)); // Compute the log of the spectrum
  }

  // Perform the Discrete Cosine Transformation
  for (var i = 0; i < filterBanks.length; i++) {
    var val = 0;
    var n = 0;
    for (var j = 0; j<preDCT.length; j++) {
      val += (preDCT[j]) * Math.cos(i * (n++ - 0.5) *  Math.PI / filterBanks.length);
    }
    val /= filterBanks.length;
    postDCT.push(val);
  }
  return postDCT;
};

/**
 * Retrieves formants. Uses the current time of the audio file or stream,
 * unless data is passed in.
 * @param {Array.<number>=} data FFT transformation data. If null, pulls from the analyzer
 * @param {number=} sampleRate the sample rate of the data. Required if data is not null
 * @return {Array.<number>} The formants found for the audio stream/file. If
 * nothing worthwhile has been found, returns an empty array.
 * @memberof VowelWorm.instance
 */
VowelWorm.instance.prototype.getFormants = function(data, sampleRate) {
  var that = this;

  if(arguments.length !== 2 && arguments.length !== 0) {
    throw new Error("Invalid arguments. Function must be called either as "+
                    " getFormants(data, sampleRate) or getFormants()");
  }
  var fftSize = null;

  if(data) {
    fftSize = data.length*2;
  }
  else
  {
    data = this.getFFT();
    fftSize = this.getFFTSize();
    sampleRate = this.getSampleRate();
  }

  for(var i = 0; i<WINDOW_SIZES.length; i++) {
    var smooth = this.hann(data, WINDOW_SIZES[i]).slice(this.HANNING_SHIFT);
    var formants = this._getPeaks(smooth, sampleRate, fftSize);

    if( formants[0]<F1_MIN || formants[0]>F1_MAX || formants[0]>=formants[1] ||
        formants[1]<F2_MIN || formants[1]>F2_MAX || formants[1]>=formants[2] ||
        formants[2]<F3_MIN || formants[2]>F3_MAX )
    {
      continue;
    }
    else
    {
      return formants;
    }
  }
  return [];  // no good formants found
};

/**
 * Removes reference to this particular worm instance as well as
 * all properties of it.
 * @memberof VowelWorm.instance
 */
VowelWorm.instance.prototype.destroy = function() {
  var index = instances.indexOf(this);
  if(index !== -1) {
    instances.splice(index, 1);
  }
  for(var i in this) {
    if(this.hasOwnProperty(i)) {
      delete this[i];
    }
  }
};

/**
 * @param {string} url Where to fetch the audio data from
 * @throws An error when the server returns an error status code
 * @throws An error when the audio file cannot be decoded
 * @private
 * @memberof VowelWorm.instance
 */
VowelWorm.instance.prototype._loadFromURL = function loadFromURL(url) {
  var that = this,
      request = new XMLHttpRequest();

  request.open("GET", url, true);
  request.responseType = "arraybuffer";

  request.onerror = function error() {
    throw new Error("Tried to load audio file at '" + url + "', but a " +
                     "netowrk error occurred: " + request.statusText);
  };

  function decodeSuccess(buffer) {
    that.mode = this.REMOTE_URL;
    that._audioBuffer = buffer;
    that._resetSourceNode();
    // TODO - enable playback through speakers, looping, etc.
  };

  function decodeError() {
    throw new Error("Could not parse audio data. Make sure the file " +
                     "(" + url + ") you are passing to " +
                     "setStream or VowelWorm.instance is a valid audio " +
                     "file.");
  };

  request.onload = function() {
    if(request.status !== 200) {
      throw new Error("Tried to load audio file at '" + url + "', but the " +
                       "server returned " + request.status + " " +
                       request.statusText + ". Make sure the URL you are " +
                       "passing to setStream or VowelWorm.instance is " +
                       "correct");
    }
    that._context.decodeAudioData(this.response, decodeSuccess, decodeError);
  };

  request.send();
};

/**
 * Loads an audio element as the data to be processed
 * @param {HTMLAudioElement} audio
 * @private
 * @memberof VowelWorm.instance
 */
VowelWorm.instance.prototype._loadFromAudio = function loadFromAudio(audio) {
  console.warn( "Cannot determine sample rate. Setting as " + DEFAULT_SAMPLE_RATE );

  this.mode = this.AUDIO;
  this._sourceNode = this._context.createMediaElementSource(audio);
  this._sourceNode.connect(this._analyzer);
  this._analyzer.connect(this._context.destination);
};

/**
 * Loads a video element as the data to be processed
 * @param {HTMLVideoElement} video
 * @private
 * @memberof VowelWorm.instance
 */
VowelWorm.instance.prototype._loadFromVideo = function loadFromVideo(video) {
  console.warn( "Cannot determine sample rate. Setting as " + DEFAULT_SAMPLE_RATE );

  this.mode = this.VIDEO;
  this._sourceNode = this._context.createMediaElementSource(video);
  this._sourceNode.connect(this._analyzer);
  this._analyzer.connect(this._context.destination);
};

/**
 * Creates (or resets) a source node, as long as an available audioBuffer
 * exists
 * @private
 * @memberof VowelWorm.instance
 */
VowelWorm.instance.prototype._resetSourceNode = function resetSourceNode() {
    this._sourceNode = this._context.createBufferSource();
    this._sourceNode.buffer = this._audioBuffer;
    this._sourceNode.connect(this._analyzer);
};

}(window.numeric));