First pass of worklets optimizations

This commit is contained in:
Aria 2025-11-12 10:48:20 -06:00
parent 15f7b7c1a2
commit 3c6de3be6d

View file

@ -6,13 +6,26 @@ import OLAProcessor from './ola-processor';
import FFT from './fft.js'; import FFT from './fft.js';
import { getDistortionAlgorithm } from './helpers.mjs'; import { getDistortionAlgorithm } from './helpers.mjs';
const blockSize = 128;
const PI = Math.PI;
const TWO_PI = 2 * PI;
const INVSR = 1 / sampleRate;
const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
const mod = (n, m) => ((n % m) + m) % m; const mod = (n, m) => ((n % m) + m) % m;
const lerp = (a, b, n) => n * (b - a) + a; const lerp = (a, b, n) => n * (b - a) + a;
const pv = (arr, n) => arr[n] ?? arr[0]; const pv = (arr, n) => arr[n] ?? arr[0];
const frac = (x) => x - Math.floor(x); const frac = (x) => x - Math.floor(x);
const ffloor = (x) => x | 0; // fast floor for non-negative
// Fast integer ops for non-negative values
const ffloor = (x) => x | 0;
const fround = (x) => ffloor(x + 0.5);
const fceil = (x) => ffloor(x + 1);
const fast_tanh = (x) => {
const x2 = x * x;
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
};
const getUnisonDetune = (unison, detune, voiceIndex) => { const getUnisonDetune = (unison, detune, voiceIndex) => {
if (unison < 2) { if (unison < 2) {
return 0; return 0;
@ -32,7 +45,6 @@ function wrapPhase(phase, maxPhase = 1) {
} }
return phase; return phase;
} }
const blockSize = 128;
// Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing // Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517 // referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
function polyBlep(phase, dt) { function polyBlep(phase, dt) {
@ -66,7 +78,7 @@ const waveshapes = {
return phase / skew; return phase / skew;
}, },
sine(phase) { sine(phase) {
return Math.sin(Math.PI * 2 * phase) * 0.5 + 0.5; return Math.sin(TWO_PI * phase) * 0.5 + 0.5;
}, },
ramp(phase) { ramp(phase) {
return phase; return phase;
@ -100,12 +112,6 @@ const waveshapes = {
return v - polyBlep(phase, dt); return v - polyBlep(phase, dt);
}, },
}; };
function getParamValue(block, param) {
if (param.length > 1) {
return param[block];
}
return param[0];
}
const waveShapeNames = Object.keys(waveshapes); const waveShapeNames = Object.keys(waveshapes);
class LFOProcessor extends AudioWorkletProcessor { class LFOProcessor extends AudioWorkletProcessor {
@ -167,7 +173,7 @@ class LFOProcessor extends AudioWorkletProcessor {
if (this.phase == null) { if (this.phase == null) {
this.phase = mod(time * frequency + phaseoffset, 1); this.phase = mod(time * frequency + phaseoffset, 1);
} }
const dt = frequency / sampleRate; const dt = frequency * INVSR;
for (let n = 0; n < blockSize; n++) { for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < output.length; i++) { for (let i = 0; i < output.length; i++) {
let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth; let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
@ -293,8 +299,8 @@ class TwoPoleFilter {
// Out of bound values can produce NaNs // Out of bound values can produce NaNs
resonance = clamp(resonance, 0, 1); resonance = clamp(resonance, 0, 1);
cutoff = clamp(cutoff, 0, sampleRate / 2 - 1); cutoff = clamp(cutoff, 0, sampleRate / 2 - 1);
const c = clamp(2 * Math.sin(cutoff * (_PI / sampleRate)), 0, 1.14); const c = clamp(2 * Math.sin(cutoff * PI * INVSR), 0, 1.14);
const r = Math.pow(0.5, (resonance + 0.125) / 0.125); const r = Math.pow(0.5, 8 * resonance + 1);
const mrc = 1 - r * c; const mrc = 1 - r * c;
this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf
this.s1 = mrc * this.s1 + c * this.s0; // lpf this.s1 = mrc * this.s1 + c * this.s0; // lpf
@ -353,11 +359,6 @@ class DJFProcessor extends AudioWorkletProcessor {
} }
registerProcessor('djf-processor', DJFProcessor); registerProcessor('djf-processor', DJFProcessor);
function fast_tanh(x) {
const x2 = x * x;
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
}
const _PI = 3.14159265359;
//adapted from https://github.com/TheBouteillacBear/webaudioworklet-wasm?tab=MIT-1-ov-file //adapted from https://github.com/TheBouteillacBear/webaudioworklet-wasm?tab=MIT-1-ov-file
class LadderProcessor extends AudioWorkletProcessor { class LadderProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() { static get parameterDescriptors() {
@ -395,7 +396,7 @@ class LadderProcessor extends AudioWorkletProcessor {
const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000); const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000);
let cutoff = parameters.frequency[0]; let cutoff = parameters.frequency[0];
cutoff = (cutoff * 2 * _PI) / sampleRate; cutoff = cutoff * TWO_PI * INVSR;
cutoff = cutoff > 1 ? 1 : cutoff; cutoff = cutoff > 1 ? 1 : cutoff;
const k = Math.min(8, resonance * 0.13); const k = Math.min(8, resonance * 0.13);
@ -545,7 +546,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
const freqVoice = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n)); const freqVoice = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n));
// We must wrap this here because it is passed into sawblep below which // We must wrap this here because it is passed into sawblep below which
// has domain [0, 1] // has domain [0, 1]
const dt = mod(freqVoice / sampleRate, 1); const dt = mod(freqVoice * INVSR, 1);
this.phase[n] = this.phase[n] ?? Math.random(); this.phase[n] = this.phase[n] ?? Math.random();
const v = waveshapes.sawblep(this.phase[n], dt); const v = waveshapes.sawblep(this.phase[n], dt);
@ -564,12 +565,16 @@ registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
// Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file // Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
const BUFFERED_BLOCK_SIZE = 2048; const BUFFERED_BLOCK_SIZE = 2048;
const hannCache = new Map();
function genHannWindow(length) { function genHannWindow(length) {
let win = new Float32Array(length); if (!hannCache.has(length)) {
for (var i = 0; i < length; i++) { const win = new Float32Array(length);
win[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / length)); for (let i = 0; i < length; i++) {
win[i] = 0.5 * (1 - Math.cos((TWO_PI * i) / length));
}
hannCache.set(length, win);
} }
return win; return hannCache.get(length);
} }
class PhaseVocoderProcessor extends OLAProcessor { class PhaseVocoderProcessor extends OLAProcessor {
@ -587,11 +592,14 @@ class PhaseVocoderProcessor extends OLAProcessor {
blockSize: BUFFERED_BLOCK_SIZE, blockSize: BUFFERED_BLOCK_SIZE,
}; };
super(options); super(options);
this.fftSize = this.blockSize;
this.timeCursor = 0; this.timeCursor = 0;
this.fftSize = this.blockSize;
this.hannWindow = genHannWindow(this.blockSize); this.invfftSize = 1 / this.fftSize;
this.hannWindow = genHannWindow(this.fftSize);
// rescale hann window (empirically sounds nicer)
for (let i = 0; i < this.hannWindow.length; i++) {
this.hannWindow[i] *= 1.62;
}
// prepare FFT and pre-allocate buffers // prepare FFT and pre-allocate buffers
this.fft = new FFT(this.fftSize); this.fft = new FFT(this.fftSize);
this.freqComplexBuffer = this.fft.createComplexArray(); this.freqComplexBuffer = this.fft.createComplexArray();
@ -604,54 +612,45 @@ class PhaseVocoderProcessor extends OLAProcessor {
processOLA(inputs, outputs, parameters) { processOLA(inputs, outputs, parameters) {
// no automation, take last value // no automation, take last value
let pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1]; let pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1];
if (pitchFactor < 0) { if (pitchFactor < 0) {
pitchFactor = pitchFactor * 0.25; pitchFactor = pitchFactor * 0.25;
} }
pitchFactor = Math.max(0, pitchFactor + 1); pitchFactor = Math.max(0, pitchFactor + 1);
for (let i = 0; i < this.nbInputs; i++) {
for (var i = 0; i < this.nbInputs; i++) { for (let j = 0; j < inputs[i].length; j++) {
for (var j = 0; j < inputs[i].length; j++) { const input = inputs[i][j];
// big assumption here: output is symetric to input const output = outputs[i][j];
var input = inputs[i][j];
var output = outputs[i][j];
this.applyHannWindow(input); this.applyHannWindow(input);
this.fft.realTransform(this.freqComplexBuffer, input); this.fft.realTransform(this.freqComplexBuffer, input);
this.computeMagnitudes(); this.computeMagnitudes();
this.findPeaks(); this.findPeaks();
this.shiftPeaks(pitchFactor); this.shiftPeaks(pitchFactor);
this.fft.completeSpectrum(this.freqComplexBufferShifted); this.fft.completeSpectrum(this.freqComplexBufferShifted);
this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted); this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted);
this.fft.fromComplexArray(this.timeComplexBuffer, output); this.fft.fromComplexArray(this.timeComplexBuffer, output);
this.applyHannWindow(output); this.applyHannWindow(output);
} }
} }
this.timeCursor += this.hopSize; this.timeCursor += this.hopSize;
} }
/** Apply Hann window in-place */ /** Apply Hann window in-place */
applyHannWindow(input) { applyHannWindow(input) {
for (var i = 0; i < this.blockSize; i++) { for (let i = 0; i < this.blockSize; i++) {
input[i] = input[i] * this.hannWindow[i] * 1.62; input[i] *= this.hannWindow[i];
} }
} }
/** Compute squared magnitudes for peak finding **/ /** Compute squared magnitudes for peak finding **/
computeMagnitudes() { computeMagnitudes() {
var i = 0, let i = 0,
j = 0; j = 0;
while (i < this.magnitudes.length) { while (i < this.magnitudes.length) {
let real = this.freqComplexBuffer[j]; const real = this.freqComplexBuffer[j];
let imag = this.freqComplexBuffer[j + 1]; const imag = this.freqComplexBuffer[j + 1];
// no need to sqrt for peak finding // no need to sqrt for peak finding
this.magnitudes[i] = real ** 2 + imag ** 2; this.magnitudes[i] = real * real + imag * imag;
i += 1; i += 1;
j += 2; j += 2;
} }
@ -660,12 +659,10 @@ class PhaseVocoderProcessor extends OLAProcessor {
/** Find peaks in spectrum magnitudes **/ /** Find peaks in spectrum magnitudes **/
findPeaks() { findPeaks() {
this.nbPeaks = 0; this.nbPeaks = 0;
var i = 2; let i = 2;
let end = this.magnitudes.length - 2; const end = this.magnitudes.length - 2;
while (i < end) { while (i < end) {
let mag = this.magnitudes[i]; const mag = this.magnitudes[i];
if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) { if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) {
i++; i++;
continue; continue;
@ -674,7 +671,6 @@ class PhaseVocoderProcessor extends OLAProcessor {
i++; i++;
continue; continue;
} }
this.peakIndexes[this.nbPeaks] = i; this.peakIndexes[this.nbPeaks] = i;
this.nbPeaks++; this.nbPeaks++;
i += 2; i += 2;
@ -685,53 +681,44 @@ class PhaseVocoderProcessor extends OLAProcessor {
shiftPeaks(pitchFactor) { shiftPeaks(pitchFactor) {
// zero-fill new spectrum // zero-fill new spectrum
this.freqComplexBufferShifted.fill(0); this.freqComplexBufferShifted.fill(0);
for (let i = 0; i < this.nbPeaks; i++) {
for (var i = 0; i < this.nbPeaks; i++) { const peakIndex = this.peakIndexes[i];
let peakIndex = this.peakIndexes[i]; const peakIndexShifted = fround(peakIndex * pitchFactor);
let peakIndexShifted = Math.round(peakIndex * pitchFactor);
if (peakIndexShifted > this.magnitudes.length) { if (peakIndexShifted > this.magnitudes.length) {
break; break;
} }
// find region of influence // find region of influence
var startIndex = 0; let startIndex = 0;
var endIndex = this.fftSize; let endIndex = this.fftSize;
if (i > 0) { if (i > 0) {
let peakIndexBefore = this.peakIndexes[i - 1]; startIndex = peakIndex - fround((peakIndex - this.peakIndexes[i - 1]) / 2);
startIndex = peakIndex - Math.floor((peakIndex - peakIndexBefore) / 2);
} }
if (i < this.nbPeaks - 1) { if (i < this.nbPeaks - 1) {
let peakIndexAfter = this.peakIndexes[i + 1]; endIndex = peakIndex + fceil((this.peakIndexes[i + 1] - peakIndex) / 2);
endIndex = peakIndex + Math.ceil((peakIndexAfter - peakIndex) / 2);
} }
// shift whole region of influence around peak to shifted peak // shift whole region of influence around peak to shifted peak
let startOffset = startIndex - peakIndex; const startOffset = startIndex - peakIndex;
let endOffset = endIndex - peakIndex; const endOffset = endIndex - peakIndex;
for (var j = startOffset; j < endOffset; j++) { const omegaDelta = TWO_PI * this.invfftSize * (binIndexShifted - binIndex);
let binIndex = peakIndex + j; const phaseShiftReal = Math.cos(omegaDelta * this.timeCursor);
let binIndexShifted = peakIndexShifted + j; const phaseShiftImag = Math.sin(omegaDelta * this.timeCursor);
for (let j = startOffset; j < endOffset; j++) {
const binIndex = peakIndex + j;
const binIndexShifted = peakIndexShifted + j;
if (binIndexShifted >= this.magnitudes.length) { if (binIndexShifted >= this.magnitudes.length) {
break; break;
} }
// apply phase correction // apply phase correction
let omegaDelta = (2 * Math.PI * (binIndexShifted - binIndex)) / this.fftSize; const indexReal = 2 * binIndex;
let phaseShiftReal = Math.cos(omegaDelta * this.timeCursor); const indexImag = indexReal + 1;
let phaseShiftImag = Math.sin(omegaDelta * this.timeCursor); const valueReal = this.freqComplexBuffer[indexReal];
const valueImag = this.freqComplexBuffer[indexImag];
let indexReal = binIndex * 2; const valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag;
let indexImag = indexReal + 1; const valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal;
let valueReal = this.freqComplexBuffer[indexReal];
let valueImag = this.freqComplexBuffer[indexImag];
let valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag; const indexShiftedReal = 2 * binIndexShifted;
let valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal; const indexShiftedImag = indexShiftedReal + 1;
let indexShiftedReal = binIndexShifted * 2;
let indexShiftedImag = indexShiftedReal + 1;
this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal; this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal;
this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag; this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag;
} }
@ -745,11 +732,10 @@ registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor);
class PulseOscillatorProcessor extends AudioWorkletProcessor { class PulseOscillatorProcessor extends AudioWorkletProcessor {
constructor() { constructor() {
super(); super();
this.pi = _PI; this.phi = -PI; // phase
this.phi = -this.pi; // phase
this.Y0 = 0; // feedback memories this.Y0 = 0; // feedback memories
this.Y1 = 0; this.Y1 = 0;
this.PW = this.pi; // pulse width this.PW = PI; // pulse width
this.B = 2.3; // feedback coefficient this.B = 2.3; // feedback coefficient
this.dphif = 0; // filtered phase increment this.dphif = 0; // filtered phase increment
this.envf = 0; // filtered envelope this.envf = 0; // filtered envelope
@ -806,9 +792,9 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
dphi; dphi;
for (let i = 0; i < (output[0].length ?? 0); i++) { for (let i = 0; i < (output[0].length ?? 0); i++) {
const pw = (1 - clamp(getParamValue(i, params.pulsewidth), -0.99, 0.99)) * this.pi; const pw = (1 - clamp(pv(params.pulsewidth, i), -0.99, 0.99)) * this.pi;
const detune = getParamValue(i, params.detune); const detune = pv(params.detune, i);
const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100); const freq = applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100);
dphi = freq * (this.pi / (sampleRate * 0.5)); // phase increment dphi = freq * (this.pi / (sampleRate * 0.5)); // phase increment
this.dphif += 0.1 * (dphi - this.dphif); this.dphif += 0.1 * (dphi - this.dphif);
@ -958,9 +944,9 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
} }
const output = outputs[0]; const output = outputs[0];
for (let i = 0; i < output[0].length; i++) { for (let i = 0; i < output[0].length; i++) {
const detune = getParamValue(i, params.detune); const detune = pv(params.detune, i);
const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100); const freq = applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100);
let local_t = (this.t / (sampleRate / 256)) * freq + this.initialOffset; let local_t = 256 * this.t * INVSR * freq + this.initialOffset;
const funcValue = this.func(local_t); const funcValue = this.func(local_t);
let signal = (funcValue & 255) / 127.5 - 1; let signal = (funcValue & 255) / 127.5 - 1;
const out = signal * 0.2; const out = signal * 0.2;
@ -1067,7 +1053,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
this.frameLen = 0; this.frameLen = 0;
this.numFrames = 0; this.numFrames = 0;
this.phase = []; this.phase = [];
this.invSR = 1 / sampleRate;
this.port.onmessage = (e) => { this.port.onmessage = (e) => {
const { type, payload } = e.data || {}; const { type, payload } = e.data || {};
@ -1104,7 +1089,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
_toBits(amt, min = 2, max = 12) { _toBits(amt, min = 2, max = 12) {
const b = max + (min - max) * amt; const b = max + (min - max) * amt;
return { b, n: Math.round(Math.pow(2, b)) }; return { b, n: fround(Math.pow(2, b)) };
} }
_warpPhase(phase, amt, mode) { _warpPhase(phase, amt, mode) {
@ -1139,7 +1124,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
} }
case WarpMode.FOLD: { case WarpMode.FOLD: {
const K = 7; const K = 7;
const k = 1 + Math.max(1, Math.round(K * amt)); const k = 1 + Math.max(1, fround(K * amt));
return Math.abs(frac(k * phase) - 0.5) * 2; return Math.abs(frac(k * phase) - 0.5) * 2;
} }
case WarpMode.PWM: { case WarpMode.PWM: {
@ -1175,7 +1160,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
} }
case WarpMode.BINARY: { case WarpMode.BINARY: {
let { b } = this._toBits(amt, 3); let { b } = this._toBits(amt, 3);
b = Math.round(b); b = fround(b);
const n = 1 << b; const n = 1 << b;
const idx = ffloor(phase * n); const idx = ffloor(phase * n);
const ridx = bitReverse(idx, b); const ridx = bitReverse(idx, b);
@ -1209,7 +1194,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
case WarpMode.LOGISTIC: { case WarpMode.LOGISTIC: {
let x = phase; let x = phase;
const r = 3.6 + 0.4 * amt; const r = 3.6 + 0.4 * amt;
const iters = 1 + Math.round(2 * amt); const iters = 1 + fround(2 * amt);
for (let i = 0; i < iters; i++) x = r * x * (1 - x); for (let i = 0; i < iters; i++) x = r * x * (1 - x);
return clamp(x, 0, 1); return clamp(x, 0, 1);
} }
@ -1296,7 +1281,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
gainR = gain1; gainR = gain1;
} }
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune
const dPhase = fVoice * this.invSR; const dPhase = fVoice * INVSR;
const level = this._chooseMip(dPhase); const level = this._chooseMip(dPhase);
const table = this.tables[level]; const table = this.tables[level];