1'use strict'; 2 3// sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file 4const WEBAUDIO_BLOCK_SIZE = 128; 5 6/** 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; 19 20 // 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 } 36 37 /** Handles dynamic reallocation of input/output channels buffer 38 * (channel numbers may vary during lifecycle) 39 * @tags internals 40 **/ 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 } 56 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 } 65 66 // allocate input buffers to send and head pointers to copy from 67 // (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 } 75 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 } 83 84 // allocate output buffers to retrieve 85 // (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 } 92 93 /** 94 * Read next web audio block to input buffers 95 * @tags internals 96 **/ 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 } 115 116 /** Write next web audio block from output buffers 117 * @tags internals 118 **/ 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 } 127 128 /** Shift left content of input buffers to receive new web audio block 129 * @tags internals 130 **/ 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 } 138 139 /** Shift left content of output buffers to receive new web audio block 140 * @tags internals 141 **/ 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 } 150 151 /** Copy contents of input buffers to buffer actually sent to process 152 * @tags internals 153 **/ 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 } 161 162 /** Add contents of output buffers just processed to output buffers 163 * @tags internals 164 **/ 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 } 174 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;