Source: meter.js

import Widget from "ui/core/widget";
import Constraint from "util/constraint";
import ConstraintSpec from "util/constraint-def";

/**
 * Class representing a Volume Meter widget.
 * @class
 * @implements {Widget}
 */
class Meter extends Widget {

  /**
   * @constructor
   * @param {object} container - DOM container for the widget.
   * @param {AudioContext} audioContext - The audio context to be used.
   * @param {object} [o] - Options object.
   * @param {string} [o.backgroundColor="#282828"] - The background color. 
   * @param {number} [o.initAmplitude=0] - The initial amplitude to be displayed (range of 0. - 1.)
   */
  constructor(container, audioCtx, o) {
    o = o || {};

    super(container, o);

    // remove the svg since we are using canvas here
    this.container.removeChild(this.svg);
    this.svg = null;

    this._initCanvasElements();
    this._initAudioModules(audioCtx);
    this._initOptions(o);

    return this;
  }

  /* ===========================================================================
  *  INITIALIZATION METHODS
  */

  /**
   * Initialize the options.
   * @override
   * @private
   */
  _initOptions(o) {
    // set the defaults
    this.o = {
      backgroundColor: "#282828",
      initAmplitude: 0
    };

    // override defaults with provided options
    super._initOptions(o);
  }

  /**
   * Initialize the audio modules necessary to analyse the volume.
   * @param {AudioContext} audioCtx - The audio context to use.
   */
  _initAudioModules(audioCtx) {
    const _this = this;

    this.audioCtx = audioCtx;
    
    // keep track of audio values
    this.amplitude = 0;
    this.prevAmplitude = 0;
    this.peak = 0;
    this.peakSetTime = audioCtx.currentTime;
    
    // create the script processor
    // TODO: ScriptProcessorNode is soon to be derpecated and replaced by AudioWorker
    this.scriptProcessor = this.audioCtx.createScriptProcessor(512, 1, 1);
    this.scriptProcessor.connect(this.audioCtx.destination);
    this.scriptProcessor.onaudioprocess = function(e) {
      _this.amplitude = _this._calcAmplitude(e.inputBuffer.getChannelData(0));
      _this.peak = _this._calcPeak();
    };

    this.input = this.scriptProcessor;
  }

  /**
   * Initialize the canvas elements.
   * @private
   */
  _initCanvasElements() {
    if (this.canvas === undefined) {
      this.canvas = document.createElement("canvas");
      this.container.appendChild(this.canvas);
      this.ctx = this.canvas.getContext("2d");
    }
  
    let containerDims = this.container.getBoundingClientRect();
    
    this.canvas.setAttribute("width", containerDims.width);
    this.canvas.setAttribute("height", containerDims.height);

    this.ledGradient = this.ctx.createLinearGradient(0, this.canvas.height, 0, 0);
    this.ledGradient.addColorStop(0, 'green');
    this.ledGradient.addColorStop(0.6, 'lightgreen');
    this.ledGradient.addColorStop(0.8, 'yellow');
    this.ledGradient.addColorStop(1, 'red');

    this._update();
  }

  /**
   * Update (redraw) component based on state.
   * @override
   * @private
   */
  _update() {
    const _this = this;

    let containerDims = this.container.getBoundingClientRect();
    let width = containerDims.width;
    let height = containerDims.height;

    redraw();

    function redraw() {
      let ledHeight = height * _this.amplitude;   
      let peakYPos = height * _this.peak;

      _this.ctx.clearRect(0, 0, width, height);
      
      // draw the background
      _this.ctx.fillStyle = _this.o.backgroundColor;
      _this.ctx.fillRect(0, 0, width, height);

      // draw the led
      _this.ctx.fillStyle = _this.ledGradient;
      _this.ctx.fillRect(0, height - ledHeight, width, ledHeight);

      // draw the peak
      _this.ctx.fillStyle = _this.ledGradient;
      _this.ctx.fillRect(0, peakYPos, width, 10);
      
      window.requestAnimationFrame(redraw);
    } 
  }

  /* ===========================================================================
  *  PUBLIC API
  */

  /**
   * Recieve audio from a source.
   * @param {AudioNode} audioSource - The audio source to connect.
   */
  receiveAudioFrom(audioSource) {
    audioSource.connect(this.scriptProcessor);
  }

  /* ===========================================================================
  *  HELPER METHODS
  */

  /**
   * Calculate the amplitude for a given audio buffer
   * @param {Float32Array} buffer
   */
  _calcAmplitude(buffer) {
    let sum = 0;
    
    for (let i = 0; i < buffer.length; ++i) {
      sum += buffer[i] * buffer[i];
    }

    return Math.sqrt(sum / buffer.length); 
  }

  /**
   * Calculate the current peak
   */
  _calcPeak() {

    // calculate the peak position
    // special cases - peak = -1 means peak expired and waiting for amplitude to rise
    // peak = 0 means amplitude is rising, waiting for peak
    if (this.amplitude < this.prevAmplitude) {
      this.peak = this.prevAmplitude;
      this.peakSetTime = this.audioCtx.currentTime;
    } else if (this.amplitude > this.prevAmplitude) {
      this.peak = 0;
    }

    // draw the peak for 2 seconds, then remove it
    if (this.audioCtx.currentTime - this.peakSetTime > 2 && this.peak !== 0) {
      this.peak = -1;
    }

    this.prevAmplitude = this.amplitude;
  }
}

export default Meter;