From c9381a11f9ba182ac4ae8e5aab3584883003125c Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 3 Oct 2025 17:21:05 -0500 Subject: [PATCH 01/45] Working version --- packages/core/controls.mjs | 144 +++++++++++++++++++++++++++++--- packages/superdough/helpers.mjs | 124 +++++++++++++++++---------- 2 files changed, 213 insertions(+), 55 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 7e3c2a8e2..a2136367d 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -72,6 +72,27 @@ export function registerControl(names, ...aliases) { return bag; } +export function registerMultiControl(names, maxControls, ...aliases) { + names = Array.isArray(names) ? names : [names]; + let bag = {}; + for (let i = 1; i <= maxControls; i++) { + let theseAliases = [...aliases]; + let theseNames = [...names]; + if (i === 1) { + // adds e.g. fm1 as an alias for fm + const aliases1 = theseAliases.map((a) => `${a}1`); + const names1 = theseNames.map((n) => `${n}1`); + theseAliases = theseAliases.concat(aliases1).concat(names1); + } else { + theseAliases = theseAliases.map((a) => `${a}${i}`); + theseNames = theseNames.map((n) => `${n}${i}`); + } + const subBag = registerControl(theseNames, ...theseAliases); + bag = { ...bag, ...subBag }; + } + return bag; +} + /** * Select a sound / sample by name. When using mininotation, you can also optionally supply 'n' and 'gain' parameters * separated by ':'. @@ -444,21 +465,22 @@ export const { attack, att } = registerControl('attack', 'att'); * ._scope() * */ -export const { fmh } = registerControl(['fmh', 'fmi'], 'fmh'); +export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerMultiControl(['fmh', 'fmi'], 8, 'fmh'); /** * Sets the Frequency Modulation of the synth. * Controls the modulation index, which defines the brightness of the sound. * - * @name fm + * @name fmi * @param {number | Pattern} brightness modulation index - * @synonyms fmi + * @synonyms fm * @example * note("c e g b g e") * .fm("<0 1 2 8 32>") * ._scope() * */ -export const { fmi, fm } = registerControl(['fmi', 'fmh'], '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 /** * Ramp type of fm envelope. Exp might be a bit broken.. @@ -474,11 +496,15 @@ export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm'); * ._scope() * */ -export const { fmenv } = registerControl('fmenv'); +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 * * @name fmattack + * @synonyms fmatt * @param {number | Pattern} time attack time * @example * note("c e g b g e") @@ -487,7 +513,26 @@ export const { fmenv } = registerControl('fmenv'); * ._scope() * */ -export const { fmattack } = registerControl('fmattack'); +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 @@ -500,12 +545,16 @@ export const { fmattack } = registerControl('fmattack'); * n("0 1 2 3".fast(4)).chord("").voicing().s("sawtooth").fmwave("brown").fm(.6) * */ -export const { fmwave } = registerControl('fmwave'); +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. * * @name fmdecay + * @synonyms fmdec * @param {number | Pattern} time decay time * @example * note("c e g b g e") @@ -515,11 +564,31 @@ export const { fmwave } = registerControl('fmwave'); * ._scope() * */ -export const { fmdecay } = registerControl('fmdecay'); +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 * * @name fmsustain + * @synonyms fmsus * @param {number | Pattern} level sustain level * @example * note("c e g b g e") @@ -529,10 +598,61 @@ export const { fmdecay } = registerControl('fmdecay'); * ._scope() * */ -export const { fmsustain } = registerControl('fmsustain'); -// these are not really useful... skipping for now -export const { fmrelease } = registerControl('fmrelease'); -export const { fmvelocity } = registerControl('fmvelocity'); +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 + * + * @name fmrelease + * @synonyms fmrel + * @param {number | Pattern} time release time + * + */ +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 +for (let i = 0; i <= 8; i++) { + for (let j = 0; j <= 8; j++) { + registerControl(`fm${i}${j}`); + } +} /** * Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`. diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 47bca330d..361733f10 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -325,7 +325,7 @@ const mod = (freq, range = 1, type = 'sine') => { osc.start(); const g = new GainNode(ctx, { gain: range }); osc.connect(g); // -range, range - return { node: g, stop: (t) => osc.stop(t) }; + return { fm: osc, scaled: g, stop: (t) => osc.stop(t) }; }; const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => { const carrfreq = frequencyparam.value; @@ -334,48 +334,86 @@ const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => return mod(modfreq, modgain, wave); }; export function applyFM(param, value, begin) { - const { - fmh: fmHarmonicity = 1, - fmi: fmModulationIndex, - fmenv: fmEnvelopeType = 'exp', - fmattack: fmAttack, - fmdecay: fmDecay, - fmsustain: fmSustain, - fmrelease: fmRelease, - fmvelocity: fmVelocity, - fmwave: fmWaveform = 'sine', - duration, - } = value; - let modulator; - let stop = () => {}; - - if (fmModulationIndex) { - const ac = getAudioContext(); - const envGain = ac.createGain(); - const fmmod = fm(param, fmHarmonicity, fmModulationIndex, fmWaveform); - - modulator = fmmod.node; - stop = fmmod.stop; - if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) { - // no envelope by default - modulator.connect(param); - } else { - const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]); - const holdEnd = begin + duration; - getParamADSR( - envGain.gain, - attack, - decay, - sustain, - release, - 0, - 1, - begin, - holdEnd, - fmEnvelopeType === 'exp' ? 'exponential' : 'linear', - ); - modulator.connect(envGain); - envGain.connect(param); + const ac = getAudioContext(); + let target = param; + let stop = (t) => {}; + const targets = [param]; + const modulators = []; + for (let i = 0; i < 8; i++) { + const iS = `${i === 0 ? '' : i}`; + let modulator; + const fmModulationIndex = value[`fmi${iS}`]; + if (fmModulationIndex) { + const fmmod = fm(target, value[`fmh${iS}`] ?? 1, fmModulationIndex, value[`fmwave${iS}`] ?? 'sine'); + modulator = fmmod.scaled; + const currStop = stop; + stop = (t) => { + currStop(t); + fmmod.stop(t); + }; + const adsr = ['attack', 'decay', 'sustain', 'release'].map((s) => value[`fm${s}${iS}`]); + if (!adsr.some((v) => v !== undefined)) { + // no envelope by default + modulator.connect(target); + modulators.push(modulator); + } else { + const envGain = ac.createGain(); + const [attack, decay, sustain, release] = getADSRValues(adsr); + const holdEnd = begin + value.duration; + const fmEnvelopeType = value[`fmenv${iS}`] ?? 'exp'; + getParamADSR( + envGain.gain, + attack, + decay, + sustain, + release, + 0, + 1, + begin, + holdEnd, + fmEnvelopeType === 'exp' ? 'exponential' : 'linear', + ); + modulator.connect(envGain); + envGain.connect(target); + modulators.push(envGain); + } + targets.push(fmmod.fm?.frequency); + if (targets[i]) { + // Cannot connect to noises, for example + target = targets[i]; + } + } + } + // Matrix + for (let i = 1; i <= 8; i++) { + for (let j = 0; j <= 8; j++) { + let control; + if (i === j + 1) { + // Standard fm3 -> fm2 -> fm1 -> param usage + control = `fm${i}`; + } else { + control = `fm${i}${j}`; + } + const amt = value[control]; + if (!amt) continue; + const source = modulators[i - 1]; + const target = targets[j]; + if (!source) { + const iS = `${i === 1 ? '' : i}`; + logger( + `[superdough] failed to connect FM from source ${i} (due to control ${control}): source ${i} does not exist. Please use control fm${iS} to set it up`, + 'warning', + ); + continue; + } + if (!target) { + logger( + `[superdough] failed to connect FM to target ${j} (due to control ${control}): target ${j} does not exist. Please use control fm${j} to set it up`, + 'warning', + ); + continue; + } + source.connect(target); } } return { stop }; From 7cf9db4872fe8d2bde619da87f197a4b1df64a70 Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 3 Oct 2025 18:20:41 -0500 Subject: [PATCH 02/45] Big matrix refactor --- packages/core/controls.mjs | 2 +- packages/superdough/helpers.mjs | 124 ++++++++++++++------------------ 2 files changed, 55 insertions(+), 71 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index a2136367d..3f8847d99 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -650,7 +650,7 @@ export const { // FM Matrix for (let i = 0; i <= 8; i++) { for (let j = 0; j <= 8; j++) { - registerControl(`fm${i}${j}`); + registerControl(`fmi${i}${j}`, `fm${i}${j}`); } } diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 361733f10..89ae7da6d 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -309,7 +309,7 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { constantNode.stop(stopTime); return constantNode; } -const mod = (freq, range = 1, type = 'sine') => { +const mod = (freq, type = 'sine') => { const ctx = getAudioContext(); let osc; if (noises.includes(type)) { @@ -321,99 +321,83 @@ const mod = (freq, range = 1, type = 'sine') => { osc.type = type; osc.frequency.value = freq; } - osc.start(); - const g = new GainNode(ctx, { gain: range }); - osc.connect(g); // -range, range - return { fm: osc, scaled: g, stop: (t) => osc.stop(t) }; + return { osc, stop: (t) => osc.stop(t), freq }; }; + const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => { const carrfreq = frequencyparam.value; const modfreq = carrfreq * harmonicityRatio; - const modgain = modfreq * modulationIndex; - return mod(modfreq, modgain, wave); + return mod(modfreq, wave) }; + export function applyFM(param, value, begin) { const ac = getAudioContext(); - let target = param; let stop = (t) => {}; - const targets = [param]; - const modulators = []; - for (let i = 0; i < 8; i++) { - const iS = `${i === 0 ? '' : i}`; - let modulator; - const fmModulationIndex = value[`fmi${iS}`]; - if (fmModulationIndex) { - const fmmod = fm(target, value[`fmh${iS}`] ?? 1, fmModulationIndex, value[`fmwave${iS}`] ?? 'sine'); - modulator = fmmod.scaled; - const currStop = stop; - stop = (t) => { - currStop(t); - fmmod.stop(t); - }; - const adsr = ['attack', 'decay', 'sustain', 'release'].map((s) => value[`fm${s}${iS}`]); - if (!adsr.some((v) => v !== undefined)) { - // no envelope by default - modulator.connect(target); - modulators.push(modulator); - } else { - const envGain = ac.createGain(); - const [attack, decay, sustain, release] = getADSRValues(adsr); - const holdEnd = begin + value.duration; - const fmEnvelopeType = value[`fmenv${iS}`] ?? 'exp'; - getParamADSR( - envGain.gain, - attack, - decay, - sustain, - release, - 0, - 1, - begin, - holdEnd, - fmEnvelopeType === 'exp' ? 'exponential' : 'linear', - ); - modulator.connect(envGain); - envGain.connect(target); - modulators.push(envGain); - } - targets.push(fmmod.fm?.frequency); - if (targets[i]) { - // Cannot connect to noises, for example - target = targets[i]; - } - } - } + const fms = {}; // Matrix for (let i = 1; i <= 8; i++) { for (let j = 0; j <= 8; j++) { let control; if (i === j + 1) { // Standard fm3 -> fm2 -> fm1 -> param usage - control = `fm${i}`; + const iS = i === 1 ? '' : i; + control = `fmi${iS}`; } else { - control = `fm${i}${j}`; + control = `fmi${i}${j}`; } const amt = value[control]; if (!amt) continue; - const source = modulators[i - 1]; - const target = targets[j]; - if (!source) { - const iS = `${i === 1 ? '' : i}`; + let io = []; + for (let [isMod, idx] of [[true, i], [false, j]]) { + debugger; + if (idx === 0) { + io.push(param); + continue; + } + if (!fms[idx]) { + const idxS = idx === 1 ? '' : idx; + const { osc, freq } = fm(param, value[`fmh${idxS}`] ?? 1, value[`fmwave${idxS}`] ?? 'sine'); + const currStop = stop; + stop = (t) => { + currStop(t); + osc.stop(t); + }; + const adsr = ['attack', 'decay', 'sustain', 'release'].map((s) => value[`fm${s}${idxS}`]); + if (!adsr.some((v) => v !== undefined)) { + fms[idx] = { input: osc.frequency, output: osc, freq}; + } else { + const envGain = ac.createGain(); + const [attack, decay, sustain, release] = getADSRValues(adsr); + const holdEnd = begin + value.duration; + const fmEnvelopeType = value[`fmenv${idxS}`] ?? 'exp'; + getParamADSR( + envGain.gain, + attack, + decay, + sustain, + release, + 0, + 1, + begin, + holdEnd, + fmEnvelopeType === 'exp' ? 'exponential' : 'linear', + ); + fms[idx] = { input: osc.frequency, output: osc.connect(envGain), freq}; + } + } + const { input, output, freq } = fms[idx]; + const g = gainNode(amt * freq); + io.push(isMod ? output.connect(g) : input); + } + if (!io[1]) { logger( - `[superdough] failed to connect FM from source ${i} (due to control ${control}): source ${i} does not exist. Please use control fm${iS} to set it up`, + `[superdough] control ${control} failed to connect FM ${i} to target ${j} due to missing frequency parameter (likely because fm${j} is noise)`, 'warning', ); continue; } - if (!target) { - logger( - `[superdough] failed to connect FM to target ${j} (due to control ${control}): target ${j} does not exist. Please use control fm${j} to set it up`, - 'warning', - ); - continue; - } - source.connect(target); + io[0].connect(io[1]); } } return { stop }; From bd73d96847889f78b4a3cc9e98d4df0eeeb5244f Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 3 Oct 2025 18:49:49 -0500 Subject: [PATCH 03/45] Cleanup --- packages/superdough/helpers.mjs | 15 +++--- test/__snapshots__/examples.test.mjs.snap | 58 +++++++++++------------ 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 89ae7da6d..de8daaf5d 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,6 +1,7 @@ +import { logger } from './logger.mjs'; +import { getNoiseBuffer } from './noise.mjs'; import { getAudioContext } from './superdough.mjs'; import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs'; -import { getNoiseBuffer } from './noise.mjs'; export const noises = ['pink', 'white', 'brown', 'crackle']; @@ -328,7 +329,7 @@ const mod = (freq, type = 'sine') => { const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => { const carrfreq = frequencyparam.value; const modfreq = carrfreq * harmonicityRatio; - return mod(modfreq, wave) + return mod(modfreq, wave); }; export function applyFM(param, value, begin) { @@ -349,8 +350,10 @@ export function applyFM(param, value, begin) { const amt = value[control]; if (!amt) continue; let io = []; - for (let [isMod, idx] of [[true, i], [false, j]]) { - debugger; + for (let [isMod, idx] of [ + [true, i], + [false, j], + ]) { if (idx === 0) { io.push(param); continue; @@ -365,7 +368,7 @@ export function applyFM(param, value, begin) { }; const adsr = ['attack', 'decay', 'sustain', 'release'].map((s) => value[`fm${s}${idxS}`]); if (!adsr.some((v) => v !== undefined)) { - fms[idx] = { input: osc.frequency, output: osc, freq}; + fms[idx] = { input: osc.frequency, output: osc, freq }; } else { const envGain = ac.createGain(); const [attack, decay, sustain, release] = getADSRValues(adsr); @@ -383,7 +386,7 @@ export function applyFM(param, value, begin) { holdEnd, fmEnvelopeType === 'exp' ? 'exponential' : 'linear', ); - fms[idx] = { input: osc.frequency, output: osc.connect(envGain), freq}; + fms[idx] = { input: osc.frequency, output: osc.connect(envGain), freq }; } } const { input, output, freq } = fms[idx]; diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 73278947d..be8b371ab 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3880,35 +3880,6 @@ exports[`runs examples > example "floor" example index 0 1`] = ` ] `; -exports[`runs examples > example "fm" example index 0 1`] = ` -[ - "[ 0/1 → 1/6 | note:c fmi:0 ]", - "[ 1/6 → 1/3 | note:e fmi:0 ]", - "[ 1/3 → 1/2 | note:g fmi:0 ]", - "[ 1/2 → 2/3 | note:b fmi:0 ]", - "[ 2/3 → 5/6 | note:g fmi:0 ]", - "[ 5/6 → 1/1 | note:e fmi:0 ]", - "[ 1/1 → 7/6 | note:c fmi:1 ]", - "[ 7/6 → 4/3 | note:e fmi:1 ]", - "[ 4/3 → 3/2 | note:g fmi:1 ]", - "[ 3/2 → 5/3 | note:b fmi:1 ]", - "[ 5/3 → 11/6 | note:g fmi:1 ]", - "[ 11/6 → 2/1 | note:e fmi:1 ]", - "[ 2/1 → 13/6 | note:c fmi:2 ]", - "[ 13/6 → 7/3 | note:e fmi:2 ]", - "[ 7/3 → 5/2 | note:g fmi:2 ]", - "[ 5/2 → 8/3 | note:b fmi:2 ]", - "[ 8/3 → 17/6 | note:g fmi:2 ]", - "[ 17/6 → 3/1 | note:e fmi:2 ]", - "[ 3/1 → 19/6 | note:c fmi:8 ]", - "[ 19/6 → 10/3 | note:e fmi:8 ]", - "[ 10/3 → 7/2 | note:g fmi:8 ]", - "[ 7/2 → 11/3 | note:b fmi:8 ]", - "[ 11/3 → 23/6 | note:g fmi:8 ]", - "[ 23/6 → 4/1 | note:e fmi:8 ]", -] -`; - exports[`runs examples > example "fmattack" example index 0 1`] = ` [ "[ 0/1 → 1/6 | note:c fmi:4 fmattack:0 ]", @@ -4025,6 +3996,35 @@ exports[`runs examples > example "fmh" example index 0 1`] = ` ] `; +exports[`runs examples > example "fmi" example index 0 1`] = ` +[ + "[ 0/1 → 1/6 | note:c fmi:0 ]", + "[ 1/6 → 1/3 | note:e fmi:0 ]", + "[ 1/3 → 1/2 | note:g fmi:0 ]", + "[ 1/2 → 2/3 | note:b fmi:0 ]", + "[ 2/3 → 5/6 | note:g fmi:0 ]", + "[ 5/6 → 1/1 | note:e fmi:0 ]", + "[ 1/1 → 7/6 | note:c fmi:1 ]", + "[ 7/6 → 4/3 | note:e fmi:1 ]", + "[ 4/3 → 3/2 | note:g fmi:1 ]", + "[ 3/2 → 5/3 | note:b fmi:1 ]", + "[ 5/3 → 11/6 | note:g fmi:1 ]", + "[ 11/6 → 2/1 | note:e fmi:1 ]", + "[ 2/1 → 13/6 | note:c fmi:2 ]", + "[ 13/6 → 7/3 | note:e fmi:2 ]", + "[ 7/3 → 5/2 | note:g fmi:2 ]", + "[ 5/2 → 8/3 | note:b fmi:2 ]", + "[ 8/3 → 17/6 | note:g fmi:2 ]", + "[ 17/6 → 3/1 | note:e fmi:2 ]", + "[ 3/1 → 19/6 | note:c fmi:8 ]", + "[ 19/6 → 10/3 | note:e fmi:8 ]", + "[ 10/3 → 7/2 | note:g fmi:8 ]", + "[ 7/2 → 11/3 | note:b fmi:8 ]", + "[ 11/3 → 23/6 | note:g fmi:8 ]", + "[ 23/6 → 4/1 | note:e fmi:8 ]", +] +`; + exports[`runs examples > example "fmsustain" example index 0 1`] = ` [ "[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.1 fmsustain:1 ]", From 4c49cd9297326cf4991f1dca879bef3ed657be51 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 18 Oct 2025 11:24:56 -0500 Subject: [PATCH 04/45] More examples --- packages/core/controls.mjs | 30 ++++++++++++++++++ test/__snapshots__/examples.test.mjs.snap | 37 +++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 55a07cc27..281bf76b7 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -456,6 +456,9 @@ export const { attack, att } = registerControl('attack', 'att'); * Whole numbers and simple ratios sound more natural, * while decimal numbers and complex ratios sound metallic. * + * A number may be added afterwards to control the harmonicity of + * any of the 8 individual FMs (e.g. `fmh2`) + * * @name fmh * @param {number | Pattern} harmonicity * @example @@ -470,6 +473,11 @@ export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerM * Sets the Frequency Modulation of the synth. * Controls the modulation index, which defines the brightness of the sound. * + * A number may be added afterwards to control the modulation index of + * any of the 8 individual FMs (e.g. `fm3`). Also, FMs may be routed into + * each other with matrix commands like `fm13`, which would send `fm1` back into + * `fm3` + * * @name fmi * @param {number | Pattern} brightness modulation index * @synonyms fm @@ -477,6 +485,10 @@ export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerM * note("c e g b g e") * .fm("<0 1 2 8 32>") * ._scope() + * @example + * s("sine").note("F1").seg(8) + * .fm(4).fm2(rand.mul(4)).fm3(saw.mul(8).slow(8)) + * .fmh(1.06).fmh2(10).fmh3(0.1) * */ export const { fmi, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8 } = @@ -485,6 +497,9 @@ export const { fmi, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2 /** * Ramp type of fm envelope. Exp might be a bit broken.. * + * A number may be added afterwards to control the envelope of + * any of the 8 individual FMs (e.g. `fmenv4`) + * * @name fmenv * @param {number | Pattern} type lin | exp * @example @@ -503,6 +518,9 @@ export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fm /** * Attack time for the FM envelope: time it takes to reach maximum modulation * + * A number may be added afterwards to control the attack of the envelope of + * any of the 8 individual FMs (e.g. `fmatt5`) + * * @name fmattack * @synonyms fmatt * @param {number | Pattern} time attack time @@ -537,6 +555,9 @@ export const { /** * Waveform of the fm modulator * + * A number may be added afterwards to control the waveform + * any of the 8 individual FMs (e.g. `fmwave6`) + * * @name fmwave * @param {number | Pattern} wave waveform * @example @@ -553,6 +574,9 @@ export const { fmwave, fmwave1, fmwave2, fmwave3, fmwave4, fmwave5, fmwave6, fmw /** * Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase. * + * A number may be added afterwards to control the decay of the envelope of + * any of the 8 individual FMs (e.g. `fmdec6`) + * * @name fmdecay * @synonyms fmdec * @param {number | Pattern} time decay time @@ -587,6 +611,9 @@ export const { /** * Sustain level for the FM envelope: how much modulation is applied after the decay phase * + * A number may be added afterwards to control the sustain of the envelope of + * any of the 8 individual FMs (e.g. `fmsus7`) + * * @name fmsustain * @synonyms fmsus * @param {number | Pattern} level sustain level @@ -621,6 +648,9 @@ export const { /** * Release time for the FM envelope: how much modulation is applied after the note is released * + * A number may be added afterwards to control the release of the envelope of + * any of the 8 individual FMs (e.g. `fmrel8`) + * * @name fmrelease * @synonyms fmrel * @param {number | Pattern} time release time diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 590c47e4e..04360a4dd 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -4101,6 +4101,43 @@ exports[`runs examples > example "fmi" example index 0 1`] = ` ] `; +exports[`runs examples > example "fmi" example index 1 1`] = ` +[ + "[ 0/1 → 1/8 | s:sine note:F1 fmi:4 fmi2:0 fmi3:0 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 1/8 → 1/4 | s:sine note:F1 fmi:4 fmi2:2.7408622801303864 fmi3:0.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 1/4 → 3/8 | s:sine note:F1 fmi:4 fmi2:1.479038767516613 fmi3:0.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 3/8 → 1/2 | s:sine note:F1 fmi:4 fmi2:1.605570062994957 fmi3:0.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 1/2 → 5/8 | s:sine note:F1 fmi:4 fmi2:1.041922464966774 fmi3:0.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 5/8 → 3/4 | s:sine note:F1 fmi:4 fmi2:0.5425434708595276 fmi3:0.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 3/4 → 7/8 | s:sine note:F1 fmi:4 fmi2:0.7833059206604958 fmi3:0.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 7/8 → 1/1 | s:sine note:F1 fmi:4 fmi2:1.590524323284626 fmi3:0.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 1/1 → 9/8 | s:sine note:F1 fmi:4 fmi2:2.078168660402298 fmi3:1 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 9/8 → 5/4 | s:sine note:F1 fmi:4 fmi2:2.689958058297634 fmi3:1.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 5/4 → 11/8 | s:sine note:F1 fmi:4 fmi2:2.914912812411785 fmi3:1.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 11/8 → 3/2 | s:sine note:F1 fmi:4 fmi2:0.7312150076031685 fmi3:1.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 3/2 → 13/8 | s:sine note:F1 fmi:4 fmi2:2.4336322993040085 fmi3:1.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 13/8 → 7/4 | s:sine note:F1 fmi:4 fmi2:1.987018845975399 fmi3:1.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 7/4 → 15/8 | s:sine note:F1 fmi:4 fmi2:0.8189515843987465 fmi3:1.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 15/8 → 2/1 | s:sine note:F1 fmi:4 fmi2:2.3782287165522575 fmi3:1.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 2/1 → 17/8 | s:sine note:F1 fmi:4 fmi2:3.838108479976654 fmi3:2 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 17/8 → 9/4 | s:sine note:F1 fmi:4 fmi2:3.613561175763607 fmi3:2.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 9/4 → 19/8 | s:sine note:F1 fmi:4 fmi2:1.3819300457835197 fmi3:2.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 19/8 → 5/2 | s:sine note:F1 fmi:4 fmi2:3.346026562154293 fmi3:2.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 5/2 → 21/8 | s:sine note:F1 fmi:4 fmi2:1.8344642966985703 fmi3:2.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 21/8 → 11/4 | s:sine note:F1 fmi:4 fmi2:0.6407739371061325 fmi3:2.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 11/4 → 23/8 | s:sine note:F1 fmi:4 fmi2:2.5329310819506645 fmi3:2.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 23/8 → 3/1 | s:sine note:F1 fmi:4 fmi2:0.06501668691635132 fmi3:2.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 3/1 → 25/8 | s:sine note:F1 fmi:4 fmi2:0.8691564425826073 fmi3:3 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 25/8 → 13/4 | s:sine note:F1 fmi:4 fmi2:1.3729755133390427 fmi3:3.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 13/4 → 27/8 | s:sine note:F1 fmi:4 fmi2:3.9694602862000465 fmi3:3.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 27/8 → 7/2 | s:sine note:F1 fmi:4 fmi2:1.6347672045230865 fmi3:3.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 7/2 → 29/8 | s:sine note:F1 fmi:4 fmi2:1.639795258641243 fmi3:3.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 29/8 → 15/4 | s:sine note:F1 fmi:4 fmi2:3.7565943151712418 fmi3:3.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 15/4 → 31/8 | s:sine note:F1 fmi:4 fmi2:3.2486692890524864 fmi3:3.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 31/8 → 4/1 | s:sine note:F1 fmi:4 fmi2:2.9446661546826363 fmi3:3.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", +] +`; + exports[`runs examples > example "fmsustain" example index 0 1`] = ` [ "[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.1 fmsustain:1 ]", From 654c1a86f08550586049fcef4a6b4c9ac5007755 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 18 Nov 2025 16:02:33 -0600 Subject: [PATCH 05/45] Simplify declarations via globalThis; fix cleanup/stop --- packages/core/controls.mjs | 106 +++++--------------------------- packages/superdough/helpers.mjs | 46 +++++++++----- 2 files changed, 44 insertions(+), 108 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 02178de97..854f48ffb 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -468,7 +468,8 @@ export const { attack, att } = registerControl('attack', 'att'); * ._scope() * */ -export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerMultiControl(['fmh', 'fmi'], 8, 'fmh'); +Object.assign(globalThis, registerMultiControl(['fmh', 'fmi'], 8, 'fmh')); + /** * Sets the Frequency Modulation of the synth. * Controls the modulation index, which defines the brightness of the sound. @@ -491,8 +492,8 @@ export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerM * .fmh(1.06).fmh2(10).fmh3(0.1) * */ -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'); +Object.assign(globalThis, registerMultiControl(['fmi', 'fmh'], 8, 'fm')); + // fm envelope /** * Ramp type of fm envelope. Exp might be a bit broken.. @@ -511,10 +512,8 @@ export const { fmi, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2 * ._scope() * */ -export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fmenv8 } = registerMultiControl( - 'fmenv', - 8, -); +Object.assign(globalThis, registerMultiControl('fmenv', 8)); + /** * Attack time for the FM envelope: time it takes to reach maximum modulation * @@ -531,26 +530,7 @@ export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fm * ._scope() * */ -export const { - fmattack, - fmattack1, - fmattack2, - fmattack3, - fmattack4, - fmattack5, - fmattack6, - fmattack7, - fmattack8, - fmatt, - fmatt1, - fmatt2, - fmatt3, - fmatt4, - fmatt5, - fmatt6, - fmatt7, - fmatt8, -} = registerMultiControl('fmattack', 8, 'fmatt'); +Object.assign(globalThis, registerMultiControl('fmattack', 8, 'fmatt')); /** * Waveform of the fm modulator @@ -566,10 +546,7 @@ export const { * n("0 1 2 3".fast(4)).chord("").voicing().s("sawtooth").fmwave("brown").fm(.6) * */ -export const { fmwave, fmwave1, fmwave2, fmwave3, fmwave4, fmwave5, fmwave6, fmwave7, fmwave8 } = registerMultiControl( - 'fmwave', - 8, -); +Object.assign(globalThis, registerMultiControl('fmwave', 8)); /** * Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase. @@ -588,26 +565,8 @@ export const { fmwave, fmwave1, fmwave2, fmwave3, fmwave4, fmwave5, fmwave6, fmw * ._scope() * */ -export const { - fmdecay, - fmdecay1, - fmdecay2, - fmdecay3, - fmdecay4, - fmdecay5, - fmdecay6, - fmdecay7, - fmdecay8, - fmdec, - fmdec1, - fmdec2, - fmdec3, - fmdec4, - fmdec5, - fmdec6, - fmdec7, - fmdec8, -} = registerMultiControl('fmdecay', 8, 'fmdec'); +Object.assign(globalThis, registerMultiControl('fmdecay', 8, 'fmdec')); + /** * Sustain level for the FM envelope: how much modulation is applied after the decay phase * @@ -625,26 +584,8 @@ export const { * ._scope() * */ -export const { - fmsustain, - fmsustain1, - fmsustain2, - fmsustain3, - fmsustain4, - fmsustain5, - fmsustain6, - fmsustain7, - fmsustain8, - fmsus, - fmsus1, - fmsus2, - fmsus3, - fmsus4, - fmsus5, - fmsus6, - fmsus7, - fmsus8, -} = registerMultiControl('fmsustain', 8, 'fmsus'); +Object.assign(globalThis, registerMultiControl('fmsustain', 8, 'fmsus')); + /** * Release time for the FM envelope: how much modulation is applied after the note is released * @@ -656,31 +597,12 @@ export const { * @param {number | Pattern} time release time * */ -export const { - fmrelease, - fmrelease1, - fmrelease2, - fmrelease3, - fmrelease4, - fmrelease5, - fmrelease6, - fmrelease7, - fmrelease8, - fmrel, - fmrel1, - fmrel2, - fmrel3, - fmrel4, - fmrel5, - fmrel6, - fmrel7, - fmrel8, -} = registerMultiControl('fmrelease', 8, 'fmrel'); +Object.assign(globalThis, registerMultiControl('fmrelease', 8, 'fmrel')); // FM Matrix for (let i = 0; i <= 8; i++) { for (let j = 0; j <= 8; j++) { - registerControl(`fmi${i}${j}`, `fm${i}${j}`); + Object.assign(globalThis, registerControl(`fmi${i}${j}`, `fm${i}${j}`)); } } diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index b856a0d88..0ab43791d 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -336,6 +336,7 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { constantNode.stop(stopTime); return constantNode; } + const mod = (freq, type = 'sine') => { const ctx = getAudioContext(); let osc; @@ -349,10 +350,10 @@ const mod = (freq, type = 'sine') => { osc.frequency.value = freq; } osc.start(); - return { osc, stop: (t) => osc.stop(t), freq }; + return { osc, freq }; }; -const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => { +const fm = (frequencyparam, harmonicityRatio, wave = 'sine') => { const carrfreq = frequencyparam.value; const modfreq = carrfreq * harmonicityRatio; return mod(modfreq, wave); @@ -360,7 +361,7 @@ const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => export function applyFM(param, value, begin) { const ac = getAudioContext(); - let stop = (t) => {}; + const toStop = []; // fm oscillators we will expose `stop` for const fms = {}; // Matrix for (let i = 1; i <= 8; i++) { @@ -377,8 +378,8 @@ export function applyFM(param, value, begin) { if (!amt) continue; let io = []; for (let [isMod, idx] of [ - [true, i], - [false, j], + [true, i], // source + [false, j], // target ]) { if (idx === 0) { io.push(param); @@ -387,15 +388,11 @@ export function applyFM(param, value, begin) { if (!fms[idx]) { const idxS = idx === 1 ? '' : idx; const { osc, freq } = fm(param, value[`fmh${idxS}`] ?? 1, value[`fmwave${idxS}`] ?? 'sine'); - const currStop = stop; - stop = (t) => { - currStop(t); - osc.stop(t); - }; + toStop.push(osc); + const toCleanup = [osc]; // nodes we want to cleanup after oscillator `stop` const adsr = ['attack', 'decay', 'sustain', 'release'].map((s) => value[`fm${s}${idxS}`]); - if (!adsr.some((v) => v !== undefined)) { - fms[idx] = { input: osc.frequency, output: osc, freq }; - } else { + let output = osc; + if (adsr.some((v) => v !== undefined)) { const envGain = ac.createGain(); const [attack, decay, sustain, release] = getADSRValues(adsr); const holdEnd = begin + value.duration; @@ -412,12 +409,16 @@ export function applyFM(param, value, begin) { holdEnd, fmEnvelopeType === 'exp' ? 'exponential' : 'linear', ); - fms[idx] = { input: osc.frequency, output: osc.connect(envGain), freq }; + toCleanup.push(envGain); + output = osc.connect(envGain); } + fms[idx] = { input: osc.frequency, output, freq, toCleanup }; + osc.onended = () => cleanupNodes(fms[idx].toCleanup); } - const { input, output, freq } = fms[idx]; + const { input, output, freq, toCleanup } = fms[idx]; const g = gainNode(amt * freq); io.push(isMod ? output.connect(g) : input); + toCleanup.push(g); } if (!io[1]) { logger( @@ -429,7 +430,9 @@ export function applyFM(param, value, begin) { io[0].connect(io[1]); } } - return { stop }; + return { + stop: (t) => toStop.forEach((m) => m?.stop(t)), + }; } // Saturation curves @@ -549,3 +552,14 @@ export const destroyAudioWorkletNode = (node) => { node.disconnect(); node.parameters.get('end')?.setValueAtTime(0, 0); }; + +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)); +}; From 8144ec46bd7eeaf1d0cfbbe5c59a54a347941aeb Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 19 Nov 2025 18:35:35 -0600 Subject: [PATCH 06/45] Attempt system for sounds tab previews --- packages/superdough/superdough.mjs | 4 +- .../src/repl/components/panel/SoundsTab.jsx | 37 +++++++++++++------ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 65c444120..73fece4f5 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -263,8 +263,8 @@ let audioReady; export async function initAudioOnFirstClick(options) { if (!audioReady) { audioReady = new Promise((resolve) => { - document.addEventListener('click', async function listener() { - document.removeEventListener('click', listener); + document.addEventListener('mousedown', async function listener() { + document.removeEventListener('mousedown', listener); await initAudio(options); resolve(); }); diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index a9f10bf12..311f593e1 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -14,6 +14,10 @@ import { prebake } from '@src/repl/prebake.mjs'; const getSamples = (samples) => Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1; +function wait(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + export function SoundsTab() { const sounds = useStore(soundMap); @@ -121,19 +125,30 @@ export function SoundsTab() { sustain: 1, duration: 0.5, }; - soundPreviewIdx++; const onended = () => trigRef.current?.node?.disconnect(); - try { - // Pre-load the sample by calling onTrigger with a future time - // This triggers the loading but schedules playback for later - const time = ctx.currentTime + 0.5; // Give 500ms for loading - const ref = await onTrigger(time, params, onended); - trigRef.current = ref; - if (ref?.node) { - connectToDestination(ref.node); + // Attempt to play the sample and retry every 200ms until 10 attempts have been reached + let errMsg; + for (let attempt = 0; attempt < 10; attempt++) { + try { + // Pre-load the sample by calling onTrigger with a future time + // This triggers the loading but schedules playback for later + const time = ctx.currentTime + 0.05; // Give 50ms for loading + const ref = await onTrigger(time, params, onended); + trigRef.current = ref; + if (ref?.node) { + connectToDestination(ref.node); + soundPreviewIdx++; + break; + } + } catch (err) { + console.log(err); + errMsg = err; + } + if (attempt == 9) { + console.warn('Failed to trigger sound after 10 attempts' + (errMsg ? `: ${errMsg}` : '')); + } else { + await wait(200); } - } catch (err) { - console.warn('Failed to trigger sound:', err); } }} > From 74488f8480d28606c192379f9df8695cfd962977 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 19 Nov 2025 18:41:56 -0600 Subject: [PATCH 07/45] Stray log message --- website/src/repl/components/panel/SoundsTab.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index 311f593e1..61d4ba1ce 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -141,7 +141,6 @@ export function SoundsTab() { break; } } catch (err) { - console.log(err); errMsg = err; } if (attempt == 9) { From cf12e3e1d8892de00d20a5c6f4e1a5a164cbc3f5 Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 23 Nov 2025 12:21:55 -0600 Subject: [PATCH 08/45] Hook up the octave control --- packages/superdough/helpers.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index de64df0e8..59608698d 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -517,7 +517,7 @@ export const getDistortion = (distort, postgain, algorithm) => { }; export const getFrequencyFromValue = (value, defaultNote = 36) => { - let { note, freq } = value; + let { note, freq, octave = 0 } = value; note = note || defaultNote; if (typeof note === 'string') { note = noteToMidi(note); // e.g. c3 => 48 @@ -526,7 +526,7 @@ export const getFrequencyFromValue = (value, defaultNote = 36) => { if (!freq && typeof note === 'number') { freq = midiToFreq(note); // + 48); } - + freq *= Math.pow(2, octave); return Number(freq); }; From 6c0d550b6b3629312eb8e65719781c28effab943 Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 23 Nov 2025 12:25:40 -0600 Subject: [PATCH 09/45] Fix example --- packages/core/controls.mjs | 3 +-- test/__snapshots__/examples.test.mjs.snap | 24 +++++++++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 9a2be7f44..5c5883e9a 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1822,8 +1822,7 @@ export const { nudge } = registerControl('nudge'); * @name octave * @param {number | Pattern} octave octave number * @example - * n("0,4,7").s('supersquare').octave("<3 4 5 6>").osc() - * @superDirtOnly + * n("0,4,7").scale("F:minor").s('supersaw').octave("<0 1 2 3>") */ export const { octave } = registerControl('octave'); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 31e4bddd3..4d4da8b22 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -6888,18 +6888,18 @@ exports[`runs examples > example "nrpv" example index 0 1`] = ` exports[`runs examples > example "octave" example index 0 1`] = ` [ - "[ 0/1 → 1/1 | n:0 s:supersquare octave:3 ]", - "[ 0/1 → 1/1 | n:4 s:supersquare octave:3 ]", - "[ 0/1 → 1/1 | n:7 s:supersquare octave:3 ]", - "[ 1/1 → 2/1 | n:0 s:supersquare octave:4 ]", - "[ 1/1 → 2/1 | n:4 s:supersquare octave:4 ]", - "[ 1/1 → 2/1 | n:7 s:supersquare octave:4 ]", - "[ 2/1 → 3/1 | n:0 s:supersquare octave:5 ]", - "[ 2/1 → 3/1 | n:4 s:supersquare octave:5 ]", - "[ 2/1 → 3/1 | n:7 s:supersquare octave:5 ]", - "[ 3/1 → 4/1 | n:0 s:supersquare octave:6 ]", - "[ 3/1 → 4/1 | n:4 s:supersquare octave:6 ]", - "[ 3/1 → 4/1 | n:7 s:supersquare octave:6 ]", + "[ 0/1 → 1/1 | note:F3 s:supersaw octave:0 ]", + "[ 0/1 → 1/1 | note:C4 s:supersaw octave:0 ]", + "[ 0/1 → 1/1 | note:F4 s:supersaw octave:0 ]", + "[ 1/1 → 2/1 | note:F3 s:supersaw octave:1 ]", + "[ 1/1 → 2/1 | note:C4 s:supersaw octave:1 ]", + "[ 1/1 → 2/1 | note:F4 s:supersaw octave:1 ]", + "[ 2/1 → 3/1 | note:F3 s:supersaw octave:2 ]", + "[ 2/1 → 3/1 | note:C4 s:supersaw octave:2 ]", + "[ 2/1 → 3/1 | note:F4 s:supersaw octave:2 ]", + "[ 3/1 → 4/1 | note:F3 s:supersaw octave:3 ]", + "[ 3/1 → 4/1 | note:C4 s:supersaw octave:3 ]", + "[ 3/1 → 4/1 | note:F4 s:supersaw octave:3 ]", ] `; From 6221099af80f87e783339d46f31b685b0ce0f3ed Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 23 Nov 2025 18:09:38 -0500 Subject: [PATCH 10/45] working --- packages/core/repl.mjs | 1 + .../repl/components/button/action-button.jsx | 24 ++++++++++++ .../panel/ImportPrebakeScriptButton.jsx | 39 +++++++++++++++++++ website/src/repl/components/panel/Panel.jsx | 2 +- .../src/repl/components/panel/PatternsTab.jsx | 2 +- .../src/repl/components/panel/SettingsTab.jsx | 12 ++++-- website/src/repl/prebake.mjs | 11 +++++- website/src/repl/useReplContext.jsx | 11 ++++-- website/src/settings.mjs | 3 ++ 9 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 website/src/repl/components/panel/ImportPrebakeScriptButton.jsx diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index bcb70eec9..90b4fcbb8 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -147,6 +147,7 @@ export function repl({ // set pattern methods that use this repl via closure const injectPatternMethods = () => { + Pattern.prototype.p = function (id) { if (typeof id === 'string' && (id.startsWith('_') || id.endsWith('_'))) { // allows muting a pattern x with x_ or _x diff --git a/website/src/repl/components/button/action-button.jsx b/website/src/repl/components/button/action-button.jsx index d589b7bed..eb395212c 100644 --- a/website/src/repl/components/button/action-button.jsx +++ b/website/src/repl/components/button/action-button.jsx @@ -8,3 +8,27 @@ export function ActionButton({ children, label, labelIsHidden, className, ...but ); } +export function ActionInput({ label, className, ...inputProps }) { + return ( + + ); +} + +export function SpecialActionButton(props) { + const { className, ...buttonProps } = props + + return + +} +export function SpecialActionInput(props) { + const { className, ...inputProps } = props + return +} + + diff --git a/website/src/repl/components/panel/ImportPrebakeScriptButton.jsx b/website/src/repl/components/panel/ImportPrebakeScriptButton.jsx new file mode 100644 index 000000000..c521c527b --- /dev/null +++ b/website/src/repl/components/panel/ImportPrebakeScriptButton.jsx @@ -0,0 +1,39 @@ +import { errorLogger } from '@strudel/core'; +import { useSettings, storeStartupScript } from '../../../settings.mjs'; +import { SpecialActionInput } from '../button/action-button'; + +async function importScript(script) { + const reader = new FileReader() + reader.readAsText(script) + + reader.onload = () => { + const text = reader.result; + console.info(text) + storeStartupScript(text) + + }; + + reader.onerror = () => { + errorLogger(new Error('failed to import prebake script'), 'ImportPrebakeScriptButton') + } + +} +export function ImportPrebakeScriptButton() { + const settings = useSettings(); + + return importScript(e.target.files[0])} /> + + // return +} \ No newline at end of file diff --git a/website/src/repl/components/panel/Panel.jsx b/website/src/repl/components/panel/Panel.jsx index b26c2edef..1af859f81 100644 --- a/website/src/repl/components/panel/Panel.jsx +++ b/website/src/repl/components/panel/Panel.jsx @@ -127,7 +127,7 @@ function PanelContent({ context, tab }) { case tabNames.reference: return ; case tabNames.settings: - return ; + return ; case tabNames.files: return ; default: diff --git a/website/src/repl/components/panel/PatternsTab.jsx b/website/src/repl/components/panel/PatternsTab.jsx index 5e0d50e72..67585d20a 100644 --- a/website/src/repl/components/panel/PatternsTab.jsx +++ b/website/src/repl/components/panel/PatternsTab.jsx @@ -83,7 +83,7 @@ function UserPatterns({ context }) { const activePattern = useActivePattern(); const viewingPatternStore = useViewingPatternData(); const viewingPatternData = parseJSON(viewingPatternStore); - const { userPatterns, patternFilter, patternAutoStart } = useSettings(); + const { userPatterns, patternFilter, patternAutoStart, } = useSettings(); const viewingPatternID = viewingPatternData?.id; return (
diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index 85991f55c..2c66ed1a3 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -7,6 +7,8 @@ import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx'; import { confirmDialog } from '../../util.mjs'; import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio'; +import { ActionButton, SpecialActionButton } from '../button/action-button.jsx'; +import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx'; function Checkbox({ label, value, onChange, disabled = false }) { return ( @@ -86,7 +88,7 @@ const fontFamilyOptions = { const RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?'; -export function SettingsTab({ started }) { +export function SettingsTab({ started,context }) { const { theme, keybindings, @@ -113,6 +115,7 @@ export function SettingsTab({ started }) { isTabIndentationEnabled, isMultiCursorEnabled, patternAutoStart, + startupScript, } = useSettings(); const shouldAlwaysSync = isUdels(); const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; @@ -204,6 +207,8 @@ export function SettingsTab({ started }) { />
+ + {/* context.editStartupScript(startupScript)}> */} Try clicking the logo in the top left! - + ); diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index 8a6ccc7e0..960771f05 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -1,14 +1,23 @@ -import { Pattern, noteToMidi, valueToMidi } from '@strudel/core'; +import { Pattern, noteToMidi, valueToMidi } from '@strudel/core'; import { aliasBank, registerSynthSounds, registerZZFXSounds, samples } from '@strudel/webaudio'; import { registerSamplesFromDB } from './idbutils.mjs'; import './piano.mjs'; import './files.mjs'; +import { settingsMap } from '@src/settings.mjs'; +import { evaluate } from '@strudel/transpiler'; const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; const baseCDN = 'https://strudel.b-cdn.net'; export async function prebake() { + + // const settings = settingsMap.get() + + + // const prebakeScript = settings.startupScript || ''; + // await evaluate(prebakeScript); + // https://archive.org/details/SalamanderGrandPianoV3 // License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm await Promise.all([ diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index ac30fe342..4e8b2b946 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core'; import { getDrawContext } from '@strudel/draw'; -import { transpiler } from '@strudel/transpiler'; +import { evaluate, transpiler } from '@strudel/transpiler'; import { getAudioContextCurrentTime, webaudioOutput, @@ -47,6 +47,7 @@ if (typeof window !== 'undefined') { multiChannelOrbits: parseBoolean(multiChannelOrbits), }); modulesLoading = loadModules(); + // prebakeScript = evaluate(settingsMap.get().startupScript ?? '') presets = prebake(); drawContext = getDrawContext(); clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width); @@ -63,11 +64,10 @@ async function getModule(name) { const initialCode = `// LOADING`; export function useReplContext() { - const { isSyncEnabled, audioEngineTarget } = useSettings(); + const { isSyncEnabled, audioEngineTarget, startupScript } = useSettings(); const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc; const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput; const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds; - const init = useCallback(() => { const drawTime = [-2, 2]; const drawContext = getDrawContext(); @@ -84,7 +84,9 @@ export function useReplContext() { pattern: silence, drawTime, drawContext, - prebake: async () => Promise.all([modulesLoading, presets]), + prebake: async () => Promise.all([modulesLoading, presets,]).then(() => { + return evaluate(startupScript ?? '') + }), onUpdateState: (state) => { setReplState({ ...state }); }, @@ -200,6 +202,7 @@ export function useReplContext() { } }; + const handleEvaluate = () => { editorRef.current.evaluate(); }; diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 7c62b0ded..1fff439be 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -45,6 +45,7 @@ export const defaultSettings = { isPanelOpen: true, togglePanelTrigger: 'click', //click | hover userPatterns: '{}', + startupScript: '//edit this script to run code on startup\n', audioEngineTarget: audioEngineTargets.webaudio, isButtonRowHidden: false, isCSSAnimationDisabled: false, @@ -108,6 +109,8 @@ export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab); export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool); export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool); +export const storeStartupScript = (script) => settingsMap.setKey('startupScript', script); + export const setIsZen = (active) => settingsMap.setKey('isZen', !!active); const patternSetting = (key) => From d5dfbd7aa4d4236bdf143b94b7abddf7a9fe93c5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 23 Nov 2025 18:45:13 -0500 Subject: [PATCH 11/45] cleanup --- packages/core/repl.mjs | 1 - .../repl/components/button/action-button.jsx | 45 ++++++++++------- .../panel/ImportPrebakeScriptButton.jsx | 48 ++++++++----------- website/src/repl/components/panel/Panel.jsx | 2 +- .../src/repl/components/panel/PatternsTab.jsx | 2 +- .../src/repl/components/panel/SettingsTab.jsx | 3 +- website/src/repl/prebake.mjs | 4 +- website/src/repl/useReplContext.jsx | 8 ++-- 8 files changed, 54 insertions(+), 59 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 90b4fcbb8..bcb70eec9 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -147,7 +147,6 @@ export function repl({ // set pattern methods that use this repl via closure const injectPatternMethods = () => { - Pattern.prototype.p = function (id) { if (typeof id === 'string' && (id.startsWith('_') || id.endsWith('_'))) { // allows muting a pattern x with x_ or _x diff --git a/website/src/repl/components/button/action-button.jsx b/website/src/repl/components/button/action-button.jsx index eb395212c..7364d2af0 100644 --- a/website/src/repl/components/button/action-button.jsx +++ b/website/src/repl/components/button/action-button.jsx @@ -8,27 +8,36 @@ export function ActionButton({ children, label, labelIsHidden, className, ...but ); } -export function ActionInput({ label, className, ...inputProps }) { + +export function SpecialActionButton(props) { + const { className, ...buttonProps } = props; + return ( -