Source: client/sink/WaveformDisplay.js

import BaseDisplay from './BaseDisplay';
import MinMax from '../../common/operator/MinMax';
import Rms from '../../common/operator/Rms';
import { getColors } from '../utils/display-utils';


const definitions = {
  colors: {
    type: 'any',
    default: getColors('waveform'),
    metas: { kind: 'dyanmic' },
  },
  rms: {
    type: 'boolean',
    default: false,
    metas: { kind: 'dyanmic' },
  }
};

/**
 * Display a waveform (along with optionnal Rms) of a given `signal` input in
 * a canvas.
 *
 * @param {Object} options - Override default parameters.
 * @param {Array<String>} [options.colors=['waveform', 'rms']] - Array
 *  containing the color codes for the waveform (index 0) and rms (index 1).
 *  _dynamic parameter_
 * @param {Boolean} [options.rms=false] - Set to `true` to display the rms.
 *  _dynamic parameter_
 * @param {Number} [options.duration=1] - Duration (in seconds) represented in
 *  the canvas. _dynamic parameter_
 * @param {Number} [options.min=-1] - Minimum value represented in the canvas.
 *  _dynamic parameter_
 * @param {Number} [options.max=1] - Maximum value represented in the canvas.
 *  _dynamic parameter_
 * @param {Number} [options.width=300] - Width of the canvas.
 *  _dynamic parameter_
 * @param {Number} [options.height=150] - Height of the canvas.
 *  _dynamic parameter_
 * @param {Element|CSSSelector} [options.container=null] - Container element
 *  in which to insert the canvas. _constant parameter_
 * @param {Element|CSSSelector} [options.canvas=null] - Canvas element
 *  in which to draw. _constant parameter_
 * @param {Number} [options.referenceTime=null] - Optionnal reference time the
 *  display should considerer as the origin. Is only usefull when synchronizing
 *  several display using the `DisplaySync` class.
 *
 * @memberof module:client.sink
 *
 * @example
 * import * as lfo from 'waves-lfo/client';
 *
 * const audioContext = new window.AudioContext();
 *
 * navigator.mediaDevices
 *   .getUserMedia({ audio: true })
 *   .then(init)
 *   .catch((err) => console.error(err.stack));
 *
 * function init(stream) {
 *   const audioIn = audioContext.createMediaStreamSource(stream);
 *
 *   const audioInNode = new lfo.source.AudioInNode({
 *     audioContext: audioContext,
 *     sourceNode: audioIn,
 *     frameSize: 512,
 *   });
 *
 *   const waveformDisplay = new lfo.sink.WaveformDisplay({
 *     canvas: '#waveform',
 *     duration: 3.5,
 *     rms: true,
 *   });
 *
 *   audioInNode.connect(waveformDisplay);
 *   audioInNode.start();
 * });
 */
class WaveformDisplay extends BaseDisplay {
  constructor(options) {
    super(definitions, options, true);

    this.minMaxOperator = new MinMax();
    this.rmsOperator = new Rms();
  }

  /** @private */
  processStreamParams(prevStreamParams) {
    this.prepareStreamParams(prevStreamParams);

    this.minMaxOperator.initStream(this.streamParams);
    this.rmsOperator.initStream(this.streamParams);

    this.propagateStreamParams();
  }

  /** @private */
  processSignal(frame, frameWidth, pixelsSinceLastFrame) {
    // drop frames that cannot be displayed
    if (frameWidth < 1) return;

    const colors = this.params.get('colors');
    const showRms = this.params.get('rms');
    const ctx = this.ctx;
    const data = frame.data;
    const iSamplesPerPixels = Math.floor(data.length / frameWidth);

    for (let index = 0; index < frameWidth; index++) {
      const start = index * iSamplesPerPixels;
      const end = index === frameWidth - 1 ? undefined : start + iSamplesPerPixels;
      const slice = data.subarray(start, end);

      const minMax = this.minMaxOperator.inputSignal(slice);
      const minY = this.getYPosition(minMax[0]);
      const maxY = this.getYPosition(minMax[1]);

      ctx.strokeStyle = colors[0];
      ctx.beginPath();
      ctx.moveTo(index, minY);
      ctx.lineTo(index, maxY);
      ctx.closePath();
      ctx.stroke();

      if (showRms) {
        const rms = this.rmsOperator.inputSignal(slice);
        const rmsMaxY = this.getYPosition(rms);
        const rmsMinY = this.getYPosition(-rms);

        ctx.strokeStyle = colors[1];
        ctx.beginPath();
        ctx.moveTo(index, rmsMinY);
        ctx.lineTo(index, rmsMaxY);
        ctx.closePath();
        ctx.stroke();
      }
    }
  }
}

export default WaveformDisplay;