<repo_name>rytrose/Voice-Remixer<file_sep>/client/js/dependencies/vowelworm.game.js
/**
 * @param {Object=} options Configuration options
 * @param {VowelWorm.instance|Array.<VowelWorm.instance>} options.worms Any
 * VowelWorm instances to begin with
 * @param {number=} [options.width=700] The width of the game board
 * @param {number=} [options.height=500] The height of the game board
 * @param {number=} [options.background=0xFFFFFF] The background color of the game
 * @param {HTMLElement=} [options.element=document.body] What to append the graph to
 * @constructor
 * @name VowelWorm.Game
 */
window.maxHeight = 0;
window.maxBackness = 0;
window.VowelWorm.Game = function (p5) {
    "use strict";

    var game = this;
    window.game = this;
    game.p5 = p5;
    game.margin = 50;

    game.x1 = 0;
    game.x2 = 4.0;
    game.y1 = 0;
    game.y2 = 3.0;

    game.minHz = 0;
    game.maxHz = 8000;

    game.numTrail = 10;

    /**
     * Represents the threshold in dB that VowelWorm's audio should be at in
     * order to to plot anything.
     * @see {@link isSilent}
     * @memberof VowelWorm.Game
     * @name silence
     * @type number
     */
    game.silence = -70;

    /**
     * Specify here which vowel mapping algorithm to use
     * @see {@link VowelWorm._MAPPING_METHODS}
     * @memberof VowelWorm.Game
     * @name map
     */
    game.map = window.VowelWorm._MAPPING_METHODS.linearRegression;
    // game.map = window.VowelWorm._MAPPING_METHODS.mfccFormants;
    // game.map = window.VowelWorm._MAPPING_METHODS.cepstrumFormants;

    /**
     * Indicates whether to normalize the MFCC vector before prediction
     * @memberof VowelWorm.Game
     * @name normalizeMFCCs
     * @type boolean
     */
    game.normalizeMFCCs = true;

    /**
     * Indicates whether to save time domain  and frequency domain data for experimentation.
     * @memberof VowelWorm.Game
     * @name saveData
     * @type boolean
     */
    game.saveData = false;

    /**
     * The number of past positions to keep when computing the simple moving average (SMA)
     * @memberof VowelWorm.Game
     * @name smoothingConstant
     * @type number
     */
    game.smoothingConstant = 5;

    /**
     * Contains all instances of worms for this game
     * @type Array.<Object>
     * @private
     */
    game.worms = [];

    var stopped = false;

    /**
     * You can change this with game.ipa = true/false
     * @type boolean
     * @memberof VowelWorm.Game
     * @name ipa
     */
    // var ipaEnabled = true;

    // var ipaChart = new PIXI.DisplayObjectContainer();

    /**
     * Begins animation of worms
     * @memberof VowelWorm.Game
     * @name play
     */
    game.draw = function () {
        game.drawWorm(game.p5);
    };

    /**
     * Inserts a worm into the ever-increasing frenzy of VowelWorm.
     * @param {window.VowelWorm.instance} worm
     * @memberof VowelWorm.Game
     * @name addWorm
     */
    game.addWorm = function (worm, stream) {
        var container = {};
        container.worm = worm;
        container.circles = [];
        container.stream = stream;

        game.p5.imageMode(game.p5.CENTER);

        // If stream is a MediaStream
        if (typeof stream === 'object' && stream['constructor']['name'] === 'MediaStream') {
            // Remove audio for animation
            var audiolessStream = stream.clone();
            audiolessStream.removeTrack(audiolessStream.getAudioTracks()[0]);
            var cap = game.p5.createCaptureFromStream(audiolessStream);
            cap.hide();
            container.video = cap;
            container.recording = false;
            game.worms.push(container);
        }
        // If stream is a video URL
        else {
            var video = game.p5.createVideo(stream);
            video.volume(0.0);
            video.loop();
            video.hide();
            container.video = video;
            container.recording = true;
            game.worms.push(container);
        }
    };

    /**
     * @private
     */
    game.drawWorm = function (p5) {
        var current_color = "#00FF00";
        game.worms.forEach(function (container) {
            var worm = container.worm,
                circles = container.circles;

            var coords = getCoords(container);

            if (coords !== null) {
                var doRender = true;

                var x = coords.x;
                var y = coords.y;

                var circle = {};
                circle.position = {};
                circle.position.x = x;
                circle.position.y = y;
                circle.tint = current_color;
                circles.unshift(circle);
            }
            current_color = getNextColor(current_color);
        });


        game.worms.forEach(function (container) {
            var circles = container.circles;

            // Calaculate fading alphas and maintain numTrail circles
            for (var i = 0; i < circles.length; i++) {
                var obj = circles[i];
                obj.alpha = 255 - p5.ceil(i * (255 / (game.numTrail - 1)));

                if (i >= game.numTrail) {
                    circles.splice(i, 1);
                    i--;
                }
            }

            // Draw circles
            for (var i = 0; i < circles.length; i++) {
                var circle = circles[i];
                p5.stroke(255);
                var color = p5.color(circle.tint);
                color.setAlpha(circle.alpha);
                p5.fill(color);
                p5.ellipse(circle.position.x, circle.position.y, 200);
            }

            // Draw video
            container.video.mask(maskImage);
            if (circles.length > 0) p5.image(container.video, circles[0].position.x, circles[0].position.y, 200, 200);
        });

    };

    var getCoords = function (container) {

        var buffer = container.worm.getFFT();

        if (isSilent(buffer)) {
            container.circles.pop();
            container.worm.resetPosition();
            return null;
        }

        // Get the position from the worm
        var position = container.worm.getPosition();

        // Transform (backness, height) to (x, y) canvas coordinates
        if (position.length) {
            var coords = transformToXAndY(position[0], position[1]);
            return coords;
        }
        return null;
    };

    /**
     * Transforms from vowel space (backness, height) to canvas space (x, y)
     * @param {number} backness
     * @param {number} height
     * @name transformToXAndY
     */
    var transformToXAndY = function (backness, height) {
        var xStart = game.x1;
        var xEnd = game.x2; // 4
        var yStart = game.y1;
        var yEnd = game.y2; // 3

        var source_x1 = 0.51;
        var source_y1 = 1.8;
        var source_x2 = 1.3;
        var source_y2 = 1.6;
        var source_x3 = 1.4 //1.3;
        var source_y3 = 0.8;
        var source_x4 = 1.45;
        var source_y4 = 1.0;

        var dest_x1 = 0;
        var dest_y1 = 1;
        var dest_x2 = 1;
        var dest_y2 = 1;
        var dest_x3 = 0;
        var dest_y3 = 0;
        var dest_x4 = 1;
        var dest_y4 = 0;

        // https://math.stackexchange.com/questions/296794/finding-the-transform-matrix-from-4-projected-points-with-javascript
        var source_a = math.matrix([[source_x1, source_x2, source_x3],
                            [source_y1, source_y2, source_y3],
                            [1, 1, 1]]);
        var source_b = math.matrix([[source_x4], [source_y4], [1]]);
        var source_aPrime = math.inv(source_a);
        var source_coeffs = math.multiply(source_aPrime, source_b);
        var source_coeffIdentity = math.matrix([[source_coeffs.subset(math.index(0, 0)), 0, 0],
                                                [0, source_coeffs.subset(math.index(1, 0)), 0],
                                                [0, 0, source_coeffs.subset(math.index(2, 0))]]);
        var source_a = math.multiply(source_a, source_coeffIdentity);

        var dest_a = math.matrix([[dest_x1, dest_x2, dest_x3],
            [dest_y1, dest_y2, dest_y3],
            [1, 1, 1]]);
        var dest_b = math.matrix([[dest_x4], [dest_y4], [1]]);
        var dest_aPrime = math.inv(dest_a);
        var dest_coeffs = math.multiply(dest_aPrime, dest_b);
        var dest_coeffIdentity = math.matrix([[dest_coeffs.subset(math.index(0, 0)), 0, 0],
                                              [0, dest_coeffs.subset(math.index(1, 0)), 0],
                                              [0, 0, dest_coeffs.subset(math.index(2, 0))]]);
        var dest_a = math.multiply(dest_a, dest_coeffIdentity);

        var c = math.multiply(dest_a, math.inv(source_a));

        var orig = math.matrix([[backness], [height], [1]]);
        var transformed = math.multiply(c, orig);
        var x_prime = transformed.subset(math.index(0, 0));
        var y_prime = transformed.subset(math.index(1, 0));
        var z_prime = transformed.subset(math.index(2, 0));

        var newCoords = {x: (x_prime / z_prime) * game.p5.width, y: game.p5.height - (y_prime / z_prime) * game.p5.height};
        return newCoords;
        
        // console.log(newCoords);
        //
        // // var xDist = game.p5.width; / (xEnd - xStart);
        // // var yDist = game.p5.height / (yEnd - yStart);
        // //
        // // var adjustedX = (backness - xStart) * xDist + game.margin;
        // // var adjustedY = game.p5.height - (height - yStart) * yDist + game.margin;
        // if(backness > maxBackness) window.maxBackness = backness;
        // if(height > maxHeight) window.maxHeight = height;
        // var adjustedX = backness * game.p5.width;
        // var adjustedY = height * game.p5.height;
        //
        // var coords = document.getElementById('coords');
        // coords.innerHTML = "(" + backness + ", " + height + ")";
        //
        // return {x: adjustedX, y: adjustedY};
    };

    /**
     * Determines whether, for plotting purposes, the audio data is silent or not
     * Compares against the threshold given for {@link game.silence}.
     * @param {Array.<number>|Float32Array} data - An array containing dB values
     * @return {boolean} Whether or not the data is essentially 'silent'
     */
    var isSilent = function (data) {
        for (var i = 0; i < data.length; i++) {
            if (data[i] > game.silence) {
                return false;
            }
        }
        return true;
    };

    //Color Functions
    //Converts an integer representing a color to an integer representing a color 45 degrees away
    var getNextColor = function (old_color) {
        if (typeof old_color == 'number') {
            old_color = old_color.toString(16);
            //Pad with 0's if necessary
            while (old_color.length < 6) {
                old_color = "0" + old_color;
            }
        }

        old_color = new tinycolor(old_color);
        var new_color = old_color.spin(45).toHexString();
        return new_color;
    };

    // /**
    //  * Fills the IPA Chart. A constructor helper method.
    //  */
    // var drawVowels = function() {
    //     if(!ipaChart.children.length) {
    //         var letters = [
    //             ["e",221.28871891963863,252.35519027188354],
    //             ["i",169.01833799969594,171.97765003235634],
    //             ["a",317.6219414250667,337.00896411883406],
    //             ["o",384.5714404194302,284.96641792056766],
    //             ["u",412.17314090483404,231.94657762575406]
    //         ];
    //         var chart = new PIXI.Sprite.fromImage("plot2.png");
    //         chart.position.x = 0 + game.margin;
    //         chart.position.y = 0 + game.margin;
    //         ipaChart.addChild(chart);
    //         // for(var i=0; i<letters.length; i++){
    //         //   var letter = new PIXI.Text(letters[i][0],{font: "35px sans-serif", fill: "black", align: "center"});
    //         //   letter.position.x = letters[i][1];
    //         //   letter.position.y = letters[i][2];
    //         //   ipaChart.addChild(letter);
    //         // }
    //     }
    // };
};
<file_sep>/client/js/master/webrtc_master.js
var localStream;
var recorder;
var mixedStream;
var peerConnections = {};
var uuid;
var serverConnection;
var numPeers = 0;

