Source: common/operator/Slicer.js

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

const definitions = {
  frameSize: {
    type: 'integer',
    default: 512,
    metas: { kind: 'static' },
  },
  hopSize: { // should be nullable
    type: 'integer',
    default: null,
    nullable: true,
    metas: { kind: 'static' },
  },
  centeredTimeTags: {
    type: 'boolean',
    default: false,
  }
}

/**
 * Change the `frameSize` and `hopSize` of a `signal` input according to
 * the given options.
 * This operator updates the stream parameters according to its configuration.
 *
 * @memberof module:common.operator
 *
 * @param {Object} options - Override default parameters.
 * @param {Number} [options.frameSize=512] - Frame size of the output signal.
 * @param {Number} [options.hopSize=null] - Number of samples between two
 *  consecutive frames. If null, `hopSize` is set to `frameSize`.
 * @param {Boolean} [options.centeredTimeTags] - Move the time tag to the middle
 *  of the frame.
 *
 * @example
 * import * as lfo from 'waves-lfo/common';
 *
 * const eventIn = new lfo.source.EventIn({
 *   frameType: 'signal',
 *   frameSize: 10,
 *   sampleRate: 2,
 * });
 *
 * const slicer = new lfo.operator.Slicer({
 *   frameSize: 4,
 *   hopSize: 2
 * });
 *
 * const logger = new lfo.sink.Logger({ time: true, data: true });
 *
 * eventIn.connect(slicer);
 * slicer.connect(logger);
 * eventIn.start();
 *
 * eventIn.process(0, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
 * > { time: 0, data: [0, 1, 2, 3] }
 * > { time: 1, data: [2, 3, 4, 5] }
 * > { time: 2, data: [4, 5, 6, 7] }
 * > { time: 3, data: [6, 7, 8, 9] }
 */
class Slicer extends BaseLfo {
  constructor(options = {}) {
    super(definitions, options);

    const hopSize = this.params.get('hopSize');
    const frameSize = this.params.get('frameSize');

    if (!hopSize)
      this.params.set('hopSize', frameSize);

    this.params.addListener(this.onParamUpdate.bind(this));

    this.frameIndex = 0;
  }

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

    const hopSize = this.params.get('hopSize');
    const frameSize = this.params.get('frameSize');

    this.streamParams.frameSize = frameSize;
    this.streamParams.frameRate = prevStreamParams.sourceSampleRate / hopSize;

    if (this.streamParams.frameSize === 1)
      this.streamParams.frameType = 'scalar';
    else
      this.streamParams.frameType = 'signal';

    this.propagateStreamParams();
  }

  /** @private */
  resetStream() {
    super.resetStream();
    this.frameIndex = 0;
  }

  /** @private */
  finalizeStream(endTime) {
    if (this.frameIndex > 0) {
      const frameRate = this.streamParams.frameRate;
      const frameSize = this.streamParams.frameSize;
      const data = this.frame.data;
      // set the time of the last frame
      this.frame.time += (1 / frameRate);

      for (let i = this.frameIndex; i < frameSize; i++)
        data[i] = 0;

      this.propagateFrame();
    }

    super.finalizeStream(endTime);
  }

  /** @private */
  processFrame(frame) {
    this.prepareFrame();
    this.processFunction(frame);
  }

  /** @private */
  processSignal(frame) {
    const time = frame.time;
    const block = frame.data;
    const metadata = frame.metadata;

    const centeredTimeTags = this.params.get('centeredTimeTags');
    const hopSize = this.params.get('hopSize');
    const outFrame = this.frame.data;
    const frameSize = this.streamParams.frameSize;
    const sampleRate = this.streamParams.sourceSampleRate;
    const samplePeriod = 1 / sampleRate;
    const blockSize = block.length;

    let frameIndex = this.frameIndex;
    let blockIndex = 0;

    while (blockIndex < blockSize) {
      let numSkip = 0;

      // skip block samples for negative frameIndex (frameSize < hopSize)
      if (frameIndex < 0) {
        numSkip = -frameIndex;
        frameIndex = 0; // reset `frameIndex`
      }

      if (numSkip < blockSize) {
        blockIndex += numSkip; // skip block segment
        // can copy all the rest of the incoming block
        let numCopy = blockSize - blockIndex;
        // connot copy more than what fits into the frame
        const maxCopy = frameSize - frameIndex;

        if (numCopy >= maxCopy)
          numCopy = maxCopy;

        // copy block segment into frame
        const copy = block.subarray(blockIndex, blockIndex + numCopy);
        outFrame.set(copy, frameIndex);
        // advance block and frame index
        blockIndex += numCopy;
        frameIndex += numCopy;

        // send frame when completed
        if (frameIndex === frameSize) {
          // define time tag for the outFrame according to configuration
          if (centeredTimeTags)
            this.frame.time = time + (blockIndex - frameSize / 2) * samplePeriod;
          else
            this.frame.time = time + (blockIndex - frameSize) * samplePeriod;

          this.frame.metadata = metadata;
          // forward to next nodes
          this.propagateFrame();

          // shift frame left
          if (hopSize < frameSize)
            outFrame.set(outFrame.subarray(hopSize, frameSize), 0);

          frameIndex -= hopSize; // hop forward
        }
      } else {
        // skip entire block
        const blockRest = blockSize - blockIndex;
        frameIndex += blockRest;
        blockIndex += blockRest;
      }
    }

    this.frameIndex = frameIndex;
  }
}

export default Slicer;