jevstrudel.git / packages / superdough / ola-processor.js
1'use strict';

sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file

4const WEBAUDIO_BLOCK_SIZE = 128;

Overlap-Add Node

7class OLAProcessor extends AudioWorkletProcessor {
8  constructor(options) {
9    super(options);
10    this.started = false;
11    this.nbInputs = options.numberOfInputs;
12    this.nbOutputs = options.numberOfOutputs;
13
14    this.blockSize = options.processorOptions.blockSize;
15    // TODO for now, the only support hop size is the size of a web audio block
16    this.hopSize = WEBAUDIO_BLOCK_SIZE;
17
18    this.nbOverlaps = this.blockSize / this.hopSize;

pre-allocate input buffers (will be reallocated if needed)

21    this.inputBuffers = new Array(this.nbInputs);
22    this.inputBuffersHead = new Array(this.nbInputs);
23    this.inputBuffersToSend = new Array(this.nbInputs);
24    // default to 1 channel per input until we know more
25    for (let i = 0; i < this.nbInputs; i++) {
26      this.allocateInputChannels(i, 1);
27    }
28    // pre-allocate input buffers (will be reallocated if needed)
29    this.outputBuffers = new Array(this.nbOutputs);
30    this.outputBuffersToRetrieve = new Array(this.nbOutputs);
31    // default to 1 channel per output until we know more
32    for (let i = 0; i < this.nbOutputs; i++) {
33      this.allocateOutputChannels(i, 1);
34    }
35  }

Handles dynamic reallocation of input/output channels buffer (channel numbers may vary during lifecycle) @tags internals

41  reallocateChannelsIfNeeded(inputs, outputs) {
42    for (let i = 0; i < this.nbInputs; i++) {
43      let nbChannels = inputs[i].length;
44      if (nbChannels != this.inputBuffers[i].length) {
45        this.allocateInputChannels(i, nbChannels);
46      }
47    }
48
49    for (let i = 0; i < this.nbOutputs; i++) {
50      let nbChannels = outputs[i].length;
51      if (nbChannels != this.outputBuffers[i].length) {
52        this.allocateOutputChannels(i, nbChannels);
53      }
54    }
55  }
57  allocateInputChannels(inputIndex, nbChannels) {
58    // allocate input buffers
59
60    this.inputBuffers[inputIndex] = new Array(nbChannels);
61    for (let i = 0; i < nbChannels; i++) {
62      this.inputBuffers[inputIndex][i] = new Float32Array(this.blockSize + WEBAUDIO_BLOCK_SIZE);
63      this.inputBuffers[inputIndex][i].fill(0);
64    }

allocate input buffers to send and head pointers to copy from (cannot directly send a pointer/subarray because input may be modified)

68    this.inputBuffersHead[inputIndex] = new Array(nbChannels);
69    this.inputBuffersToSend[inputIndex] = new Array(nbChannels);
70    for (let i = 0; i < nbChannels; i++) {
71      this.inputBuffersHead[inputIndex][i] = this.inputBuffers[inputIndex][i].subarray(0, this.blockSize);
72      this.inputBuffersToSend[inputIndex][i] = new Float32Array(this.blockSize);
73    }
74  }
76  allocateOutputChannels(outputIndex, nbChannels) {
77    // allocate output buffers
78    this.outputBuffers[outputIndex] = new Array(nbChannels);
79    for (let i = 0; i < nbChannels; i++) {
80      this.outputBuffers[outputIndex][i] = new Float32Array(this.blockSize);
81      this.outputBuffers[outputIndex][i].fill(0);
82    }

allocate output buffers to retrieve (cannot send a pointer/subarray because new output has to be add to exising output)

86    this.outputBuffersToRetrieve[outputIndex] = new Array(nbChannels);
87    for (let i = 0; i < nbChannels; i++) {
88      this.outputBuffersToRetrieve[outputIndex][i] = new Float32Array(this.blockSize);
89      this.outputBuffersToRetrieve[outputIndex][i].fill(0);
90    }
91  }

Read next web audio block to input buffers @tags internals

97  readInputs(inputs) {
98    // when playback is paused, we may stop receiving new samples
99    if (inputs[0].length && inputs[0][0].length == 0) {
100      for (let i = 0; i < this.nbInputs; i++) {
101        for (let j = 0; j < this.inputBuffers[i].length; j++) {
102          this.inputBuffers[i][j].fill(0, this.blockSize);
103        }
104      }
105      return;
106    }
107
108    for (let i = 0; i < this.nbInputs; i++) {
109      for (let j = 0; j < this.inputBuffers[i].length; j++) {
110        let webAudioBlock = inputs[i][j];
111        this.inputBuffers[i][j].set(webAudioBlock, this.blockSize);
112      }
113    }
114  }

Write next web audio block from output buffers @tags internals

119  writeOutputs(outputs) {
120    for (let i = 0; i < this.nbInputs; i++) {
121      for (let j = 0; j < this.inputBuffers[i].length; j++) {
122        let webAudioBlock = this.outputBuffers[i][j].subarray(0, WEBAUDIO_BLOCK_SIZE);
123        outputs[i][j].set(webAudioBlock);
124      }
125    }
126  }

Shift left content of input buffers to receive new web audio block @tags internals

131  shiftInputBuffers() {
132    for (let i = 0; i < this.nbInputs; i++) {
133      for (let j = 0; j < this.inputBuffers[i].length; j++) {
134        this.inputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE);
135      }
136    }
137  }

Shift left content of output buffers to receive new web audio block @tags internals

142  shiftOutputBuffers() {
143    for (let i = 0; i < this.nbOutputs; i++) {
144      for (let j = 0; j < this.outputBuffers[i].length; j++) {
145        this.outputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE);
146        this.outputBuffers[i][j].subarray(this.blockSize - WEBAUDIO_BLOCK_SIZE).fill(0);
147      }
148    }
149  }

Copy contents of input buffers to buffer actually sent to process @tags internals

154  prepareInputBuffersToSend() {
155    for (let i = 0; i < this.nbInputs; i++) {
156      for (let j = 0; j < this.inputBuffers[i].length; j++) {
157        this.inputBuffersToSend[i][j].set(this.inputBuffersHead[i][j]);
158      }
159    }
160  }

Add contents of output buffers just processed to output buffers @tags internals

165  handleOutputBuffersToRetrieve() {
166    for (let i = 0; i < this.nbOutputs; i++) {
167      for (let j = 0; j < this.outputBuffers[i].length; j++) {
168        for (let k = 0; k < this.blockSize; k++) {
169          this.outputBuffers[i][j][k] += this.outputBuffersToRetrieve[i][j][k] / this.nbOverlaps;
170        }
171      }
172    }
173  }
175  process(inputs, outputs, params) {
176    const input = inputs[0];
177    const hasInput = !(input[0] === undefined);
178    if (this.started && !hasInput) {
179      return false;
180    }
181    this.started = hasInput;
182    this.reallocateChannelsIfNeeded(inputs, outputs);
183
184    this.readInputs(inputs);
185    this.shiftInputBuffers();
186    this.prepareInputBuffersToSend();
187    this.processOLA(this.inputBuffersToSend, this.outputBuffersToRetrieve, params);
188    this.handleOutputBuffersToRetrieve();
189    this.writeOutputs(outputs);
190    this.shiftOutputBuffers();
191
192    return true;
193  }
194
195  processOLA(inputs, outputs, params) {
196    console.assert(false, 'Not overriden');
197  }
198}
199
200export default OLAProcessor;