diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 8a848fec0..426fd7479 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -468,7 +468,7 @@ export const { attack, att } = registerControl('attack', 'att'); * ._scope() * */ -Object.assign(globalThis, registerMultiControl(['fmh', 'fmi'], 8, 'fmh')); +export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerMultiControl(['fmh', 'fmi'], 8, 'fmh'); /** * Sets the Frequency Modulation of the synth. @@ -492,7 +492,8 @@ Object.assign(globalThis, registerMultiControl(['fmh', 'fmi'], 8, 'fmh')); * .fmh(1.06).fmh2(10).fmh3(0.1) * */ -Object.assign(globalThis, registerMultiControl(['fmi', 'fmh'], 8, 'fm')); +export const { fmi, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8 } = + registerMultiControl(['fmi', 'fmh'], 8, 'fm'); // fm envelope /** @@ -512,7 +513,10 @@ Object.assign(globalThis, registerMultiControl(['fmi', 'fmh'], 8, 'fm')); * ._scope() * */ -Object.assign(globalThis, registerMultiControl('fmenv', 8)); +export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fmenv8 } = registerMultiControl( + 'fmenv', + 8, +); /** * Attack time for the FM envelope: time it takes to reach maximum modulation @@ -530,7 +534,26 @@ Object.assign(globalThis, registerMultiControl('fmenv', 8)); * ._scope() * */ -Object.assign(globalThis, registerMultiControl('fmattack', 8, 'fmatt')); +export const { + fmattack, + fmattack1, + fmattack2, + fmattack3, + fmattack4, + fmattack5, + fmattack6, + fmattack7, + fmattack8, + fmatt, + fmatt1, + fmatt2, + fmatt3, + fmatt4, + fmatt5, + fmatt6, + fmatt7, + fmatt8, +} = registerMultiControl('fmattack', 8, 'fmatt'); /** * Waveform of the fm modulator @@ -546,7 +569,10 @@ Object.assign(globalThis, registerMultiControl('fmattack', 8, 'fmatt')); * n("0 1 2 3".fast(4)).chord("").voicing().s("sawtooth").fmwave("brown").fm(.6) * */ -Object.assign(globalThis, registerMultiControl('fmwave', 8)); +export const { fmwave, fmwave1, fmwave2, fmwave3, fmwave4, fmwave5, fmwave6, fmwave7, fmwave8 } = registerMultiControl( + 'fmwave', + 8, +); /** * Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase. @@ -565,7 +591,26 @@ Object.assign(globalThis, registerMultiControl('fmwave', 8)); * ._scope() * */ -Object.assign(globalThis, registerMultiControl('fmdecay', 8, 'fmdec')); +export const { + fmdecay, + fmdecay1, + fmdecay2, + fmdecay3, + fmdecay4, + fmdecay5, + fmdecay6, + fmdecay7, + fmdecay8, + fmdec, + fmdec1, + fmdec2, + fmdec3, + fmdec4, + fmdec5, + fmdec6, + fmdec7, + fmdec8, +} = registerMultiControl('fmdecay', 8, 'fmdec'); /** * Sustain level for the FM envelope: how much modulation is applied after the decay phase @@ -584,7 +629,26 @@ Object.assign(globalThis, registerMultiControl('fmdecay', 8, 'fmdec')); * ._scope() * */ -Object.assign(globalThis, registerMultiControl('fmsustain', 8, 'fmsus')); +export const { + fmsustain, + fmsustain1, + fmsustain2, + fmsustain3, + fmsustain4, + fmsustain5, + fmsustain6, + fmsustain7, + fmsustain8, + fmsus, + fmsus1, + fmsus2, + fmsus3, + fmsus4, + fmsus5, + fmsus6, + fmsus7, + fmsus8, +} = registerMultiControl('fmsustain', 8, 'fmsus'); /** * Release time for the FM envelope: how much modulation is applied after the note is released @@ -597,12 +661,35 @@ Object.assign(globalThis, registerMultiControl('fmsustain', 8, 'fmsus')); * @param {number | Pattern} time release time * */ -Object.assign(globalThis, registerMultiControl('fmrelease', 8, 'fmrel')); +export const { + fmrelease, + fmrelease1, + fmrelease2, + fmrelease3, + fmrelease4, + fmrelease5, + fmrelease6, + fmrelease7, + fmrelease8, + fmrel, + fmrel1, + fmrel2, + fmrel3, + fmrel4, + fmrel5, + fmrel6, + fmrel7, + fmrel8, +} = registerMultiControl('fmrelease', 8, 'fmrel'); // FM Matrix +// Note: we do not declare top-level exports here since it would add +// ~162 more explicit exports. This is likely fine as the most common use-case would be to at least +// declare one other FM prior to utilizing the matrix functionality, but if we ever decide we need it, +// TODO to add it explicitly / go with the globalThis approach for (let i = 0; i <= 8; i++) { for (let j = 0; j <= 8; j++) { - Object.assign(globalThis, registerControl(`fmi${i}${j}`, `fm${i}${j}`)); + registerControl(`fmi${i}${j}`, `fm${i}${j}`); } } diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 073ed00bd..2080d32ed 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -372,13 +372,13 @@ const mod = (freq, type = 'sine') => { osc.frequency.value = freq; } osc.start(); - return { osc, freq }; + return osc; }; const fm = (frequencyparam, harmonicityRatio, wave = 'sine') => { const carrfreq = frequencyparam.value; const modfreq = carrfreq * harmonicityRatio; - return mod(modfreq, wave); + return { osc: mod(modfreq, wave), freq: modfreq }; }; export function applyFM(param, value, begin) { @@ -434,13 +434,12 @@ export function applyFM(param, value, begin) { toCleanup.push(envGain); output = osc.connect(envGain); } - fms[idx] = { input: osc.frequency, output, freq, toCleanup }; - osc.onended = () => cleanupNodes(fms[idx].toCleanup); + fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup }; } - const { input, output, freq, toCleanup } = fms[idx]; + const { input, output, freq, osc, toCleanup } = fms[idx]; const g = gainNode(amt * freq); io.push(isMod ? output.connect(g) : input); - toCleanup.push(g); + cleanupOnEnd(osc, [...toCleanup, g]); } if (!io[1]) { logger( @@ -451,11 +450,6 @@ export function applyFM(param, value, begin) { } io[0].connect(io[1]); } - fmmod.osc.onended = () => { - envGain.disconnect(); - modulator.disconnect(); - fmmod.osc.disconnect(); - }; } return { stop: (t) => toStop.forEach((m) => m?.stop(t)), @@ -575,10 +569,9 @@ export const getFrequencyFromValue = (value, defaultNote = 36) => { // This helper should be used instead of the `node.onended = callback` pattern // It adds a mechanism to help minimize gc retention export const onceEnded = (node, callback) => { - let onended = callback; + const onended = callback; node.onended = function cleanup() { onended && onended(); - onended = null; this.onended = null; }; }; @@ -624,13 +617,10 @@ export const releaseAudioNode = (node) => { } }; -export const cleanupNode = (node, time) => { - if (node == null) return; - node.disconnect?.(); - node.parameters?.get('end')?.setValueAtTime(0, 0); - node.stop?.(time); -}; - -export const cleanupNodes = (nodes, time) => { - nodes.forEach((n) => cleanupNode(n, time)); +// Once the `anchor` node has ended, release all nodes in `toCleanup` +export const cleanupOnEnd = (anchor, toCleanup) => { + onceEnded( + anchor, + () => toCleanup.forEach((n) => releaseAudioNode(n)), + ); };