From 835659213e0ffc09bc9cad1c52d92649f5933186 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 22 Nov 2025 20:29:05 -0600 Subject: [PATCH] Resets and folding things into processorOptions --- packages/codemirror/codemirror.mjs | 7 +++ packages/superdough/helpers.mjs | 22 ++++---- packages/superdough/synth.mjs | 17 +++++- packages/superdough/wavetable.mjs | 23 ++++++-- packages/superdough/worklets.mjs | 84 ++++++++++++++++-------------- 5 files changed, 98 insertions(+), 55 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 69fce4b50..e91e6f259 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -237,6 +237,7 @@ export class StrudelMirror { cmEditor.style.backgroundColor = 'transparent'; } const settings = codemirrorSettings.get(); + this.isPatternHighlightingEnabled = parseBooleans(settings.isPatternHighlightingEnabled); this.setFontSize(settings.fontSize); this.setFontFamily(settings.fontFamily); @@ -321,6 +322,9 @@ export class StrudelMirror { flash(this.editor, ms); } highlight(haps, time) { + if (!this.isPatternHighlightingEnabled) { + return; + } highlightMiniLocations(this.editor, time, haps); } setFontSize(size) { @@ -339,6 +343,9 @@ export class StrudelMirror { return; } value = parseBooleans(value); + if (key === 'isPatternHighlightingEnabled') { + this.isPatternHighlightingEnabled = value; + } const newValue = extensions[key](value, this); this.editor.dispatch({ effects: compartments[key].reconfigure(newValue), diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index c7ac893f7..be2327e1a 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -41,21 +41,25 @@ const voicePools = {}; // Cycles through the pool attempting to find an inactive voice that hasn't been GC'd export const claimVoice = (key) => { - voicePools[key] ??= []; + voicePools[key] ??= {}; const pool = voicePools[key]; let node; - while (pool.length && !node) { - const ref = pool.pop(); + for (const [id, ref] of Object.entries(pool)) { node = ref?.deref?.(); + delete pool[id]; + if (node !== null) break; } - node?.reset(); return node; }; // Releases a voice from use and adds to the inactive pool -export const releaseVoice = (key, node) => { - voicePools[key] ??= []; - voicePools[key].push(new WeakRef(node)); +export const releaseVoice = (key, id, node) => { + voicePools[key] ??= {}; + voicePools[key][id] = new WeakRef(node); +}; + +export const removeVoice = (key, id) => { + delete voicePools[key][id]; }; export const getParamADSR = ( @@ -451,8 +455,8 @@ export function applyFM(param, value, begin) { const fastTanh = (x) => { const x2 = x * x; const y = (x * (27 + x2)) / (27 + 9 * x2); - return y < -1 ? -1 : (y > 1 ? 1 : y); -} + 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; diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 1d2e26c27..4014a8e08 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -15,6 +15,7 @@ import { getWorklet, setParams, releaseVoice, + removeVoice, noises, webAudioTimeout, } from './helpers.mjs'; @@ -173,15 +174,21 @@ export function registerSynthSounds() { begin, end, freqspread: detune, - voices, panspread, }; + const processorOptions = { + voices, + }; if (o == null) { + console.log('Building'); o = getWorklet(ac, 'supersaw-oscillator', params, { outputChannelCount: [2], + processorOptions, }); } else { + console.log('Reusing'); setParams(o, params); + o.port.postMessage({ type: 'reset', payload: { processorOptions } }); } const gainAdjustment = 1 / Math.sqrt(voices); getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); @@ -202,8 +209,14 @@ export function registerSynthSounds() { const timeoutNode = webAudioTimeout( ac, () => { + const id = Math.floor(Math.random() * 10000); destroyAudioWorkletNode(o); - releaseVoice('supersaw', o); + releaseVoice('supersaw', id, o); + o.port.onmessage = (e) => { + if (e.data.type === 'finished') { + removeVoice('supersaw', id); + } + }; onended(); fm?.stop(); vibratoOscillator?.stop(); diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 3bfa4ac4d..480453f7f 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -12,6 +12,7 @@ import { getWorklet, setParams, releaseVoice, + removeVoice, webAudioTimeout, destroyAudioWorkletNode, } from './helpers.mjs'; @@ -237,20 +238,28 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { position: value.wt, warp: value.warp, warpMode: warpmode, - voices: Math.max(value.unison ?? 1, 1), panspread: value.spread, - phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0, + }; + const processorOptions = { + phaseRand: value.wtphaserand ?? (value.unison > 1 ? 1 : 0), + voices: Math.max(value.unison ?? 1, 1), }; if (source == null) { - source = getWorklet(ac, 'wavetable-oscillator-processor', params, { outputChannelCount: [2] }); + source = getWorklet(ac, 'wavetable-oscillator-processor', params, { outputChannelCount: [2], processorOptions }); } else { setParams(source, params); + source.port.postMessage({ type: 'reset', payload: { processorOptions } }); } source.port.postMessage({ type: 'table', payload }); if (ac.currentTime > t) { logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight'); destroyAudioWorkletNode(source); releaseVoice(voiceKey, source); + source.port.onmessage = (e) => { + if (e.data.type === 'finished') { + removeVoice(voiceKey, id); + } + }; return; } const posADSRParams = [value.wtattack, value.wtdecay, value.wtsustain, value.wtrelease]; @@ -328,7 +337,13 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { wtPosModulators?.disconnect(); wtWarpModulators?.disconnect(); destroyAudioWorkletNode(source); - releaseVoice(voiceKey, source); + const id = Math.floor(Math.random() * 10000); + releaseVoice(voiceKey, id, source); + source.port.onmessage = (e) => { + if (e.data.type === 'finished') { + removeVoice(voiceKey, id); + } + }; onended(); }, t, diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index fba52a554..d5ba329ba 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -470,13 +470,17 @@ registerProcessor('distort-processor', DistortProcessor); // SUPERSAW class SuperSawOscillatorProcessor extends AudioWorkletProcessor { - constructor() { + constructor(options) { super(); this.phases = new Float32Array(MAX_VOICES); - this.reset(options); + this.reset(options.processorOptions); } reset(options) { - this.phases.map(() => Math.random()); + const phases = this.phases; + for (let i = 0; i < phases.length; i++) { + phases[i] = Math.random(); + } + this.voices = options.voices; } static get parameterDescriptors() { return [ @@ -513,13 +517,6 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { defaultValue: 0, min: 0, }, - { - name: 'voices', - defaultValue: 5, - min: 1, - max: MAX_VOICES, - automationRate: 'k-rate', - }, { name: 'postgain', defaultValue: 1, @@ -529,20 +526,20 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { process(_input, outputs, params) { const started = currentTime >= params.begin[0]; const end = params.end[0]; - const ended = currentTime >= end + 1; - const inGracePeriod = (currentTime >= end) && !ended; - if (started|| inGracePeriod) { + const ended = currentTime >= end + 5; + const inGracePeriod = currentTime >= end && !ended; + if (!started || inGracePeriod) { return true; } else if (ended) { + this.port.postMessage({ type: 'finished' }); return false; } const output = outputs[0]; - const voices = params.voices[0]; // k-rate // fast k-rate paths let freq, detune, freqspread, detuner; if (params.freqspread.length === 1) { freqspread = params.freqspread[0]; - detuner = getDetuner(voices, freqspread); + detuner = getDetuner(this.voices, freqspread); } if (params.frequency.length === 1 && params.detune.length === 1) { freq = params.frequency[0]; @@ -551,7 +548,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { } let panspread, gainL, gainR; if (params.panspread.length === 1) { - panspread = voices > 1 ? clamp(pv(params.panspread, i), 0, 1) : 0; + panspread = this.voices > 1 ? clamp(params.panspread[0], 0, 1) : 0; gainL = Math.sqrt(0.5 - 0.5 * panspread); gainR = Math.sqrt(0.5 + 0.5 * panspread); } @@ -564,8 +561,8 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { 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++) { + detuner ??= getDetuner(this.voices, freqspread); + for (let n = 0; n < this.voices; n++) { // Individual voice detuning const freqVoice = applyFastDetune(freq, detuner(n)); // We must wrap this here because it is passed into sawblep below which @@ -574,12 +571,13 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { const v = waveshapes.sawblep(this.phases[n], dt); output[0][i] += v * gainL; output[1][i] += v * gainR; - let pn = this.phase[n] + dt; + let pn = this.phases[n] + dt; if (pn >= 1.0) pn -= 1.0; this.phases[n] = pn; // invert right and left gain + const tmp = gainL; gainL = gainR; - gainR = gainL; + gainR = tmp; } } return true; @@ -1160,7 +1158,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { { name: 'position', defaultValue: 0, min: 0, max: 1 }, { name: 'warp', defaultValue: 0, min: 0, max: 1 }, { name: 'warpMode', defaultValue: 0 }, - { name: 'voices', defaultValue: 1, min: 1, automationRate: 'k-rate' }, { name: 'panspread', defaultValue: 0.7, min: 0, max: 1 }, { name: 'postgain', defaultValue: 1, min: 0 }, ]; @@ -1196,13 +1193,21 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } this.tables = tablesCache[key]; this.numFrames = this.tables[0].length; + } else if (type === 'reset') { + this.reset(payload.processorOptions); } }; - this.reset(options); + this.reset(options.processorOptions); } reset(options) { - this.phases.map(() => Math.random() * (options.processorOptions.phaseRand ?? 1)); + const phaseRand = options.phaseRand ?? 1; + const phases = this.phases; + for (let i = 0; i < phases.length; i++) { + phases[i] = Math.random() * phaseRand; + } + this.voices = options.voices; + this.voicesDenom = 1 / Math.sqrt(this.voices); } _mirror(x) { @@ -1359,22 +1364,20 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { process(_inputs, outputs, params) { const started = currentTime >= params.begin[0]; const end = params.end[0]; - const ended = currentTime >= end + 1; - const inGracePeriod = (currentTime >= end) && !ended; - if (!this.tables || started || inGracePeriod) { + const ended = currentTime >= end + 5; + 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); + detuner = getDetuner(this.voices, freqspread); } if (params.frequency.length === 1 && params.detune.length === 1) { freq = params.frequency[0]; @@ -1383,7 +1386,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } let panspread, gainL, gainR; if (params.panspread.length === 1) { - panspread = voices > 1 ? clamp(pv(params.panspread, i), 0, 1) : 0; + panspread = this.voices > 1 ? clamp(params.panspread[0], 0, 1) : 0; gainL = Math.sqrt(0.5 - 0.5 * panspread); gainR = Math.sqrt(0.5 + 0.5 * panspread); } @@ -1395,34 +1398,35 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { 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; - panspread ??= voices > 1 ? clamp(pv(params.panspread, i), 0, 1) : 0; + const normalizer = postgain * this.voicesDenom; + panspread ??= this.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 fVoice = applyFastDetune(f, detuner(n)); // voice detune - const dPhase = fVoice * INVSR; + detuner ??= getDetuner(this.voices, freqspread); + for (let n = 0; n < this.voices; n++) { + const freqVoice = applyFastDetune(freq, detuner(n)); // voice detune + const dPhase = freqVoice * INVSR; const level = this._chooseMip(dPhase); const table = this.tables[level]; // warp phase then sample - const ph = this._warpPhase(this.phase[n], warpAmount, warpMode); + const ph = this._warpPhase(this.phases[n], warpAmount, warpMode); const s0 = this._sampleFrame(table[fIdx], ph); const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph); let s = lerp(s0, s1, interpT); - if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) { + if (warpMode === WarpMode.FLIP && this.phases[n] < warpAmount) { s = -s; } outL[i] += s * gainL * normalizer; outR[i] += s * gainR * normalizer; - this.phase[n] = frac(this.phase[n] + dPhase); + this.phases[n] = frac(this.phases[n] + dPhase); // invert right and left gain - gainL = gainR; + const tmp = gainL; gainR = gainL; + gainL = gainR; } } return true;