Source: common/source/EventIn.js

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

// http://stackoverflow.com/questions/17575790/environment-detection-node-js-or-browser
const isNode = new Function('try { return this === global; } catch(e) { return false }');

/**
 * Create a function that returns time in seconds according to the current
 * environnement (node or browser).
 * If running in node the time rely on `process.hrtime`, while if in the browser
 * it is provided by the `currentTime` of an `AudioContext`, this context can
 * optionnaly be provided to keep time consistency between several `EventIn`
 * nodes.
 *
 * @param {AudioContext} [audioContext=null] - Optionnal audio context.
 * @return {Function}
 * @private
 */
function getTimeFunction(audioContext = null) {
  if (isNode()) {
    return () => {
      const t = process.hrtime();
      return t[0] + t[1] * 1e-9;
    }
  } else {
    return () => performance.now() / 1000;
  }
}


const definitions = {
  absoluteTime: {
    type: 'boolean',
    default: false,
    constant: true,
  },
  audioContext: {
    type: 'any',
    default: null,
    constant: true,
    nullable: true,
  },
  frameType: {
    type: 'enum',
    list: ['signal', 'vector', 'scalar'],
    default: 'signal',
    constant: true,
  },
  frameSize: {
    type: 'integer',
    default: 1,
    min: 1,
    max: +Infinity, // not recommended...
    metas: { kind: 'static' },
  },
  sampleRate: {
    type: 'float',
    default: null,
    min: 0,
    max: +Infinity, // same here
    nullable: true,
    metas: { kind: 'static' },
  },
  frameRate: {
    type: 'float',
    default: null,
    min: 0,
    max: +Infinity, // same here
    nullable: true,
    metas: { kind: 'static' },
  },
  description: {
    type: 'any',
    default: null,
    constant: true,
  }
};

/**
 * The `EventIn` operator allows to manually create a stream of data or to feed
 * a stream from another source (e.g. sensors) into a processing graph.
 *
 * @param {Object} options - Override parameters' default values.
 * @param {String} [options.frameType='signal'] - Type of the input - allowed
 * values: `signal`,  `vector` or `scalar`.
 * @param {Number} [options.frameSize=1] - Size of the output frame.
 * @param {Number} [options.sampleRate=null] - Sample rate of the source stream,
 *  if of type `signal`.
 * @param {Number} [options.frameRate=null] - Rate of the source stream, if of
 *  type `vector`.
 * @param {Array|String} [options.description] - Optionnal description
 *  describing the dimensions of the output frame
 * @param {Boolean} [options.absoluteTime=false] - Define if time should be used
 *  as forwarded as given in the process method, or relatively to the time of
 *  the first `process` call after start.
 *
 * @memberof module:common.source
 *
 * @todo - Add a `logicalTime` parameter to tag frame according to frame rate.
 *
 * @example
 * import * as lfo from 'waves-lfo/client';
 *
 * const eventIn = new lfo.source.EventIn({
 *   frameType: 'vector',
 *   frameSize: 3,
 *   frameRate: 1 / 50,
 *   description: ['alpha', 'beta', 'gamma'],
 * });
 *
 * // connect source to operators and sink(s)
 *
 * // initialize and start the graph
 * eventIn.start();
 *
 * // feed `deviceorientation` data into the graph
 * window.addEventListener('deviceorientation', (e) => {
 *   const frame = {
 *     time: window.performace.now() / 1000,
 *     data: [e.alpha, e.beta, e.gamma],
 *   };
 *
 *   eventIn.processFrame(frame);
 * }, false);
 */
class EventIn extends SourceMixin(BaseLfo) {
  constructor(options = {}) {
    super(definitions, options);

    const audioContext = this.params.get('audioContext');
    this._getTime = getTimeFunction(audioContext);
    this._startTime = null;
    this._systemTime = null;
    this._absoluteTime = this.params.get('absoluteTime');
  }