var peerConnectionConfig = {
    'iceServers': [
        {'urls': 'stun:stun.services.mozilla.com'},
        {'urls': 'stun:stun.l.google.com:19302'},
    ]
};

function loadWebRTC() {
    uuid = createUUID();
    document.getElementById('masterId').innerHTML = uuid;

    serverConnection = new WebSocket('wss://' + window.location.hostname + ':8443');
    serverConnection.onmessage = gotMessageFromServer;

    // Tell server what UUID this client is
    serverConnection.onopen = () => serverConnection.send(JSON.stringify({'identify': true, 'uuid': uuid}));

    var constraints = {
        video: true,
        audio: true,
    };

    if (navigator.mediaDevices.getUserMedia) {
        navigator.mediaDevices.getUserMedia(constraints).then(getUserMediaSuccess).catch(errorHandler);
    } else {
        alert('Your browser does not support getUserMedia API');
    }
}

function getUserMediaSuccess(stream) {
    localStream = stream.clone();

    // Create new recorder from local stream
    recorder = new MediaRecorder(localStream);
    recorder.ondataavailable = function (e) {
        recordingChunks.push(e.data);
    }

    // Set recorder callback
    recorder.onstop = function () {
        var newRecordingWorm = document.createElement('video');
        var newRecordingMixer = document.createElement('video');
        var blob = new Blob(recordingChunks, {'type': 'video/mp4'});
        recordingChunks = [];
        var videoURL = URL.createObjectURL(blob);
        newRecordingWorm.src = videoURL;
        newRecordingMixer.src = videoURL;
        newRecordingWorm.loop = true;
        newRecordingMixer.loop = true;
        newRecordingWorm.play();
        newRecordingMixer.play();
        // Needs two separate recordings for two separate Web Audio graphs
        // 1. the Vowel Worm, 2. the Mixer

        // Create a new worm from the audio
        var worm = new window.VowelWorm.instance(newRecordingWorm);
        window.vw.addWorm(worm, videoURL);

        // Add recording to mixer
        mixer.addSource(newRecordingMixer);
    }

    mixedStream = stream;

    // Add stream to mixer
    mixer.addSource(mixedStream);

    // Get the mixed audio stream from mixer
    var streamFromMixer = mixer.getOutputStream();

    // Remove the original audio track from local stream
    mixedStream.removeTrack(mixedStream.getAudioTracks()[0]);

    // Add the mixer audio track to mixed stream
    mixedStream.addTrack(streamFromMixer.getAudioTracks()[0]);

    // Add localStream to VowelWorm
    var worm = new window.VowelWorm.instance(localStream);
    window.vw.addWorm(worm, localStream);

    // Add to Tonejs
    var toneStream = new Tone.UserMedia();
    toneStream.openMediaStream(streamFromMixer);
    toneStream.toMaster();
}

