'use strict';
// sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
const WEBAUDIO_BLOCK_SIZE = 128;
/** Overlap-Add Node */
class OLAProcessor extends AudioWorkletProcessor {
constructor(options) {
super(options);
this.started = false;
this.nbInputs = options.numberOfInputs;
this.nbOutputs = options.numberOfOutputs;
this.blockSize = options.processorOptions.blockSize;
// TODO for now, the only support hop size is the size of a web audio block
this.hopSize = WEBAUDIO_BLOCK_SIZE;
this.nbOverlaps = this.blockSize / this.hopSize;
// pre-allocate input buffers (will be reallocated if needed)
this.inputBuffers = new Array(this.nbInputs);
this.inputBuffersHead = new Array(this.nbInputs);
this.inputBuffersToSend = new Array(this.nbInputs);
// default to 1 channel per input until we know more
for (let i = 0; i < this.nbInputs; i++) {
this.allocateInputChannels(i, 1);
}
// pre-allocate input buffers (will be reallocated if needed)
this.outputBuffers = new Array(this.nbOutputs);
this.outputBuffersToRetrieve = new Array(this.nbOutputs);
// default to 1 channel per output until we know more
for (let i = 0; i < this.nbOutputs; i++) {
this.allocateOutputChannels(i, 1);
}
}
/** Handles dynamic reallocation of input/output channels buffer
(channel numbers may lety during lifecycle) **/
reallocateChannelsIfNeeded(inputs, outputs) {
for (let i = 0; i < this.nbInputs; i++) {
let nbChannels = inputs[i].length;
if (nbChannels != this.inputBuffers[i].length) {
this.allocateInputChannels(i, nbChannels);
}
}
for (let i = 0; i < this.nbOutputs; i++) {
let nbChannels = outputs[i].length;
if (nbChannels != this.outputBuffers[i].length) {
this.allocateOutputChannels(i, nbChannels);
}
}
}
allocateInputChannels(inputIndex, nbChannels) {
// allocate input buffers
this.inputBuffers[inputIndex] = new Array(nbChannels);
for (let i = 0; i < nbChannels; i++) {
this.inputBuffers[inputIndex][i] = new Float32Array(this.blockSize + WEBAUDIO_BLOCK_SIZE);
this.inputBuffers[inputIndex][i].fill(0);
}
// allocate input buffers to send and head pointers to copy from
// (cannot directly send a pointer/subarray because input may be modified)
this.inputBuffersHead[inputIndex] = new Array(nbChannels);
this.inputBuffersToSend[inputIndex] = new Array(nbChannels);
for (let i = 0; i < nbChannels; i++) {
this.inputBuffersHead[inputIndex][i] = this.inputBuffers[inputIndex][i].subarray(0, this.blockSize);
this.inputBuffersToSend[inputIndex][i] = new Float32Array(this.blockSize);
}
}
allocateOutputChannels(outputIndex, nbChannels) {
// allocate output buffers
this.outputBuffers[outputIndex] = new Array(nbChannels);
for (let i = 0; i < nbChannels; i++) {
this.outputBuffers[outputIndex][i] = new Float32Array(this.blockSize);
this.outputBuffers[outputIndex][i].fill(0);
}
// allocate output buffers to retrieve
// (cannot send a pointer/subarray because new output has to be add to exising output)
this.outputBuffersToRetrieve[outputIndex] = new Array(nbChannels);
for (let i = 0; i < nbChannels; i++) {
this.outputBuffersToRetrieve[outputIndex][i] = new Float32Array(this.blockSize);
this.outputBuffersToRetrieve[outputIndex][i].fill(0);
}
}
/** Read next web audio block to input buffers **/
readInputs(inputs) {
// when playback is paused, we may stop receiving new samples
if (inputs[0].length && inputs[0][0].length == 0) {
for (let i = 0; i < this.nbInputs; i++) {
for (let j = 0; j < this.inputBuffers[i].length; j++) {
this.inputBuffers[i][j].fill(0, this.blockSize);
}
}
return;
}
for (let i = 0; i < this.nbInputs; i++) {
for (let j = 0; j < this.inputBuffers[i].length; j++) {
let webAudioBlock = inputs[i][j];
this.inputBuffers[i][j].set(webAudioBlock, this.blockSize);
}
}
}
/** Write next web audio block from output buffers **/
writeOutputs(outputs) {
for (let i = 0; i < this.nbInputs; i++) {
for (let j = 0; j < this.inputBuffers[i].length; j++) {
let webAudioBlock = this.outputBuffers[i][j].subarray(0, WEBAUDIO_BLOCK_SIZE);
outputs[i][j].set(webAudioBlock);
}
}
}
/** Shift left content of input buffers to receive new web audio block **/
shiftInputBuffers() {
for (let i = 0; i < this.nbInputs; i++) {
for (let j = 0; j < this.inputBuffers[i].length; j++) {
this.inputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE);
}
}
}
/** Shift left content of output buffers to receive new web audio block **/
shiftOutputBuffers() {
for (let i = 0; i < this.nbOutputs; i++) {
for (let j = 0; j < this.outputBuffers[i].length; j++) {
this.outputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE);
this.outputBuffers[i][j].subarray(this.blockSize - WEBAUDIO_BLOCK_SIZE).fill(0);
}
}
}
/** Copy contents of input buffers to buffer actually sent to process **/
prepareInputBuffersToSend() {
for (let i = 0; i < this.nbInputs; i++) {
for (let j = 0; j < this.inputBuffers[i].length; j++) {
this.inputBuffersToSend[i][j].set(this.inputBuffersHead[i][j]);
}
}
}
/** Add contents of output buffers just processed to output buffers **/
handleOutputBuffersToRetrieve() {
for (let i = 0; i < this.nbOutputs; i++) {
for (let j = 0; j < this.outputBuffers[i].length; j++) {
for (let k = 0; k < this.blockSize; k++) {
this.outputBuffers[i][j][k] += this.outputBuffersToRetrieve[i][j][k] / this.nbOverlaps;
}
}
}
}
process(inputs, outputs, params) {
const input = inputs[0];
const hasInput = !(input[0] === undefined);
if (this.started && !hasInput) {
return false;
}
this.started = hasInput;
this.reallocateChannelsIfNeeded(inputs, outputs);
this.readInputs(inputs);
this.shiftInputBuffers();
this.prepareInputBuffersToSend();
this.processOLA(this.inputBuffersToSend, this.outputBuffersToRetrieve, params);
this.handleOutputBuffersToRetrieve();
this.writeOutputs(outputs);
this.shiftOutputBuffers();
return true;
}
processOLA(inputs, outputs, params) {
console.assert(false, 'Not overriden');
}
}
export default OLAProcessor;