  /**
   * Propagate the `streamParams` in the graph and allow to push frames into
   * the graph. Any call to `process` or `processFrame` before `start` will be
   * ignored.
   *
   * @see {@link module:core.BaseLfo#processStreamParams}
   * @see {@link module:core.BaseLfo#resetStream}
   * @see {@link module:common.source.EventIn#stop}
   */
  start(startTime = null) {
    if (this.initialized === false) {
      if (this.initPromise === null) // init has not yet been called
        this.initPromise = this.init();

      return this.initPromise.then(() => this.start(startTime));
    }

    this._startTime = startTime;
    this._systemTime = null; // value set in the first `process` call

    this.started = true;
  }

  /**
   * Finalize the stream and stop the whole graph. Any call to `process` or
   * `processFrame` after `stop` will be ignored.
   *
   * @see {@link module:core.BaseLfo#finalizeStream}
   * @see {@link module:common.source.EventIn#start}
   */
  stop() {
    if (this.started && this._startTime !== null) {
      const currentTime = this._getTime();
      const endTime = this.frame.time + (currentTime - this._systemTime);

      this.finalizeStream(endTime);
      this.started = false;
    }
  }

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

    // init operator's stream params
    this.streamParams.frameSize = frameType === 'scalar' ? 1 : frameSize;
    this.streamParams.frameType = frameType;
    this.streamParams.description = description;

    if (frameType === 'signal') {
      if (sampleRate === null)
        throw new Error('Undefined "sampleRate" for "signal" stream');

      this.streamParams.sourceSampleRate = sampleRate;
      this.streamParams.frameRate = sampleRate / frameSize;
      this.streamParams.sourceSampleCount = frameSize;

    } else if (frameType === 'vector' || frameType === 'scalar') {
      if (frameRate === null)
        throw new Error(`Undefined "frameRate" for "${frameType}" stream`);

      this.streamParams.frameRate = frameRate;
      this.streamParams.sourceSampleRate = frameRate;
      this.streamParams.sourceSampleCount = 1;
    }

    this.propagateStreamParams();
  }

  /** @private */
  processFunction(frame) {
    const currentTime = this._getTime();
    const inData = frame.data.length ? frame.data : [frame.data];
    const outData = this.frame.data;
    // if no time provided, use system time
    let time = Number.isFinite(frame.time) ? frame.time : currentTime;

    if (this._startTime === null)
      this._startTime = time;

    if (this._absoluteTime === false)
      time = time - this._startTime;

    for (let i = 0, l = this.streamParams.frameSize; i < l; i++)
      outData[i] = inData[i];

    this.frame.time = time;
    this.frame.metadata = frame.metadata;
    // store current time to compute `endTime` on stop
    this._systemTime = currentTime;
  }

  /**
   * Alternative interface to propagate a frame in the graph. Pack `time`,
   * `data` and `metadata` in a frame object.
   *
   * @param {Number} time - Frame time.
   * @param {Float32Array|Array} data - Frame data.
   * @param {Object} metadata - Optionnal frame metadata.
   *
   * @example
   * eventIn.process(1, [0, 1, 2]);
   * // is equivalent to
   * eventIn.processFrame({ time: 1, data: [0, 1, 2] });
   */
  process(time, data, metadata = null) {
    this.processFrame({ time, data, metadata });
  }

  /**
   * Propagate a frame object in the graph.
   *
   * @param {Object} frame - Input frame.
   * @param {Number} frame.time - Frame time.
   * @param {Float32Array|Array} frame.data - Frame data.
   * @param {Object} [frame.metadata=undefined] - Optionnal frame metadata.
   *
   * @example
   * eventIn.processFrame({ time: 1, data: [0, 1, 2] });
   */
  processFrame(frame) {
    if (!this.started) return;

    this.prepareFrame();
    this.processFunction(frame);
    this.propagateFrame();
  }
}

export default EventIn;