function start(signal) {
    var peerConnection = new RTCPeerConnection(peerConnectionConfig);
    peerConnection.onicecandidate = gotIceCandidate;
    peerConnection.ontrack = gotRemoteStream;
    peerConnection.addStream(mixedStream);
    peerConnections[signal.uuid] = peerConnection;
}

function gotMessageFromServer(message) {
    var signal = JSON.parse(message.data);

    // Ignore messages from ourself
    if (signal.uuid == uuid) return;

    if (signal.sdp) {
        start(signal);
        var peerConnection = peerConnections[signal.uuid];
        peerConnection.setRemoteDescription(new RTCSessionDescription(signal.sdp)).then(function () {
            // Only create answers in response to offers
            if (signal.sdp.type == 'offer') {

                if (numPeers < MAX_CONNECTIONS) {
                    peerConnection.createAnswer().then(
                        function (description) {
                            console.log('got description');

                            peerConnection.setLocalDescription(description).then(function () {
                                serverConnection.send(JSON.stringify({
                                    'sdp': peerConnection.localDescription,
                                    'uuid': uuid
                                }));
                            }).catch(errorHandler);
                        }).catch(errorHandler);
                } else {
                    alert("Only " + MAX_CONNECTIONS + " connections are allowed.")
                }
            }
        }).catch(errorHandler);
    } else if (signal.ice) {
        var peerConnection = peerConnections[signal.uuid];
        peerConnection.addIceCandidate(new RTCIceCandidate(signal.ice)).catch(errorHandler);
    }
}

