From 763f900c6978b82b1aef3c38accc6cd6f0cece66 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 27 Aug 2025 19:19:54 -0500 Subject: [PATCH 1/5] First pass at envelopes with curves --- packages/superdough/worklets.mjs | 94 ++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 2406d56dc..61a93ce7b 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -907,3 +907,97 @@ class ByteBeatProcessor extends AudioWorkletProcessor { } registerProcessor('byte-beat-processor', ByteBeatProcessor); + +class EnvelopeProcessor extends AudioWorkletProcessor { + static get parameterDescriptors() { + return [ + { name: 'start', defaultValue: 0 }, + { name: 'end', defaultValue: 0 }, + { name: 'attack', defaultValue: 0.005, minValue: 0 }, + { name: 'decay', defaultValue: 0.14, minValue: 0 }, + { name: 'sustain', defaultValue: 0, minValue: 0, maxValue: 1 }, + { name: 'release', defaultValue: 0.1, minValue: 0 }, + { name: 'attackCurve', defaultValue: 0, minValue: -1, maxValue: 1 }, + { name: 'decayCurve', defaultValue: 0, minValue: -1, maxValue: 1 }, + { name: 'releaseCurve', defaultValue: 0, minValue: -1, maxValue: 1 }, + { name: 'peak', defaultValue: 1 }, + { name: 'retrigger', defaultValue: 1, minValue: 0, maxValue: 1 }, + ]; + } + + constructor() { + super(); + this.val = 0; + this.segIdx = 0; + this.state = 0; + this.startTime = 0; + this.endTime = 0; + } + + _shape(u, c) { + if (c === 0) return u; + const k = 4; + if (c > 0) { + const p = 1 + k * c; + return 1 - Math.pow(1 - u, p); + } else { + const p = 1 + k * (-c); + return Math.pow(u, p); + } + } + + _advance(start, target, length, curve) { + if (length <= 1 || start === target) { + this.val = target; + } else { + const u = Math.min(1, this.segIdx / (length - 1)); + const us = this._shape(u, curve); + this.val = start + (target - start) * us; + } + this.segIdx++; + } + + process(_inputs, outputs, params) { + const out = outputs[0][0]; + if (!out) return true; + const start = pv(params.start, 0); + this.endTime = pv(params.end, 0); + const retrig = pv(params.retrig, 0) >= 0.5; // convert to bool + if (start !== this.startTime) { + // triggered + this.startTime = start; + if (this.state === 0 || retrig) { + this.state = 1; + this._resetSegment(this.env, pv(params.peak, 0), pv(params.attack, 0)); + } + } + for (let i = 0; i < out.length; i++) { + const attack = pv(params.attack, i); + const decay = pv(params.decay, i); + const sustain = pv(params.sustain, i); + const release = pv(params.release, i); + const aCurve = pv(params.aCurve, i); + const dCurve = pv(params.dCurve, i); + const rCurve = pv(params.rCurve, i); + const peak = pv(params.peak, i); + const states = [ + { time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle + { time: attack, start: 0, target: peak, curve: aCurve }, + { time: decay, start: peak, target: sustain * peak, curve: dCurve }, + { time: this.endTime - this.startTime - attack - decay }, + { time: release, start: sustain * peak, target: 0, curve: rCurve }, + ] + const {time, start, target, curve} = states[this.state]; + const length = Math.floor(time * sampleRate); + this._advance(start, target, length, curve); + if (this.segIdx >= length) { + this.state = (this.state + 1) % states.length; + } + out[i] = this.val * peak; + } + + return true; + } +} + +registerProcessor('envelope-processor', EnvelopeProcessor); From ea017e762228ca81ca5099e9b72e03a828aafe0c Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 27 Aug 2025 20:11:00 -0500 Subject: [PATCH 2/5] Working version --- packages/superdough/worklets.mjs | 34 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 61a93ce7b..0885f5fe2 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -7,6 +7,7 @@ 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; +const pv = (arr, idx) => arr[idx] ?? arr[0]; // Restrict phase to the range [0, maxPhase) via wrapping function wrapPhase(phase, maxPhase = 1) { @@ -911,7 +912,7 @@ registerProcessor('byte-beat-processor', ByteBeatProcessor); class EnvelopeProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [ - { name: 'start', defaultValue: 0 }, + { name: 'begin', defaultValue: 0 }, { name: 'end', defaultValue: 0 }, { name: 'attack', defaultValue: 0.005, minValue: 0 }, { name: 'decay', defaultValue: 0.14, minValue: 0 }, @@ -930,13 +931,13 @@ class EnvelopeProcessor extends AudioWorkletProcessor { this.val = 0; this.segIdx = 0; this.state = 0; - this.startTime = 0; + this.beginTime = 0; this.endTime = 0; } _shape(u, c) { if (c === 0) return u; - const k = 4; + const k = 8; if (c > 0) { const p = 1 + k * c; return 1 - Math.pow(1 - u, p); @@ -960,38 +961,37 @@ class EnvelopeProcessor extends AudioWorkletProcessor { process(_inputs, outputs, params) { const out = outputs[0][0]; if (!out) return true; - const start = pv(params.start, 0); + const begin = pv(params.begin, 0); this.endTime = pv(params.end, 0); - const retrig = pv(params.retrig, 0) >= 0.5; // convert to bool - if (start !== this.startTime) { + const retrigger = pv(params.retrigger, 0) >= 0.5; // convert to bool + if (begin !== this.beginTime && (this.state === 0 || retrigger)) { // triggered - this.startTime = start; - if (this.state === 0 || retrig) { - this.state = 1; - this._resetSegment(this.env, pv(params.peak, 0), pv(params.attack, 0)); - } + this.beginTime = begin; + this.state = 1; + this.segIdx = 0; } for (let i = 0; i < out.length; i++) { const attack = pv(params.attack, i); const decay = pv(params.decay, i); const sustain = pv(params.sustain, i); const release = pv(params.release, i); - const aCurve = pv(params.aCurve, i); - const dCurve = pv(params.dCurve, i); - const rCurve = pv(params.rCurve, i); + const aCurve = pv(params.attackCurve, i); + const dCurve = pv(params.decayCurve, i); + const rCurve = pv(params.releaseCurve, i); const peak = pv(params.peak, i); const states = [ { time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle { time: attack, start: 0, target: peak, curve: aCurve }, - { time: decay, start: peak, target: sustain * peak, curve: dCurve }, - { time: this.endTime - this.startTime - attack - decay }, - { time: release, start: sustain * peak, target: 0, curve: rCurve }, + { time: decay, start: 1, target: sustain, curve: dCurve }, + { time: this.endTime - this.beginTime - attack - decay, start: sustain, target: sustain }, + { time: release, start: sustain, target: 0, curve: rCurve }, ] const {time, start, target, curve} = states[this.state]; const length = Math.floor(time * sampleRate); this._advance(start, target, length, curve); if (this.segIdx >= length) { this.state = (this.state + 1) % states.length; + this.segIdx = 0; } out[i] = this.val * peak; } From 869141b0a3c0021b84919e5d10bf1a76cf8c36d7 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 28 Aug 2025 00:31:37 -0500 Subject: [PATCH 3/5] Parity with old adsr --- packages/superdough/worklets.mjs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 0885f5fe2..494bd4dae 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -933,6 +933,7 @@ class EnvelopeProcessor extends AudioWorkletProcessor { this.state = 0; this.beginTime = 0; this.endTime = 0; + this.attackStart = 0; } _shape(u, c) { @@ -947,29 +948,29 @@ class EnvelopeProcessor extends AudioWorkletProcessor { } } - _advance(start, target, length, curve) { - if (length <= 1 || start === target) { + _advance(start, target, time, curve) { + if (time === 0 || start === target) { this.val = target; } else { - const u = Math.min(1, this.segIdx / (length - 1)); + const u = Math.min(1, (currentTime - this.beginTime) / time); const us = this._shape(u, curve); this.val = start + (target - start) * us; } - this.segIdx++; } process(_inputs, outputs, params) { const out = outputs[0][0]; if (!out) return true; const begin = pv(params.begin, 0); - this.endTime = pv(params.end, 0); const retrigger = pv(params.retrigger, 0) >= 0.5; // convert to bool if (begin !== this.beginTime && (this.state === 0 || retrigger)) { // triggered this.beginTime = begin; this.state = 1; - this.segIdx = 0; + this.endTime = pv(params.end, 0); + this.attackStart = this.val; } + const susTime = this.endTime - this.beginTime; for (let i = 0; i < out.length; i++) { const attack = pv(params.attack, i); const decay = pv(params.decay, i); @@ -981,17 +982,16 @@ class EnvelopeProcessor extends AudioWorkletProcessor { const peak = pv(params.peak, i); const states = [ { time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle - { time: attack, start: 0, target: peak, curve: aCurve }, - { time: decay, start: 1, target: sustain, curve: dCurve }, - { time: this.endTime - this.beginTime - attack - decay, start: sustain, target: sustain }, - { time: release, start: sustain, target: 0, curve: rCurve }, + { time: attack, start: this.attackStart, target: 1, curve: aCurve }, + { time: attack + decay, start: 1, target: sustain, curve: dCurve }, + { time: susTime, start: sustain, target: sustain }, + { time: susTime + release, start: sustain, target: 0, curve: rCurve }, ] - const {time, start, target, curve} = states[this.state]; - const length = Math.floor(time * sampleRate); - this._advance(start, target, length, curve); - if (this.segIdx >= length) { + let {time, start, target, curve} = states[this.state]; + this._advance(start, target, time, curve); + while (currentTime - this.beginTime >= time) { this.state = (this.state + 1) % states.length; - this.segIdx = 0; + time = states[this.state].time; } out[i] = this.val * peak; } From 9eeb6da3e60957a67f18f368e2777d35cb2f9887 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 28 Aug 2025 14:54:09 -0500 Subject: [PATCH 4/5] Codeformat --- packages/superdough/worklets.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 494bd4dae..619855e31 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -922,7 +922,7 @@ class EnvelopeProcessor extends AudioWorkletProcessor { { name: 'decayCurve', defaultValue: 0, minValue: -1, maxValue: 1 }, { name: 'releaseCurve', defaultValue: 0, minValue: -1, maxValue: 1 }, { name: 'peak', defaultValue: 1 }, - { name: 'retrigger', defaultValue: 1, minValue: 0, maxValue: 1 }, + { name: 'retrigger', defaultValue: 1, minValue: 0, maxValue: 1 }, ]; } @@ -943,7 +943,7 @@ class EnvelopeProcessor extends AudioWorkletProcessor { const p = 1 + k * c; return 1 - Math.pow(1 - u, p); } else { - const p = 1 + k * (-c); + const p = 1 + k * -c; return Math.pow(u, p); } } @@ -986,8 +986,8 @@ class EnvelopeProcessor extends AudioWorkletProcessor { { time: attack + decay, start: 1, target: sustain, curve: dCurve }, { time: susTime, start: sustain, target: sustain }, { time: susTime + release, start: sustain, target: 0, curve: rCurve }, - ] - let {time, start, target, curve} = states[this.state]; + ]; + let { time, start, target, curve } = states[this.state]; this._advance(start, target, time, curve); while (currentTime - this.beginTime >= time) { this.state = (this.state + 1) % states.length; From e4070b86f8378bdf2f72fe2fa7a4c036a7786e3a Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 29 Aug 2025 14:33:14 -0500 Subject: [PATCH 5/5] More descriptive variables and comments --- packages/superdough/worklets.mjs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 619855e31..6c66d2350 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -936,25 +936,29 @@ class EnvelopeProcessor extends AudioWorkletProcessor { this.attackStart = 0; } - _shape(u, c) { - if (c === 0) return u; - const k = 8; - if (c > 0) { - const p = 1 + k * c; - return 1 - Math.pow(1 - u, p); + _warp(phase, curvature, strength = 8) { + if (phase === 0 || phase === 1) return phase; // fast exit + if (curvature > 0) { + // snappier + const exp = 1 + strength * curvature; + return 1 - Math.pow(1 - phase, exp); } else { - const p = 1 + k * -c; - return Math.pow(u, p); + // more calm + const exp = 1 - strength * curvature; + return Math.pow(phase, exp); } } - _advance(start, target, time, curve) { + _advance(start, target, time, curvature) { if (time === 0 || start === target) { this.val = target; } else { - const u = Math.min(1, (currentTime - this.beginTime) / time); - const us = this._shape(u, curve); - this.val = start + (target - start) * us; + // We compute our progress through this section of the envelope in time + // as a `phase` value, which is warped by the curvature, and then used + // to compute the value of the envelope at that time + const phase = Math.min(1, (currentTime - this.beginTime) / time); + const phaseWarped = this._warp(phase, curvature); + this.val = start + (target - start) * phaseWarped; } }