More optimizations

This commit is contained in:
Aria
2025-11-22 15:00:50 -06:00
parent 352344aa91
commit af2ad9505d
3 changed files with 105 additions and 67 deletions
+3
View File
@@ -91,6 +91,9 @@ Fraction.prototype.or = function (other) {
};
const fraction = (n) => {
if (n instanceof Fraction) {
return n;
}
if (typeof n === 'number') {
/*
https://github.com/infusion/Fraction.js/#doubles
+6 -1
View File
@@ -447,11 +447,16 @@ export function applyFM(param, value, begin) {
// Saturation curves
const fastTanh = (x) => {
const x2 = x * x;
const y = (x * (27 + x2)) / (27 + 9 * x2);
return y < -1 ? -1 : (y > 1 ? 1 : y);
}
const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1)
const _mod = (n, m) => ((n % m) + m) % m;
const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x));
const _soft = (x, k) => Math.tanh(x * (1 + k));
const _soft = (x, k) => fastTanh(x * (1 + k));
const _hard = (x, k) => clamp((1 + k) * x, -1, 1);
const _fold = (x, k) => {
+96 -66
View File
@@ -10,6 +10,7 @@ const blockSize = 128;
const PI = Math.PI;
const TWO_PI = 2 * PI;
const INVSR = 1 / sampleRate;
const MAX_VOICES = 64;
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
const mod = (n, m) => ((n % m) + m) % m;
@@ -24,9 +25,10 @@ const fceil = (x) => ffloor(x + 1);
const ffrac = (x) => x - ffloor(x);
const fast_tanh = (x) => {
const x2 = x ** 2;
const x2 = x * x;
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
};
const fastexpm1 = (x) => x * (1 + x + 0.5 * x * x); // Taylor approximation
// Optimized per-voice detuner which precomputes constants
const getDetuner = (unison, detune) => {
@@ -169,10 +171,11 @@ class LFOProcessor extends AudioWorkletProcessor {
}
const dt = frequency * INVSR;
for (let n = 0; n < blockSize; n++) {
let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
modval = curve !== 1 ? Math.pow(modval, curve) : modval;
const out = clamp(modval, min, max);
for (let i = 0; i < output.length; i++) {
let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
modval = Math.pow(modval, curve);
output[i][n] = clamp(modval, min, max);
output[i][n] = out;
}
this.incrementPhase(dt);
}
@@ -443,7 +446,7 @@ class DistortProcessor extends AudioWorkletProcessor {
this.started = hasInput;
for (let n = 0; n < blockSize; n++) {
const postgain = clamp(pv(parameters.postgain, n), 0.001, 1);
const shape = Math.expm1(pv(parameters.distort, n));
const shape = fastexpm1(pv(parameters.distort, n));
for (let ch = 0; ch < input.length; ch++) {
const x = input[ch][n];
output[ch][n] = postgain * this.algorithm(x, shape);
@@ -458,7 +461,11 @@ registerProcessor('distort-processor', DistortProcessor);
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.phase = [];
this.phases = new Float32Array(MAX_VOICES);
this.reset(options);
}
reset(options) {
this.phases.map(() => Math.random());
}
static get parameterDescriptors() {
return [
@@ -481,7 +488,6 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
defaultValue: 440,
min: Number.EPSILON,
},
{
name: 'panspread',
defaultValue: 0.4,
@@ -502,6 +508,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
name: 'voices',
defaultValue: 5,
min: 1,
max: MAX_VOICES,
automationRate: 'k-rate',
},
{
@@ -511,42 +518,56 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
];
}
process(_input, outputs, params) {
const output = outputs[0];
const begin = params.begin[0];
const started = currentTime >= params.begin[0];
const end = params.end[0];
if (currentTime <= begin || currentTime >= end) {
output[0].fill(0);
output[1].fill(0);
const ended = currentTime >= end + 0.25;
const inGracePeriod = (currentTime >= end) && !ended;
if (started|| inGracePeriod) {
return true;
} else if (ended) {
return false;
}
const output = outputs[0];
const voices = params.voices[0]; // k-rate
for (let i = 0; i < output[0].length; i++) {
const detune = pv(params.detune, i);
const postgain = pv(params.postgain, i);
const freqspread = pv(params.freqspread, i);
const panspread = pv(params.panspread, i) * 0.5 + 0.5;
let gainL = Math.sqrt(1 - panspread) * postgain;
let gainR = Math.sqrt(panspread) * postgain;
let freq = pv(params.frequency, i);
// Main detuning
// fast k-rate paths
let freq, detune, freqspread, detuner;
if (params.freqspread.length === 1) {
freqspread = params.freqspread[0];
detuner = getDetuner(voices, freqspread);
}
if (params.frequency.length === 1 && params.detune.length === 1) {
freq = params.frequency[0];
detune = params.detune[0];
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
const detuner = getDetuner(voices, freqspread);
}
let panspread, gainL, gainR;
if (params.panspread.length === 1) {
panspread = voices > 1 ? clamp(pv(params.panspread, i), 0, 1) : 0;
gainL = Math.sqrt(0.5 - 0.5 * panspread);
gainR = Math.sqrt(0.5 + 0.5 * panspread);
}
for (let i = 0; i < output[0].length; i++) {
const postgain = pv(params.postgain, i);
const panspread = pv(params.panspread, i) * 0.5 + 0.5;
gainL = Math.sqrt(1 - panspread) * postgain;
gainR = Math.sqrt(panspread) * postgain;
// Main detuning (a-rate path)
detune ??= pv(params.detune, i);
freqspread ??= pv(params.freqspread, i);
freq ??= applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100);
detuner ??= getDetuner(voices, freqspread);
for (let n = 0; n < voices; n++) {
// Individual voice detuning
const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(n));
// We must wrap this here because it is passed into sawblep below which
// has domain [0, 1]
const dt = frac(freqVoice * INVSR);
this.phase[n] = this.phase[n] ?? Math.random();
const v = waveshapes.sawblep(this.phase[n], dt);
const v = waveshapes.sawblep(this.phases[n], dt);
output[0][i] += v * gainL;
output[1][i] += v * gainR;
let pn = this.phase[n] + dt;
if (pn >= 1.0) pn -= 1.0;
this.phase[n] = pn;
this.phases[n] = pn;
// invert right and left gain
gainL = gainR;
gainR = gainL;
@@ -1132,7 +1153,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
{ name: 'warpMode', defaultValue: 0 },
{ name: 'voices', defaultValue: 1, min: 1, automationRate: 'k-rate' },
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
{ name: 'postgain', defaultValue: 1, min: 0 },
];
}
@@ -1141,8 +1161,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
super(options);
this.frameLen = 0;
this.numFrames = 0;
this.phase = [];
this.phases = new Float32Array(MAX_VOICES);
this.port.onmessage = (e) => {
const { type, payload } = e.data || {};
if (type === 'table') {
@@ -1151,8 +1170,8 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
if (!tablesCache[key]) {
const tables = [payload.frames];
let table = tables[0];
for (let level = 1; level < 1; level++) {
const nextLen = table.length >> 1;
for (let level = 1; level < 6; level++) {
const nextLen = table[0].length >> 1;
const nextTable = table.map((frame) => {
const avg = new Float32Array(nextLen);
for (let i = 0; i < nextLen; i++) {
@@ -1170,6 +1189,11 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
this.numFrames = this.tables[0].length;
}
};
this.reset(options);
}
reset(options) {
this.phases.map(() => Math.random() * (options.processorOptions.phaseRand ?? 1));
}
_mirror(x) {
@@ -1323,57 +1347,60 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
return level;
}
_zeroOutputs(outputs) {
outputs[0][0].fill(0);
outputs[0][1].fill(0);
}
process(_inputs, outputs, parameters) {
const begin = parameters.begin[0];
const end = parameters.end[0];
if (!this.tables || currentTime <= begin || currentTime >= end) {
this._zeroOutputs(outputs);
process(_inputs, outputs, params) {
const started = currentTime >= params.begin[0];
const end = params.end[0];
const ended = currentTime >= end + 0.25;
const inGracePeriod = (currentTime >= end) && !ended;
if (!this.tables || started || inGracePeriod) {
return true;
} else if (ended) {
return false;
}
const outL = outputs[0][0];
const outR = outputs[0][1] || outputs[0][0];
const voices = parameters.voices[0]; // k-rate
const voicesDenom = 1 / Math.sqrt(voices);
// fast k-rate paths
let freq, detune, freqspread, detuner;
if (params.freqspread.length === 1) {
freqspread = params.freqspread[0];
detuner = getDetuner(voices, freqspread);
}
if (params.frequency.length === 1 && params.detune.length === 1) {
freq = params.frequency[0];
detune = params.detune[0];
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
}
let panspread, gainL, gainR;
if (params.panspread.length === 1) {
panspread = voices > 1 ? clamp(pv(params.panspread, i), 0, 1) : 0;
gainL = Math.sqrt(0.5 - 0.5 * panspread);
gainR = Math.sqrt(0.5 + 0.5 * panspread);
}
for (let i = 0; i < outL.length; i++) {
const detune = pv(parameters.detune, i);
const freqspread = pv(parameters.freqspread, i);
const tablePos = clamp(pv(parameters.position, i), 0, 1);
const tablePos = clamp(pv(params.position, i), 0, 1);
const idx = tablePos * (this.numFrames - 1);
const fIdx = idx | 0;
const interpT = idx - fIdx;
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
const warpMode = pv(parameters.warpMode, i);
const postgain = pv(parameters.postgain, i);
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
let f = pv(parameters.frequency, i);
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
const warpAmount = clamp(pv(params.warp, i), 0, 1);
const warpMode = pv(params.warpMode, i);
const postgain = pv(params.postgain, i);
const normalizer = postgain * voicesDenom;
const detuner = getDetuner(voices, freqspread);
panspread ??= voices > 1 ? clamp(pv(params.panspread, i), 0, 1) : 0;
gainL ??= Math.sqrt(0.5 - 0.5 * panspread);
gainR ??= Math.sqrt(0.5 + 0.5 * panspread);
detune ??= pv(params.detune, i);
freqspread ??= pv(params.freqspread, i);
freq ??= applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100); // overall detune
detuner ??= getDetuner(voices, freqspread);
for (let n = 0; n < voices; n++) {
const isOdd = (n & 1) == 1;
let gainL = gain1;
let gainR = gain2;
// invert right and left gain
if (isOdd) {
gainL = gain2;
gainR = gain1;
}
const fVoice = applySemitoneDetuneToFrequency(f, detuner(n)); // voice detune
const dPhase = fVoice * INVSR;
const level = this._chooseMip(dPhase);
const table = this.tables[level];
// warp phase then sample
this.phase[n] = this.phase[n] ?? Math.random() * phaseRand;
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
const s0 = this._sampleFrame(table[fIdx], ph);
const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
@@ -1384,6 +1411,9 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
outL[i] += s * gainL * normalizer;
outR[i] += s * gainR * normalizer;
this.phase[n] = frac(this.phase[n] + dPhase);
// invert right and left gain
gainL = gainR;
gainR = gainL;
}
}
return true;