const WEBAUDIO_BLOCK_SIZE = 128 /** Overlap-Add Node */ class OLAProcessor extends window.AudioWorkletProcessor { constructor(options) { super(options) 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 vary 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) { 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