diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 809a45ab2..80a244efc 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2584,3 +2584,5 @@ export const scrub = register( }, false, ); + +export const { transient } = registerControl(['transient', 'transsustain', 'transattack']); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index bf633a63b..334e2d829 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -11,6 +11,9 @@ const PI = Math.PI; const TWO_PI = 2 * PI; const INVSR = 1 / sampleRate; +const timeToCoeff = (t) => Math.exp(-INVSR / t); +const dbToLin = (db) => Math.pow(10, db / 20); + const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const mod = (n, m) => ((n % m) + m) % m; const lerp = (a, b, n) => n * (b - a) + a; @@ -1383,3 +1386,70 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor); + +class TransientProcessor extends AudioWorkletProcessor { + static get parameterDescriptors() { + return []; + } + + constructor(options) { + super(); + this.gainCoeff = timeToCoeff(0.2); + this.avgGain = 1; + let { + attackTime = 0.003, + sustainTime = 0.08, + attack = 0, + sustain = 0, + sensitivity = 0.1, + mix = 1, + } = options.processorOptions; + attackTime = clamp(attackTime, 0.0005, 0.05); + sustainTime = clamp(sustainTime, 0.01, 0.5); + this.attackCoeff = timeToCoeff(attackTime); + this.sustainCoeff = timeToCoeff(sustainTime); + this.attackAmt = clamp(attack, -1, 1); + this.sustainAmt = clamp(sustain, -1, 1); + this.scaling = 0.5 + 5 * clamp(sensitivity, 0, 1); + this.mix = clamp(mix, 0, 1); + } + + process(inputs, outputs, _params) { + const input = inputs[0]; + const output = outputs[0]; + if (!input || !output) { + return true; + } + const channels = output.length; + this.attackEnv ??= new Float32Array(channels); + this.sustainEnv ??= new Float32Array(channels); + let avgGain = this.avgGain; + for (let ch = 0; ch < channels; ch++) { + const inCh = input[ch]; + const outCh = output[ch]; + let attEnv = this.attackEnv[ch]; + let susEnv = this.sustainEnv[ch]; + for (let i = 0; i < blockSize; i++) { + const x = Math.abs(inCh[i]); + attEnv = lerp(attEnv, x, this.attackCoeff); + susEnv = lerp(susEnv, x, this.sustainCoeff); + const peakiness = clamp((this.scaling * (attEnv - susEnv)) / (susEnv + 1e-6), -1.5, 1.5); + const attScale = Math.max(0, peakiness); + const susScale = Math.max(0, -peakiness); + const attackGain = dbToLin(this.attackAmt * attScale * 18); // max 18db + const sustainGain = dbToLin(this.sustainAmt * susScale * 36); // max 36db + const gain = clamp(attackGain * sustainGain, 0, 8); + avgGain = lerp(avgGain, gain, this.gainCoeff); + const makeup = avgGain > 1e-3 ? 1 / avgGain : 1; + const wet = x * gain * makeup; + outCh[i] = lerp(x, wet, this.mix); + } + this.attackEnv[ch] = attEnv; + this.sustainEnv[ch] = susEnv; + } + this.avgGain = avgGain; + return true; + } +} + +registerProcessor('transient-processor', TransientProcessor);