Further optimizations, including detuner

This commit is contained in:
Aria 2025-11-12 11:53:38 -06:00
parent 3c6de3be6d
commit aabf122161

View file

@ -21,44 +21,42 @@ const frac = (x) => x - Math.floor(x);
const ffloor = (x) => x | 0; const ffloor = (x) => x | 0;
const fround = (x) => ffloor(x + 0.5); const fround = (x) => ffloor(x + 0.5);
const fceil = (x) => ffloor(x + 1); const fceil = (x) => ffloor(x + 1);
const ffrac = (x) => x - ffloor(x);
const fast_tanh = (x) => { const fast_tanh = (x) => {
const x2 = x * x; const x2 = x * x;
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2); return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
}; };
const getUnisonDetune = (unison, detune, voiceIndex) => {
// Optimized per-voice detuner which precomputes constants
const getDetuner = (unison, detune) => {
if (unison < 2) { if (unison < 2) {
return 0; return (_voiceIdx) => 0;
} }
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1)); const scale = detune / (unison - 1);
const center = detune * 0.5;
return (voiceIdx) => voiceIdx * scale - center;
}; };
const applySemitoneDetuneToFrequency = (frequency, detune) => { const applySemitoneDetuneToFrequency = (frequency, detune) => {
return frequency * Math.pow(2, detune / 12); return frequency * Math.pow(2, detune / 12);
}; };
// Restrict phase to the range [0, maxPhase) via wrapping
function wrapPhase(phase, maxPhase = 1) {
if (phase >= maxPhase) {
phase -= maxPhase;
} else if (phase < 0) {
phase += maxPhase;
}
return phase;
}
// 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) {
dt = Math.min(dt, 1 - dt); dt = Math.min(dt, 1 - dt);
const invdt = 1 / dt;
// Start of cycle // Start of cycle
if (phase < dt) { if (phase < dt) {
phase /= dt; phase *= invdt;
// 2 * (phase - phase^2/2 - 0.5) // 2 * (phase - phase^2/2 - 0.5)
return phase + phase - phase * phase - 1; return phase + phase - phase * phase - 1;
} }
// End of cycle // End of cycle
else if (phase > 1 - dt) { else if (phase > 1 - dt) {
phase = (phase - 1) / dt; phase = (phase - 1) * invdt;
// 2 * (phase^2/2 + phase + 0.5) // 2 * (phase^2/2 + phase + 0.5)
return phase * phase + phase + phase + 1; return phase * phase + phase + phase + 1;
} }
@ -171,7 +169,7 @@ class LFOProcessor extends AudioWorkletProcessor {
const blockSize = output[0].length ?? 0; const blockSize = output[0].length ?? 0;
if (this.phase == null) { if (this.phase == null) {
this.phase = mod(time * frequency + phaseoffset, 1); this.phase = ffrac(time * frequency + phaseoffset);
} }
const dt = frequency * INVSR; const dt = frequency * INVSR;
for (let n = 0; n < blockSize; n++) { for (let n = 0; n < blockSize; n++) {
@ -533,6 +531,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
let freq = pv(params.frequency, i); let freq = pv(params.frequency, i);
// Main detuning // Main detuning
freq = applySemitoneDetuneToFrequency(freq, detune / 100); freq = applySemitoneDetuneToFrequency(freq, detune / 100);
const detuner = getDetuner(voices, freqspread);
for (let n = 0; n < voices; n++) { for (let n = 0; n < voices; n++) {
const isOdd = (n & 1) == 1; const isOdd = (n & 1) == 1;
let gainL = gain1; let gainL = gain1;
@ -543,17 +542,17 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
gainR = gain1; gainR = gain1;
} }
// Individual voice detuning // Individual voice detuning
const freqVoice = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n)); const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(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 * INVSR, 1); const dt = ffrac(freqVoice * INVSR);
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);
output[0][i] = output[0][i] + v * gainL; output[0][i] += v * gainL;
output[1][i] = output[1][i] + v * gainR; output[1][i] += v * gainR;
this.phase[n] = wrapPhase(this.phase[n] + dt); this.phase[n] = ffrac(this.phase[n] + dt);
} }
} }
return true; return true;
@ -699,7 +698,7 @@ class PhaseVocoderProcessor extends OLAProcessor {
// shift whole region of influence around peak to shifted peak // shift whole region of influence around peak to shifted peak
const startOffset = startIndex - peakIndex; const startOffset = startIndex - peakIndex;
const endOffset = endIndex - peakIndex; const endOffset = endIndex - peakIndex;
const omegaDelta = TWO_PI * this.invfftSize * (binIndexShifted - binIndex); const omegaDelta = TWO_PI * this.invfftSize * (peakIndexShifted - peakIndex);
const phaseShiftReal = Math.cos(omegaDelta * this.timeCursor); const phaseShiftReal = Math.cos(omegaDelta * this.timeCursor);
const phaseShiftImag = Math.sin(omegaDelta * this.timeCursor); const phaseShiftImag = Math.sin(omegaDelta * this.timeCursor);
for (let j = startOffset; j < endOffset; j++) { for (let j = startOffset; j < endOffset; j++) {
@ -792,11 +791,11 @@ 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(pv(params.pulsewidth, i), -0.99, 0.99)) * this.pi; const pw = (1 - clamp(pv(params.pulsewidth, i), -0.99, 0.99)) * PI;
const detune = pv(params.detune, i); const detune = pv(params.detune, i);
const freq = applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100); const freq = applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100);
dphi = freq * (this.pi / (sampleRate * 0.5)); // phase increment dphi = freq * (PI / (sampleRate * 0.5)); // phase increment
this.dphif += 0.1 * (dphi - this.dphif); this.dphif += 0.1 * (dphi - this.dphif);
env *= 0.9998; // exponential decay envelope env *= 0.9998; // exponential decay envelope
@ -808,7 +807,7 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
// Waveform generation (half-Tomisawa oscillators) // Waveform generation (half-Tomisawa oscillators)
this.phi += this.dphif; // phase increment this.phi += this.dphif; // phase increment
if (this.phi >= this.pi) this.phi -= 2 * this.pi; // phase wrapping if (this.phi >= PI) this.phi -= TWO_PI; // phase wrapping
// First half-Tomisawa generator // First half-Tomisawa generator
let out0 = Math.cos(this.phi + this.B * this.Y0); // self-phase modulation let out0 = Math.cos(this.phi + this.B * this.Y0); // self-phase modulation
@ -847,13 +846,13 @@ const chyx = {
} }
}, },
/*sin that loops every 128 "steps", instead of every pi steps*/ sinf: function (x) { /*sin that loops every 128 "steps", instead of every pi steps*/ sinf: function (x) {
return Math.sin(x / (128 / Math.PI)); return Math.sin((x * PI) / 128);
}, },
/*cos that loops every 128 "steps", instead of every pi steps*/ cosf: function (x) { /*cos that loops every 128 "steps", instead of every pi steps*/ cosf: function (x) {
return Math.cos(x / (128 / Math.PI)); return Math.cos((x * PI) / 128);
}, },
/*tan that loops every 128 "steps", instead of every pi steps*/ tanf: function (x) { /*tan that loops every 128 "steps", instead of every pi steps*/ tanf: function (x) {
return Math.tan(x / (128 / Math.PI)); return Math.tan((x * PI) / 128);
}, },
/*converts t into a string composed of it's bits, regex's that*/ regG: function (t, X) { /*converts t into a string composed of it's bits, regex's that*/ regG: function (t, X) {
return X.test(t.toString(2)); return X.test(t.toString(2));
@ -986,6 +985,7 @@ export const WarpMode = Object.freeze({
SIGMOID: 19, SIGMOID: 19,
FRACTAL: 20, FRACTAL: 20,
FLIP: 21, FLIP: 21,
MODULAR: 22,
}); });
function hash32(u) { function hash32(u) {
@ -1125,7 +1125,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
case WarpMode.FOLD: { case WarpMode.FOLD: {
const K = 7; const K = 7;
const k = 1 + Math.max(1, fround(K * amt)); const k = 1 + Math.max(1, fround(K * amt));
return Math.abs(frac(k * phase) - 0.5) * 2; return Math.abs(ffrac(k * phase) - 0.5) * 2;
} }
case WarpMode.PWM: { case WarpMode.PWM: {
const w = clamp(0.5 + 0.49 * (2 * amt - 1), 0, 1); const w = clamp(0.5 + 0.49 * (2 * amt - 1), 0, 1);
@ -1135,12 +1135,12 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
case WarpMode.ORBIT: { case WarpMode.ORBIT: {
const depth = 0.5 * amt; const depth = 0.5 * amt;
const n = 3; const n = 3;
return frac(phase + depth * Math.sin(2 * Math.PI * n * phase)); return frac(phase + depth * Math.sin(TWO_PI * n * phase));
} }
case WarpMode.SPIN: { case WarpMode.SPIN: {
const depth = 0.5 * amt; const depth = 0.5 * amt;
const { n } = this._toBits(amt, 1, 6); const { n } = this._toBits(amt, 1, 6);
return frac(phase + depth * Math.sin(2 * Math.PI * n * phase)); return frac(phase + depth * Math.sin(TWO_PI * n * phase));
} }
case WarpMode.CHAOS: { case WarpMode.CHAOS: {
const r = 3.7 + 0.3 * amt; const r = 3.7 + 0.3 * amt;
@ -1169,8 +1169,8 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
case WarpMode.MODULAR: { case WarpMode.MODULAR: {
const { n } = this._toBits(amt); const { n } = this._toBits(amt);
const depth = 0.5 * amt; const depth = 0.5 * amt;
const jump = frac(phase * n) / n; const jump = ffrac(phase * n) / n;
return frac(phase + depth * jump); return ffrac(phase + depth * jump);
} }
case WarpMode.BROWNIAN: { case WarpMode.BROWNIAN: {
const disp = 0.25 * amt * brownian(64 * phase, 4); const disp = 0.25 * amt * brownian(64 * phase, 4);
@ -1207,7 +1207,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
return (y - y0) / (y1 - y0); return (y - y0) / (y1 - y0);
} }
case WarpMode.FRACTAL: { case WarpMode.FRACTAL: {
const d = 0.5 * Math.sin(2 * Math.PI * phase) * amt; const d = 0.5 * Math.sin(TWO_PI * phase) * amt;
return frac(phase + d); return frac(phase + d);
} }
case WarpMode.FLIP: { case WarpMode.FLIP: {
@ -1271,6 +1271,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
let f = pv(parameters.frequency, i); let f = pv(parameters.frequency, i);
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
const normalizer = 1 / Math.sqrt(voices); const normalizer = 1 / Math.sqrt(voices);
const detuner = getDetuner(voices, freqspread);
for (let n = 0; n < voices; n++) { for (let n = 0; n < voices; n++) {
const isOdd = (n & 1) == 1; const isOdd = (n & 1) == 1;
let gainL = gain1; let gainL = gain1;
@ -1280,7 +1281,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
gainL = gain2; gainL = gain2;
gainR = gain1; gainR = gain1;
} }
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune const fVoice = applySemitoneDetuneToFrequency(f, detuner(n)); // voice detune
const dPhase = fVoice * 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];
@ -1296,7 +1297,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
} }
outL[i] += s * gainL * normalizer; outL[i] += s * gainL * normalizer;
outR[i] += s * gainR * normalizer; outR[i] += s * gainR * normalizer;
this.phase[n] = wrapPhase(this.phase[n] + dPhase); this.phase[n] = ffrac(this.phase[n] + dPhase);
} }
} }
return true; return true;