text: // https://github.com/kdavis-mozilla/vad.js/blob/master/lib/vad.js
  // https://github.com/kdavis-mozilla/vad.js
  //
  // Copyright (c) 2015, Kelly Davis 
  // All rights reserved.
  
  // Redistribution and use in source and binary forms, with or without modification,
  // are permitted provided that the following conditions are met:
  
  // * Redistributions of source code must retain the above copyright notice, this
  //   list of conditions and the following disclaimer.
  
  // * 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.
  
  // * Neither the name of the {organization} nor the names of its
  //   contributors may be used to endorse or promote products derived from
  //   this software without specific prior written permission.
  
  // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT HOLDER OR 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.
  //
  
  (function(window) {
  
    var VAD = function(options) {
      // Default options
      this.options = {
        fftSize: 512,
        bufferLen: 512, 
        voice_stop: function() {},
        voice_start: function() {},
        smoothingTimeConstant: 0.99, 
        energy_offset: 1e-8, // The initial offset.
        energy_threshold_ratio_pos: 2, // Signal must be twice the offset
        energy_threshold_ratio_neg: 0.5, // Signal must be half the offset
        energy_integration: 1, // Size of integration change compared to the signal per second.
        filter: [
          {f: 200, v:0}, // 0 -> 200 is 0
          {f: 2000, v:1} // 200 -> 2k is 1
        ],
        source: null,
        context: null
      };
  
      // User options
      for(var option in options) {
        if(options.hasOwnProperty(option)) {
          this.options[option] = options[option];
        }
      }
  
      // Require source
     if(!this.options.source)
       throw new Error("The options must specify a MediaStreamAudioSourceNode.");
  
      // Set this.options.context
      this.options.context = this.options.source.context;
  
      // Calculate time relationships
      this.hertzPerBin = this.options.context.sampleRate / this.options.fftSize;
      this.iterationFrequency = this.options.context.sampleRate / this.options.bufferLen;
      this.iterationPeriod = 1 / this.iterationFrequency;
  
      var DEBUG = true;
      if(DEBUG) console.log(
        'Vad' +
        ' | sampleRate: ' + this.options.context.sampleRate +
        ' | hertzPerBin: ' + this.hertzPerBin +
        ' | iterationFrequency: ' + this.iterationFrequency +
        ' | iterationPeriod: ' + this.iterationPeriod
      );
  
      this.setFilter = function(shape) {
        this.filter = [];
        for(var i = 0, iLen = this.options.fftSize / 2; i < iLen; i++) {
          this.filter[i] = 0;
          for(var j = 0, jLen = shape.length; j < jLen; j++) {
            if(i * this.hertzPerBin < shape[j].f) {
              this.filter[i] = shape[j].v;
              break; // Exit j loop
            }
          }
        }
      }
  
      this.setFilter(this.options.filter);
  
      this.ready = {};
      this.vadState = false; // True when Voice Activity Detected
  
      // Energy detector props
      this.energy_offset = this.options.energy_offset;
      this.energy_threshold_pos = this.energy_offset * this.options.energy_threshold_ratio_pos;
      this.energy_threshold_neg = this.energy_offset * this.options.energy_threshold_ratio_neg;
  
      this.voiceTrend = 0;
      this.voiceTrendMax = 10;
      this.voiceTrendMin = -10;
      this.voiceTrendStart = 5;
      this.voiceTrendEnd = -5;
  
      // Create analyser 
      this.analyser = this.options.context.createAnalyser();
      this.analyser.smoothingTimeConstant = this.options.smoothingTimeConstant; // 0.99;
      this.analyser.fftSize = this.options.fftSize;
  
      this.floatFrequencyData = new Float32Array(this.analyser.frequencyBinCount);
  
      // Setup local storage of the Linear FFT data
      this.floatFrequencyDataLinear = new Float32Array(this.floatFrequencyData.length);
  
      // Connect this.analyser
      this.options.source.connect(this.analyser); 
  
      // Create ScriptProcessorNode
      this.scriptProcessorNode = this.options.context.createScriptProcessor(this.options.bufferLen, 1, 1);
  
      // Connect scriptProcessorNode (Theretically, not required)
      this.scriptProcessorNode.connect(this.options.context.destination);
  
      // Create callback to update/analyze floatFrequencyData
      var self = this;
      this.scriptProcessorNode.onaudioprocess = function(event) {
        self.analyser.getFloatFrequencyData(self.floatFrequencyData);
        self.update();
        self.monitor();
      };
  
      // Connect scriptProcessorNode
      this.options.source.connect(this.scriptProcessorNode);
  
      // log stuff
      this.logging = false;
      this.log_i = 0;
      this.log_limit = 100;
  
      this.triggerLog = function(limit) {
        this.logging = true;
        this.log_i = 0;
        this.log_limit = typeof limit === 'number' ? limit : this.log_limit;
      }
  
      this.log = function(msg) {
        if(this.logging && this.log_i < this.log_limit) {
          this.log_i++;
          console.log(msg);
        } else {
          this.logging = false;
        }
      }
  
      this.update = function() {
        // Update the local version of the Linear FFT
        var fft = this.floatFrequencyData;
        for(var i = 0, iLen = fft.length; i < iLen; i++) {
          this.floatFrequencyDataLinear[i] = Math.pow(10, fft[i] / 10);
        }
        this.ready = {};
      }
  
      this.getEnergy = function() {
        if(this.ready.energy) {
          return this.energy;
        }
  
        var energy = 0;
        var fft = this.floatFrequencyDataLinear;
  
        for(var i = 0, iLen = fft.length; i < iLen; i++) {
          energy += this.filter[i] * fft[i] * fft[i];
        }
  
        this.energy = energy;
        this.ready.energy = true;
  
        return energy;
      }
  
      this.monitor = function() {
        var energy = this.getEnergy();
        var signal = energy - this.energy_offset;
  
        if(signal > this.energy_threshold_pos) {
          this.voiceTrend = (this.voiceTrend + 1 > this.voiceTrendMax) ? this.voiceTrendMax : this.voiceTrend + 1;
        } else if(signal < -this.energy_threshold_neg) {
          this.voiceTrend = (this.voiceTrend - 1 < this.voiceTrendMin) ? this.voiceTrendMin : this.voiceTrend - 1;
        } else {
          // voiceTrend gets smaller
          if(this.voiceTrend > 0) {
            this.voiceTrend--;
          } else if(this.voiceTrend < 0) {
            this.voiceTrend++;
          }
        }
  
        var start = false, end = false;
        if(this.voiceTrend > this.voiceTrendStart) {
          // Start of speech detected
          start = true;
        } else if(this.voiceTrend < this.voiceTrendEnd) {
          // End of speech detected
          end = true;
        }
  
        // Integration brings in the real-time aspect through the relationship with the frequency this functions is called.
        var integration = signal * this.iterationPeriod * this.options.energy_integration;
  
        // Idea?: The integration is affected by the voiceTrend magnitude? - Not sure. Not doing atm.
  
        // The !end limits the offset delta boost till after the end is detected.
        if(integration > 0 || !end) {
          this.energy_offset += integration;
        } else {
          this.energy_offset += integration * 10;
        }
        this.energy_offset = this.energy_offset < 0 ? 0 : this.energy_offset;
        this.energy_threshold_pos = this.energy_offset * this.options.energy_threshold_ratio_pos;
        this.energy_threshold_neg = this.energy_offset * this.options.energy_threshold_ratio_neg;
  
        // Broadcast the messages
        if(start && !this.vadState) {
          this.vadState = true;
          this.options.voice_start();
        }
        if(end && this.vadState) {
          this.vadState = false;
          this.options.voice_stop();
        }
  
        this.log(
          'e: ' + energy +
          ' | e_of: ' + this.energy_offset +
          ' | e+_th: ' + this.energy_threshold_pos +
          ' | e-_th: ' + this.energy_threshold_neg +
          ' | signal: ' + signal +
          ' | int: ' + integration +
          ' | voiceTrend: ' + this.voiceTrend +
          ' | start: ' + start +
          ' | end: ' + end
        );
  
        return signal;
      }
    };
  
    window.VAD = VAD;
  
  })(window);
  