function gotIceCandidate(event) {
    if (event.candidate != null) {
        serverConnection.send(JSON.stringify({'ice': event.candidate, 'uuid': uuid}));
    }
}

var remoteStream = null;

function gotRemoteStream(event) {
    console.log('got remote stream');

    // First track event
    if (!remoteStream) {
        remoteStream = new MediaStream([event.track]);
        if (event.track.kind == "audio") {
            mixer.addSource(event.streams[0]);
        }
    }
    // Second track event
    else {
        remoteStream.addTrack(event.track);
        if (event.track.kind == "audio") mixer.addSource(event.streams[0]);
        var worm = new window.VowelWorm.instance(remoteStream);
        window.vw.addWorm(worm, remoteStream);
        remoteStream = null;
        numPeers++;
    }
}

function errorHandler(error) {
    console.log(error);
}

// Taken from http://stackoverflow.com/a/105074/515584
// Strictly speaking, it's not a real UUID, but it gets the job done here
function createUUID() {
    return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
        (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
    )
}

<file_sep>/README.md
# Voice Remixer


<file_sep>/server.js
const HTTPS_PORT = 8443;

const fs = require('fs');
const https = require('https');
const WebSocket = require('ws');
const WebSocketServer = WebSocket.Server;
const express = require('express');
const url_shortener_api_key = "<KEY>";

// Yes, TLS is required
const serverConfig = {
  key: fs.readFileSync('key.pem'),
  cert: fs.readFileSync('cert.pem'),
};

// ----------------------------------------------------------------------------------------

var app = express();
var path = require('path');
var httpsServer = https.createServer(serverConfig, app).listen(HTTPS_PORT, "0.0.0.0");

var redis = require("redis");
var redis_client = redis.createClient();

redis_client.on('error', function (err) {
  console.log('Error ' + err)
})

process.on('SIGTERM', function() {
    redis_client.quit();
    process.exit();
});

process.on('SIGINT', function() {
    redis_client.quit();
    process.exit();
});

app.use(express.static( __dirname + '/client'));

app.get('/', function (req, res) {
  res.sendFile(path.join(__dirname, 'client/views', 'index.html'));
});

