Home Manual Reference Source

src/audio/audio-tools.js

/* eslint-disable
 new-cap
 */
/**
 * @title ITSLanguage Javascript Audio
 * @overview This is part of the ITSLanguage Javascript SDK to perform audio related functions.
 * @copyright (c) 2014 ITSLanguage
 * @license MIT
 * @author d-centralize
 */


/**
@module its.Audio.Tools
ITSLanguage Audio tools.
*/


import pcm from 'pcmjs';


/**
 * Generate a dummy Wave file for testing purposes.
 *
 * @param {number} duration - Length of audio in seconds.
 * @returns {string} A new URL containing the Wave file.
 */
export function generateWaveSample(duration) {
  const effect = [];
  const sampleRate = 22000;
  const loops = duration * sampleRate;
  for (let i = 0; i < loops; i++) {
    effect[i] = 64 + Math.round(
      32 * (Math.cos(i * i / 2000) + Math.sin(i * i / 4000)));
  }
  const wave = new pcm({channels: 1, rate: 22000, depth: 8}).toWav(effect);
  return wave.encode();
}


export default class VolumeMeter {
  /**
   * Represents a volume meter.
   *
   * @param {AudioContext} audioContext - The WebAudio context.
   * @param {MediaStream} inputStream - The MediaStream to analyze.
   */
  constructor(audioContext, inputStream) {
    this.audioContext = audioContext;
    this.stream = inputStream;
    this.willAnimate = true;
  }


  /**
   * Start analysing the audio stream and provide updates to the specified callback function.
   *
   * @param {Function} callback - This function is called when there's a new volume reading is available.
   * First parameter is the volume.
   * @param {?Array} args - Optional array of parameters to pass to the callback after the volume parameter.
   */
  getVolumeIndication(callback, args) {
    if (!callback) {
      throw new Error('Callback parameter unspecified.');
    }

    // Convert single callback to Array of callbacks
    if (!(callback instanceof Array)) {
      callback = [callback];
    }

    this.volumeIndicationCallback = callback;
    this.volumeIndicationCallbackArgs = args || [];

    this.analyserNode = this.audioContext.createAnalyser();
    this.analyserNode.fftSize = 2048;
    this.stream.connect(this.analyserNode);

    this._updateAnalysers();
  }

  static _getAverageVolume(array) {
    let values = 0;

    const length = array.length;

    // Get all the frequency amplitudes
    for (let i = 0; i < length; i++) {
      values += array[i];
    }

    const average = values / length;
    return average;
  }

  /**
   * Calculate the volume, inform listeners by executing the callback.
   * Repeat indefinitely.
   */
  _updateAnalysers() {
    const volumeIndicationCallback = this.volumeIndicationCallback;
    const volumeIndicationCallbackArgs = this.volumeIndicationCallbackArgs;
    const analyserNode = this.analyserNode;
    const volumeMeter = this;
    let skippedCallbacks = 0;
    let lastVolume = -1;

    animloop();

    function animloop() {
      /* The Window.requestAnimationFrame() method tells the
       * browser that you wish to perform an animation and
       * requests that the browser call a specified function to
       * update an animation before the next repaint. The method
       * takes as an argument a callback to be invoked before
       * the repaint.
       */
      const requestAnimationFrame = window.requestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.msRequestAnimationFrame;

      const freqByteData = new Uint8Array(analyserNode.frequencyBinCount);

      analyserNode.getByteFrequencyData(freqByteData);
      let averageVolume = VolumeMeter._getAverageVolume(freqByteData);

      if (volumeMeter.willAnimate) {
        requestAnimationFrame(animloop);
      } else {
        // Stop animating, provide callback with zero volume so the
        // meter doesn't appear to be stuck at the last volume level.
        averageVolume = 0;
      }

      // Callback only on substantial changes.
      const minDiff = 1;
      if (parseInt(averageVolume) >= lastVolume - minDiff &&
        parseInt(averageVolume) <= lastVolume + minDiff) {
        // console.log('Skip same average: ' + lastVolume);
        return true;
      }
      // console.log('Got new volume: ' + parseInt(averageVolume) +
      // ' (old: ' + lastVolume + ')');
      lastVolume = parseInt(averageVolume);

      const args = [averageVolume].concat(volumeIndicationCallbackArgs);
      // Fire all callbacks.
      volumeIndicationCallback.forEach(cb => {
        // This kludge prevents firing an averageVolume of zero
        // right away. The buffer probably needs filling before useful
        // values become available. 5 seems to be the magic number.
        if (skippedCallbacks < 5) {
          skippedCallbacks += 1;
          return true;
        }
        return cb(args);
      });
    }
  }

  /**
   * Stop calculating the volume.
   */
  stopAnalyser() {
    this.willAnimate = false;
  }

  /**
   * Start calculating the volume.
   */
  resumeAnalyser() {
    this.willAnimate = true;
  }
}