Source: client/source/AudioInBuffer.js

import BaseLfo from '../../core/BaseLfo';
import SourceMixin from '../../core/SourceMixin';


const definitions = {
  audioBuffer: {
    type: 'any',
    default: null,
    constant: true,
  },
  frameSize: {
    type: 'integer',
    default: 512,
    constant: true,
  },
  channel: {
    type: 'integer',
    default: 0,
    constant: true,
  },
  progressCallback: {
    type: 'any',
    default: null,
    nullable: true,
    constant: true,
  },
  progressCallback: {
    type: 'any',
    default: null,
    nullable: true,
    constant: true,
  },
  async: {
    type: 'boolean',
    default: false,
  },
};

const noop = function() {};

/**
 * Slice an `AudioBuffer` into signal blocks and propagate the resulting frames
 * through the graph.
 *
 * @param {Object} options - Override parameter' default values.
 * @param {AudioBuffer} [options.audioBuffer] - Audio buffer to process.
 * @param {Number} [options.frameSize=512] - Size of the output blocks.
 * @param {Number} [options.channel=0] - Number of the channel to process.
 * @param {Number} [options.progressCallback=null] - Callback to be excuted on each
 *  frame output, receive as argument the current progress ratio.
 *
 * @memberof module:client.source
 *
 * @example
 * import * as lfo from 'waves-lfo/client';
 *
 * const audioInBuffer = new lfo.source.AudioInBuffer({
 *   audioBuffer: audioBuffer,
 *   frameSize: 512,
 * });
 *
 * const waveform = new lfo.sink.Waveform({
 *   canvas: '#waveform',
 *   duration: 1,
 *   color: 'steelblue',
 *   rms: true,
 * });
 *
 * audioInBuffer.connect(waveform);
 * audioInBuffer.start();
 */
class AudioInBuffer extends SourceMixin(BaseLfo) {
  constructor(options = {}) {
    super(definitions, options);

    const audioBuffer = this.params.get('audioBuffer');

    if (!audioBuffer)
      throw new Error('Invalid "audioBuffer" parameter');

    this.endTime = 0;
  }

  /**
   * Propagate the `streamParams` in the graph and start propagating frames.
   * When called, the slicing of the given `audioBuffer` starts immediately and
   * each resulting frame is propagated in graph.
   *
   * @see {@link module:core.BaseLfo#processStreamParams}
   * @see {@link module:core.BaseLfo#resetStream}
   * @see {@link module:client.source.AudioInBuffer#stop}
   */
  start() {
    if (this.initialized === false) {
      if (this.initPromise === null) // init has not yet been called
        this.initPromise = this.init();

      this.initPromise.then(this.start);
      return;
    }

    const channel = this.params.get('channel');
    const audioBuffer = this.params.get('audioBuffer');
    const buffer = audioBuffer.getChannelData(channel);
    this.endTime = 0;
    this.started = true;

    this.processFrame(buffer);
  }

  /**
   * Finalize the stream and stop the whole graph. When called, the slicing of
   * the `audioBuffer` stops immediately.
   *
   * @see {@link module:core.BaseLfo#finalizeStream}
   * @see {@link module:client.source.AudioInBuffer#start}
   */
  stop() {
    this.finalizeStream(this.endTime);
    this.started = false;
  }

  /** @private */
  processStreamParams() {
    const audioBuffer = this.params.get('audioBuffer');
    const frameSize = this.params.get('frameSize');
    const sourceSampleRate = audioBuffer.sampleRate;
    const frameRate = sourceSampleRate / frameSize;

    this.streamParams.frameSize = frameSize;
    this.streamParams.frameRate = frameRate;
    this.streamParams.frameType = 'signal';
    this.streamParams.sourceSampleRate = sourceSampleRate;
    this.streamParams.sourceSampleCount = frameSize;

    this.propagateStreamParams();
  }

  /** @private */
  processFrame(buffer) {
    const async = this.params.get('async');
    const sampleRate = this.streamParams.sourceSampleRate;
    const frameSize = this.streamParams.frameSize;
    const progressCallback = this.params.get('progressCallback') || noop;
    const length = buffer.length;
    const nbrFrames = Math.ceil(buffer.length / frameSize);
    const data = this.frame.data;
    const that = this;
    let i = 0;

    function slice() {
      const offset = i * frameSize;
      const nbrCopy = Math.min(length - offset, frameSize);

      for (let j = 0; j < frameSize; j++)
        data[j] = j < nbrCopy ? buffer[offset + j] : 0;

      that.frame.time = offset / sampleRate;
      that.endTime = that.frame.time + nbrCopy / sampleRate;
      that.propagateFrame();

      i += 1;
      progressCallback(i / nbrFrames);

      if (i < nbrFrames) {
        if (async)
          setTimeout(slice, 0);
        else
          slice();
      } else {
        that.finalizeStream(that.endTime);
      }
    };

    // allow the following to do the expected thing:
    // audioIn.connect(recorder);
    // audioIn.start();
    // recorder.start();
    setTimeout(slice, 0);
  }
}

export default AudioInBuffer;