app.get('/master', function (req, res) {
  res.sendFile(path.join(__dirname, 'client/views', 'master.html'));
});

app.get('/slave', function (req, res) {
  res.sendFile(path.join(__dirname, 'client/views', 'slave.html'));
});

// ----------------------------------------------------------------------------------------

const wss = new WebSocketServer({server: httpsServer, clientTracking: true});
var networks = {};

wss.on('connection', function(ws) {
  
  ws.on('error', (e) => { if(e.code != 'ECONNRESET') console.log(e); });

  ws.uuid = -99;
  ws.on('message', function(message) {
    var signal = JSON.parse(message);
    if(signal.identify) { ws.uuid = signal.uuid; return; }

    console.log("MESSAGE: " + signal);
    console.log("Master in message: " + signal.master);

    // this message is from a slave
    if(signal.master){
      // if this is not the first message to the master
      if(signal.master in networks) {
        console.log("NOT the first message from slave.");
        var slaves = networks[signal.master];
        // if this is a new slave
        if(slaves.indexOf(signal.uuid) == -1) networks[signal.master].push(signal.uuid);
      } 
      // this is the first message from the slave, establish master<-->slave link
      else {
        console.log("First message from slave.");
        networks[signal.master] = [signal.uuid];
      }

      console.log(networks);

      // forward the message to the master
      var client_array = Array.from(wss.clients);
      console.log("Clients: " + client_array);
      client_array.forEach(cl => console.log(cl.uuid));

      var master = Array.from(wss.clients).filter(client => client.uuid == signal.master)[0];
      console.log("Master: " + master);
      if(master && master.readyState === WebSocket.OPEN) master.send(message);
      else console.log("Master not found!");
    }
    // this message is from a master
    else {
      // forward the message to all slaves
      console.log("Master uuid: " + signal.uuid);
      console.log("networks: " + networks);
      networks[signal.uuid].forEach(function each(slave) {
        var slave_ws = Array.from(wss.clients).filter(client => client.uuid == slave)[0];
        console.log(slave_ws);
        if(slave_ws && slave_ws.readyState === WebSocket.OPEN) slave_ws.send(message);
        else console.log("Slave not found!");
      });
    }
  });
});

wss.broadcast = function(data) {
  this.clients.forEach(function(client) {
    if(client.readyState === WebSocket.OPEN) {
      client.send(data);
    }
  });
};


// -------------------------------------------------
// Ensures that there are no hanging ws connections
function noop() {}

function heartbeat() {
  this.isAlive = true;
}

wss.on('connection', function connection(ws) {
  ws.isAlive = true;
  ws.on('pong', heartbeat);
});

const interval = setInterval(function ping() {
  wss.clients.forEach(function each(ws) {
    if (ws.isAlive === false) return ws.terminate();

    ws.isAlive = false;
    ws.ping(noop);
  });
}, 30000);

// -------------------------------------------------

