Source: common/operator/Delta.js

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


function simpleLinearRegression(values, dt) {
  // means
  let xSum = 0;
  let ySum = 0;
  const length = values.length;

  for (let i = 0; i < length; i++) {
    xSum += i * dt;
    ySum += values[i];
  }

  const xMean = xSum / length;
  const yMean = ySum / length;

  let sumDiffXMeanSquared = 0; // sum[ pow((x - xMean), 2) ]
  let sumDiffYMeanSquared = 0; // sum[ pow((y - yMean), 2) ]
  let sumDiffXYMean = 0;       // sum[ (x - xMean)(y - yMean) ]

  for (let i = 0; i < length; i++) {
    const diffXMean = dt * i - xMean;
    const diffYMean = values[i] - yMean;

    const diffXMeanSquared = diffXMean * diffXMean;
    const diffYMeanSquared = diffYMean * diffYMean;
    const diffXYMean = diffXMean * diffYMean;

    sumDiffXMeanSquared += diffXMeanSquared;
    sumDiffYMeanSquared += diffYMeanSquared;
    sumDiffXYMean += diffXYMean;
  }

  // horizontal line, all y on same line
  if (sumDiffYMeanSquared === 0)
    return 0;

  // Pearson correlation coefficient:
  // cf. https://www.youtube.com/watch?v=2SCg8Kuh0tE
  //
  //                 ∑ [ (x - xMean)(y - yMean) ]
  // r = ------------------------------------------------------
  //     sqrt( ∑ [ pow((x - xMean), 2), pow((y - yMean), 2) ] )
  //
  //
  const r = sumDiffXYMean / Math.sqrt(sumDiffXMeanSquared * sumDiffYMeanSquared);

  // then we have:
  // cf. https://www.youtube.com/watch?v=GhrxgbQnEEU
  //
  // y = a + bx
  // where:
  //         Sy
  // b = r * --
  //         Sx
  //
  // a = yMean - b * xMean
  //
  // S for standard deviation
  //            ∑ [ pow((x - xMean), 2) ]
  // Sx = sqrt( -------------------------  )
  //                      N - 1
  const Sx = Math.sqrt(sumDiffXMeanSquared / (length - 1));
  const Sy = Math.sqrt(sumDiffYMeanSquared / (length - 1));
  const b = r * (Sy / Sx);

  return b;
}

const definitions = {
  size: {
    type: 'integer',
    min: 2,
    max: +Infinity,
    default: 3,
  },
  useFrameRate: {
    type: 'integer',
    min: 0,
    max: +Infinity,
    default: null,
    nullable: true,
  },
};

/**
 * Returns the simple derivative of successive value using
 * simple linear regression.
 * The current implementation assumes a fixed `frameRate` (`frame.time` is ignored)
 *
 * Before the module is filled, it outputs a value of 0.
 *
 * @param {Object} options - Override default parameters
 * @param {Number} [options.size=3] - Size of the window
 * @param {Number} [options.useFrameRate=null] - Override stream frame rate for
 *  the regression
 */
class Delta extends BaseLfo {
  constructor(options = {}) {
    super(definitions, options);

    this.buffers = null;
    this.ringIndex = 0;
    this.frameRate = null;
  }

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

    const frameSize = this.streamParams.frameSize;
    const size = this.params.get('size');
    const bufferSize = frameSize * size;

    this.buffers = [];
    // counter before the operator starts outputing frames
    this.ringIndex = 0;
    this.frameRate = this.params.get('useFrameRate') === null ?
      this.streamParams.frameRate :
      this.params.get('useFrameRate');

    for (let i = 0; i < frameSize; i++)
      this.buffers[i] = new Float32Array(size);

    this.propagateStreamParams();
  }

  /** @private */
  resetStream() {
    super.resetStream();

    const frameSize = this.streamParams.frameSize;
    const size = this.params.get('size');
    const buffers = this.buffers;

    for (let i = 0; i < frameSize; i++) {
      for (let j = 0; j < size; j++)
        buffers[i][j] = 0;
    }

    this.ringIndex = 0;
  }

  /**
   * Assume a stream of vector at a fixed `frameRate`.
   */
  inputVector(data) {
    const size = this.params.get('size');
    const outData = this.frame.data;
    const frameSize = this.streamParams.frameSize;
    // const frameRate = this.streamParams.frameRate;
    const buffers = this.buffers;
    const dt = 1 / this.frameRate;

    // console.log(dt);

    if (this.ringIndex < size)
      this.ringIndex += 1;

    // copy incomming data into buffer
    for (let i = 0; i < frameSize; i++) {
      const buffer = buffers[i];

      // we need to keep the order of the incomming frames
      // so we have to shift all the values in the buffers
      for (let j = 1; j < size; j++)
        buffer[j - 1] = buffer[j];

      buffer[size - 1] = data[i];

      if (this.ringIndex >= size)
        outData[i] = simpleLinearRegression(buffer, dt);
      else
        outData[i] = 0;
    }

    return outData;
  }

  /** @private */
  processVector(frame) {
    this.frame.data = this.inputVector(frame.data);
    // center time according to delta size
    const size = this.params.get('size');
    const frameRate = this.streamParams.frameRate;
    this.frame.time -= 0.5 * (size - 1) / frameRate;
  }
}

export default Delta;