From e6f3b1f1efca40727230bc1bec88f091094550af Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 10 Jun 2024 00:06:35 -0400 Subject: [PATCH] copied files --- packages/superdough/ola-processor.js | 178 ++++++++++++++++++++++++++ packages/superdough/worklets.mjs | 180 ++++++++++++++++++++++++++- 2 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 packages/superdough/ola-processor.js diff --git a/packages/superdough/ola-processor.js b/packages/superdough/ola-processor.js new file mode 100644 index 00000000..36ed95bd --- /dev/null +++ b/packages/superdough/ola-processor.js @@ -0,0 +1,178 @@ +"use strict"; + +const WEBAUDIO_BLOCK_SIZE = 128; + +/** Overlap-Add Node */ +class OLAProcessor extends 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 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) { + 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; \ No newline at end of file diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 62697ef9..3ecc987c 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1,6 +1,10 @@ // coarse, crush, and shape processors adapted from dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js // LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE // TOFIX: THIS FILE DOES NOT SUPPORT IMPORTS ON DEPOLYMENT + +import OLAProcessor from './ola-processor.js'; +import FFT from 'fft.js'; + const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const _mod = (n, m) => ((n % m) + m) % m; @@ -463,4 +467,178 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { } } -registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor); \ No newline at end of file +registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor); + + + +const BUFFERED_BLOCK_SIZE = 2048; + +function genHannWindow(length) { + let win = new Float32Array(length); + for (var i = 0; i < length; i++) { + win[i] = 0.5 * (1 - Math.cos(2 * Math.PI * i / length)); + } + return win; +} + +class PhaseVocoderProcessor extends OLAProcessor { + static get parameterDescriptors() { + return [{ + name: 'pitchFactor', + defaultValue: 1.0 + }]; + } + + constructor(options) { + options.processorOptions = { + blockSize: BUFFERED_BLOCK_SIZE, + }; + super(options); + + this.fftSize = this.blockSize; + this.timeCursor = 0; + + this.hannWindow = genHannWindow(this.blockSize); + + // prepare FFT and pre-allocate buffers + this.fft = new FFT(this.fftSize); + this.freqComplexBuffer = this.fft.createComplexArray(); + this.freqComplexBufferShifted = this.fft.createComplexArray(); + this.timeComplexBuffer = this.fft.createComplexArray(); + this.magnitudes = new Float32Array(this.fftSize / 2 + 1); + this.peakIndexes = new Int32Array(this.magnitudes.length); + this.nbPeaks = 0; + } + + processOLA(inputs, outputs, parameters) { + // no automation, take last value + const pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1]; + + for (var i = 0; i < this.nbInputs; i++) { + for (var j = 0; j < inputs[i].length; j++) { + // big assumption here: output is symetric to input + var input = inputs[i][j]; + var output = outputs[i][j]; + + this.applyHannWindow(input); + + this.fft.realTransform(this.freqComplexBuffer, input); + + this.computeMagnitudes(); + this.findPeaks(); + this.shiftPeaks(pitchFactor); + + this.fft.completeSpectrum(this.freqComplexBufferShifted); + this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted); + this.fft.fromComplexArray(this.timeComplexBuffer, output); + + this.applyHannWindow(output); + } + } + + this.timeCursor += this.hopSize; + } + + /** Apply Hann window in-place */ + applyHannWindow(input) { + for (var i = 0; i < this.blockSize; i++) { + input[i] = input[i] * this.hannWindow[i]; + } + } + + /** Compute squared magnitudes for peak finding **/ + computeMagnitudes() { + var i = 0, j = 0; + while (i < this.magnitudes.length) { + let real = this.freqComplexBuffer[j]; + let imag = this.freqComplexBuffer[j + 1]; + // no need to sqrt for peak finding + this.magnitudes[i] = real ** 2 + imag ** 2; + i+=1; + j+=2; + } + } + + /** Find peaks in spectrum magnitudes **/ + findPeaks() { + this.nbPeaks = 0; + var i = 2; + let end = this.magnitudes.length - 2; + + while (i < end) { + let mag = this.magnitudes[i]; + + if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) { + i++; + continue; + } + if (this.magnitudes[i + 1] >= mag || this.magnitudes[i + 2] >= mag) { + i++; + continue; + } + + this.peakIndexes[this.nbPeaks] = i; + this.nbPeaks++; + i += 2; + } + } + + /** Shift peaks and regions of influence by pitchFactor into new specturm */ + shiftPeaks(pitchFactor) { + // zero-fill new spectrum + this.freqComplexBufferShifted.fill(0); + + for (var i = 0; i < this.nbPeaks; i++) { + let peakIndex = this.peakIndexes[i]; + let peakIndexShifted = Math.round(peakIndex * pitchFactor); + + if (peakIndexShifted > this.magnitudes.length) { + break; + } + + // find region of influence + var startIndex = 0; + var endIndex = this.fftSize; + if (i > 0) { + let peakIndexBefore = this.peakIndexes[i - 1]; + startIndex = peakIndex - Math.floor((peakIndex - peakIndexBefore) / 2); + } + if (i < this.nbPeaks - 1) { + let peakIndexAfter = this.peakIndexes[i + 1]; + endIndex = peakIndex + Math.ceil((peakIndexAfter - peakIndex) / 2); + } + + // shift whole region of influence around peak to shifted peak + let startOffset = startIndex - peakIndex; + let endOffset = endIndex - peakIndex; + for (var j = startOffset; j < endOffset; j++) { + let binIndex = peakIndex + j; + let binIndexShifted = peakIndexShifted + j; + + if (binIndexShifted >= this.magnitudes.length) { + break; + } + + // apply phase correction + let omegaDelta = 2 * Math.PI * (binIndexShifted - binIndex) / this.fftSize; + let phaseShiftReal = Math.cos(omegaDelta * this.timeCursor); + let phaseShiftImag = Math.sin(omegaDelta * this.timeCursor); + + let indexReal = binIndex * 2; + let indexImag = indexReal + 1; + let valueReal = this.freqComplexBuffer[indexReal]; + let valueImag = this.freqComplexBuffer[indexImag]; + + let valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag; + let valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal; + + let indexShiftedReal = binIndexShifted * 2; + let indexShiftedImag = indexShiftedReal + 1; + this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal; + this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag; + } + } + } +} + +registerProcessor("phase-vocoder-processor", PhaseVocoderProcessor); \ No newline at end of file