console.log('Server running. Visit https://localhost:' + HTTPS_PORT + ' in Firefox/Chrome.\n\n\
Some important notes:\n\
  * Note the HTTPS; there is no HTTP -> HTTPS redirect.\n\
  * You\'ll also need to accept the invalid TLS certificate.\n\
  * Some browsers or OSs may not allow the webcam to be used by multiple pages at once. You may need to use two different browsers or machines.\n'
);
<file_sep>/client/js/dependencies/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;

    /**
     * The window size to use for each analysis frame
     * From both Wikipedia (http://en.wikipedia.org/wiki/Formant; retrieved 23 Jun.
     * 2014, 2:52 PM UTC) and <NAME>'s chart (personal email)
     *
     * These indicate the minimum values in Hz in which we should find our formants
     *
     * TODO get better documentation (wikipedia's not the most reliable of sources, and I don't know if anyone who's still here has access to the email with <NAME>son's chart.)
     */

    /**
     * @const
     * @type number
     */
    var F1_MIN = 100;
    /**
     * @const
     * @type number
     */
    var WINDOW_SIZE = 0.046;

    /**
     * The number of filter banks to use when computing MFCCs.
     * @const
     * @type number
     */
    var NUM_FILTER_BANKS = 40;

    /**
     * The first MFCC to use in the mapping algorithms.
     * @const
     * @type number
     */
    var FIRST_MFCC = 2;

    /**
     * The last MFCC to use in the mapping algorithms.
     * @const
     * @type number
     */
    var LAST_MFCC = 25;

    /**
     <<<<<<< HEAD
     * The minimum backness value. Used for transforming between formants to backness.
     =======
     * Represent the minimum differences between formants, to ensure they are
     * properly spaced
     *
     *
     * I'm not sure if this is possible. Sometimes the formants, especially the
     * second and third formants, merge together for a little bit. (TODO Find source.)
     *
     * If it's possible/reasonable, we might want to look at the beginning and end of
     * the vowelss to see if it's one or two formants.
     *
     >>>>>>> ae5b061f1d0660bdb59dd980c7dd801ac5f31fb2
     * @const
     * @type number
     */
    var BACKNESS_MIN = 0;

    /**
     * The maximum backness value. Used for transforming between formants and backness.
     * @const
     * @type number
     */
    var BACKNESS_MAX = 4;

    /**
     * The minimum height value. Used for transforming between formants and height.
     * @const
     * @type number
     */
    var HEIGHT_MIN = 0;

    /**
     * The maximum height value. Used for transforming between formants and height.
     * @const
     * @type number
     */
    var HEIGHT_MAX = 3;

    /**
     * Transposed MFCC weight matrices for prediction.
     * Numeric keys represent the number of filter MFFCs used as regressors (features).
     * These are transposed from the matrices given in the VowelWorm MATLAB code
     * @constant
     */
    /**
     * @license
     * VowelWorm concept and VowelWorm._MFCC_WEIGHTS from <NAME>, Andreas
     * Arzt, and <NAME> at the Department of Computational Perception
     * Johannes Kepler University, Linz, Austria.
     * http://www.cp.jku.at/projects/realtime/vowelworm.html
     *
     */
    VowelWorm._MFCC_WEIGHTS = {
        25: {
            height: new Float32Array([
                1.104270, 0.120389, 0.271996, 0.246571, 0.029848, -0.489273, -0.734283,
                -0.796145, -0.441830, -0.033330, 0.415667, 0.341943, 0.380445, 0.260451,
                0.092989, -0.161122, -0.173544, -0.015523, 0.251668, 0.022534, 0.054093,
                0.005430, -0.035820, -0.057551, 0.161558
            ]),
            backness: new Float32Array([
                0.995437, 0.540693, 0.121922, -0.585859, -0.443847, 0.170546, 0.188879,
                -0.306358, -0.308599, -0.212987, 0.012301, 0.574838, 0.681862, 0.229355,
                -0.222245, -0.222203, -0.129962, 0.329717, 0.142439, -0.132018, 0.103092,
                0.052337, -0.034299, -0.041558, 0.141547
            ])
        }
    };

    /**
     * Loads the regression weights from the server
     * @param boolean normalizeMFCCs indicates whether to use weights for normalized or
     * non-normalized MFCCs
     */
    VowelWorm.loadRegressionWeights = function (normalizeMFCCs) {

        var weightsReq = new XMLHttpRequest();
        weightsReq.addEventListener("load", function () {

            // Parse the backness and height weights
            var xmlDoc = weightsReq.responseXML;
            var backWeightsElements = xmlDoc.getElementsByTagName("backness")[0]
                .getElementsByTagName("weight");
            var heightWeightsElements = xmlDoc.getElementsByTagName("height")[0]
                .getElementsByTagName("weight");
            var backWeights = [];
            var heightWeights = [];
            for (var i = 0; i < backWeightsElements.length; i++) {
                backWeights.push(backWeightsElements[i].childNodes[0].nodeValue);
                heightWeights.push(heightWeightsElements[i].childNodes[0].nodeValue);
            }
            VowelWorm._MFCC_WEIGHTS[25].backness = new Float32Array(backWeights);
            VowelWorm._MFCC_WEIGHTS[25].height = new Float32Array(heightWeights);
        })
        if (normalizeMFCCs) {
            weightsReq.open("GET", "training/weights_norm_mfcc.xml", true);
        }
        else {
            weightsReq.open("GET", "training/weights.xml", true);
        }
        weightsReq.send();
    }


    /**
     * Given an array of fft data, returns backness and height coordinates
     * in the vowel space.
     * @param {Array.<number>} fftData The fftData to map
     * @return {Array.<number>} an array formatted thusly: [x,y]. May be empty
     * @nosideeffects
     */
    VowelWorm._MAPPING_METHODS = {
        linearRegression: function (fftData, options) {

            // Get the mfccs to use as features
            var mfccs = window.AudioProcessor.getMFCCs({
                fft: fftData,
                fftSize: options.fftSize,
                minFreq: options.minHz,
                maxFreq: options.maxHz,
                filterBanks: NUM_FILTER_BANKS,
                sampleRate: options.sampleRate,
            });

            // Predict the backness and height using multiple linear regression
            if (mfccs.length) {

                // Get the specified MFCCs to use as regressors (features)
                // Also makes a copy of mfccs (since they are changing with the streaming audio)
                var features = mfccs.slice(FIRST_MFCC - 1, LAST_MFCC);

                // Normalize the MFCC vector
                if (window.game.normalizeMFCCs) {
                    var normSquared = 0;
                    for (var i = 0; i < features.length; i++) {
                        normSquared += features[i] * features[i];
                    }
                    for (var i = 0; i < features.length; i++) {
                        features[i] /= Math.sqrt(normSquared);
                    }
                }

                // Insert DC coefficient for regression
                features.splice(0, 0, 1);

                // Check for corresponding weights
                if (VowelWorm._MFCC_WEIGHTS[features.length] === undefined) {
                    throw new Error("No weights found for mfccs of length " +
                        mfccs.length + ". If you are using getMFCCs, make sure the " +
                        "amount of filter banks you are looking for corresponds to one of " +
                        "the keys found in VowelWorm._MFCC_WEIGHTS.");
                }

                // Do the prediction
                var backness = window.MathUtils.predict(features, VowelWorm._MFCC_WEIGHTS[features.length].backness);
                var height = window.MathUtils.predict(features, VowelWorm._MFCC_WEIGHTS[features.length].height);

                return [backness, height];
            }
            return [];
        },
        mfccFormants: function (fftData, options) {

            var mfccs = window.AudioProcessor.getMFCCs({
                fft: fftData,
                fftSize: options.fftSize,
                minFreq: options.minHz,
                maxFreq: options.maxHz,
                filterBanks: options.numFilterBanks,
                sampleRate: options.sampleRate
            });

            if (mfccs.length) {

                // Convert the mfccs to formants
                var formants = window.AudioProcessor.getFormantsFromMfccs(mfccs);
                var backness;
                var height;
                if (formants.length > 0) {
                    var pos = mapFormantsToIPA(formants[0], formants[1]);
                }

                return [backness, height];
            }
            return [];
        },
        cepstrumFormants: function (fftData, options) {
            var cepstrum = window.AudioProcessor.getCepstrum(fftData, {});

            if (cepstrum.length) {

                // Convert the cepstrum to formants
                var formants = window.AudioProcessor.getFormantsFromCepstrum(cepstrum, {
                    numFormants: 2,
                    sampleRate: options.sampleRate,
                    fftSize: options.fftSize,
                    cutoff: 200
                });

                if (formants.length > 0) {
                    var pos = mapFormantsToIPA(formants[0], formants[1]);
                    return pos;
                }
                else {
                    return [];
                }
            }
            return [];
        }
    };

    /**
     * Maps first and second formants to the IPA vowel space.
     */
    var mapFormantsToIPA = function (f1, f2) {

        // var backness = window.MathUtils.mapToScale(f2 - f1, 
        //     window.AudioProcessor.F2_MAX - window.AudioProcessor.F1_MAX, 
        //     window.AudioProcessor.F2_MIN - window.AudioProcessor.F1_MIN, BACKNESS_MIN, BACKNESS_MAX);

        var backness = window.MathUtils.mapToScale(f2,
            window.AudioProcessor.F2_MAX, window.AudioProcessor.F2_MIN, BACKNESS_MIN, BACKNESS_MAX);

        var height = window.MathUtils.mapToScale(f1,
            window.AudioProcessor.F1_MAX, window.AudioProcessor.F1_MIN, HEIGHT_MIN, HEIGHT_MAX);

        return [backness, height];
    }

    /**
     * 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;

    /**
     * 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 = window.MathUtils.nextPow2(this._context.sampleRate * WINDOW_SIZE);
        this._buffer = new Float32Array(this._analyzer.frequencyBinCount);
        this._audioBuffer = null; // comes from downloading an audio file

        // Attach an processor node to analyze data from every buffer.
        // Note: this is deprecated but the replacement has not been implemented in any browers yet.
        // See https://developer.mozilla.org/en-US/docs/Web/API/ScriptProcessorNode/onaudioprocess
        this._processorNode = this._context.createScriptProcessor(this._analyzer.fftSize, 1, 1);
        this._processorNode.onaudioprocess = function (e) {
            that.computePosition(window.game.map, window.game.smoothingConstant);

            if (window.game.saveData) {

                // Save the timestamp for this frame
                this.timestamps.push(this._analyzer.currentTime);

                // Save the time domain data
                var timeDomainBuffer = new Float32Array(this.getFFTSize());
                this._analyzer.getFloatTimeDomainData(timeDomainBuffer);
                var timeDomainData = new Float32Array(this.fftSize);
                for (var i = 0; i < timeDomainBuffer.length; i++) {
                    timeDomainData[i] = timeDomainBuffer[i];
                }
                this.timeDomainData.push(timeDomainData[i]);

                // Save the FFT data
                var fftBuffer = this.getFFT();
                var fftData = new Float32Array(fftBuffer.length);
                for (var i = 0; i < fftBuffer.length; i++) {
                    fftData[i] = fftBuffer[i];
                }
                this.ffts.push(fftBuffer[i]);
            }
        }
        this._processorNode.connect(this._context.destination);

        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;

    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];
        });
    };
    /**
     * Callback used by {@link VowelWorm.module}
     * @callback VowelWorm~createModule
     * @param {VowelWorm.instance.prototype} prototype
     */

    /**
     * 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;

    /**
     * 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];
            }
        }
    };

    /**
     * The sample rate of the attached audio source
     * @return {number}
     * @memberof VowelWorm.instance
     * @nosideeffects
     */
    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. This is twice the number of bins.
     * @return {number}
     * @memberof VowelWorm.instance
     * @nosideeffects
     */
    VowelWorm.instance.prototype.getFFTSize = function () {
        return this._analyzer.fftSize;
    };

    /**
     * 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;
    };

    /**
     * Stores all timestamps for future use.
     */
    VowelWorm.instance.prototype.timestamps = [];

    /**
     * Stores all fft buffers for future use.
     */
    VowelWorm.instance.prototype.ffts = [];

    /**
     * Stores all time domain buffers for future use.
     */
    VowelWorm.instance.prototype.timeDomainData = [];

    /**
     * Stores the previous positions for smoothing
     */
    VowelWorm.instance.prototype.positions = [];

    /**
     * Stores the current simple moving average for smoothing.
     */
    VowelWorm.instance.prototype.positionSMA = [];

    /**
     * Computes the (backness, height) coordinates of this worm for the current time frame
     * The position is smoothed over time with a simple moving average.
     * See https://en.wikipedia.org/wiki/Moving_average#Simple_moving_average
     */
    VowelWorm.instance.prototype.computePosition = function (mappingMethod, smoothingConstant) {

        var buffer = this.getFFT();

        // Copy the fft data since it will change as audio streams in
        var fft = [];
        for (var i = 0; i < buffer.length; i++) {
            fft.push(buffer[i]);
        }

        // Map the fft data to (backness, height) coordinates in the vowel space
        var position = mappingMethod(fft, {
            fftSize: this.getFFTSize(),
            minHz: window.game.minHz,
            maxHz: window.game.maxHz,
            sampleRate: this.getSampleRate()
        });

        // Smooth the position over time
        if (this.positions.length == 0) {
            this.positionSMA = position;
        }
        else if (this.positions.length < smoothingConstant) {
            // Compute each coordinate separately
            for (var i = 0; i < this.positionSMA.length; i++) {
                // Until we have enough previous data, this is the same as the cumulative moving average
                this.positionSMA[i] = (position[i] + this.positions.length * this.positionSMA[i]) /
                    (this.positions.length + 1)
            }
        }
        else {
            var oldPosition = this.positions[0];
            for (var i = 0; i < this.positionSMA.length; i++) {
                this.positionSMA[i] += (position[i] - oldPosition[i]) / this.positions.length;
            }
            this.positions = this.positions.slice(1);
        }
        // Make sure to store this position for next time
        this.positions.push(position);
    }

    /**
     * Gets the current (backness, height) coordinates of this worm.
     */
    VowelWorm.instance.prototype.getPosition = function () {
        return this.positionSMA;
    }

    VowelWorm.instance.prototype.resetPosition = function () {
        this.positions = [];
        this.positionSMA = [];
    }

    /**
     * @license
     *
     * VowelWorm.instance.prototype.setStream helper functions borrow heavily from
     * <NAME>'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 {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();
    };

    /**
     * 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);
        this._sourceNode.connect(this._processorNode);
    };

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

    /**
     * Loads an audio element as the data to be processed
     * @param {HTMLAudioElement} audio
     * @private
     * @memberof VowelWorm.instance
     */
    VowelWorm.instance.prototype._loadFromAudio = function loadFromAudio(audio) {

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

    /**
     * Loads a video element as the data to be processed
     * @param {HTMLVideoElement} video
     * @private
     * @memberof VowelWorm.instance
     */
    VowelWorm.instance.prototype._loadFromVideo = function loadFromVideo(video) {

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