From 7f103e77e3a28ba325b2c94a12e795b17b6077c7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 28 Jul 2025 04:05:21 +0200 Subject: [PATCH 001/110] prefix "S" for solo --- packages/core/repl.mjs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 47231af4d..47489054e 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -138,9 +138,9 @@ export function repl({ // allows muting a pattern x with x_ or _x return silence; } - if (id === '$') { + if (id.includes('$')) { // allows adding anonymous patterns with $: - id = `$${anonymousIndex}`; + id = `${id}${anonymousIndex}`; anonymousIndex++; } pPatterns[id] = this; @@ -201,6 +201,13 @@ export function repl({ let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); if (Object.keys(pPatterns).length) { let patterns = Object.values(pPatterns); + + // if there are solo patterns, only use those + const soloPatterns = Object.entries(pPatterns).filter(([key]) => key.length > 1 && key.startsWith('S')); + if (soloPatterns.length) { + patterns = Object.values(Object.fromEntries(soloPatterns)); + } + if (eachTransform) { // Explicit lambda so only element (not index and array) are passed patterns = patterns.map((x) => eachTransform(x)); From c9381a11f9ba182ac4ae8e5aab3584883003125c Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 3 Oct 2025 17:21:05 -0500 Subject: [PATCH 002/110] 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 003/110] 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 004/110] 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 cc4452ce58a3d2a3fd12fe37166d7d2047222d58 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 4 Oct 2025 20:33:25 -0500 Subject: [PATCH 005/110] Working version of partials and phases --- packages/core/controls.mjs | 1 + packages/core/signal.mjs | 38 ++++++++++++++++- packages/core/test/pattern.test.mjs | 4 +- packages/superdough/synth.mjs | 52 +++++++++++++++++------- website/src/components/Header/Search.css | 4 +- website/src/pages/learn/samples.mdx | 2 +- 6 files changed, 79 insertions(+), 22 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 7e3c2a8e2..c8005927d 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2040,6 +2040,7 @@ export const { real } = registerControl('real'); export const { imag } = registerControl('imag'); export const { enhance } = registerControl('enhance'); export const { partials } = registerControl('partials'); +export const { phases } = registerControl('phases'); export const { comb } = registerControl('comb'); export const { smear } = registerControl('smear'); export const { scram } = registerControl('scram'); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 6bd6fde67..b4dc825fc 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -228,7 +228,7 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n); export const run = (n) => saw.range(0, n).round().segment(n); /** - * Creates a pattern from a binary number. + * Creates a binary pattern from a number. * * @name binary * @param {number} n - input number to convert to binary @@ -242,7 +242,7 @@ export const binary = (n) => { }; /** - * Creates a pattern from a binary number, padded to n bits long. + * Creates a binary pattern from a number, padded to n bits long. * * @name binaryN * @param {number} n - input number to convert to binary @@ -258,6 +258,40 @@ export const binaryN = (n, nBits = 16) => { return reify(n).segment(nBits).brshift(bitPos).band(pure(1)); }; +/** + * Creates a binary list pattern from a number. + * + * @name binaryL + * @param {number} n - input number to convert to binary + */ +export const binaryL = (n) => { + const nBits = reify(n).log2(0).floor().add(1); + return binaryNL(n, nBits); +}; + +/** + * Creates a binary list pattern from a number, padded to n bits long. + * + * @name binaryNL + * @param {number} n - input number to convert to binary + * @param {number} nBits - pattern length, defaults to 16 + */ +export const binaryNL = (n, nBits = 16) => { + return reify(n) + .withValue((v) => (bits) => { + const bList = []; + for (let i = bits - 1; i >= 0; i--) { + bList.push((v >> i) & 1); + } + return bList; + }) + .appLeft(reify(nBits)); +}; + +export const randL = (n) => { + return signal((t) => (nVal) => timeToRands(t, nVal).map(Math.abs)).appLeft(reify(n)); +}; + export const randrun = (n) => { return signal((t) => { // Without adding 0.5, the first cycle is always 0,1,2,3,... diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 1df5c8776..625f40756 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -796,7 +796,7 @@ describe('Pattern', () => { }); }); describe('apply', () => { - it('Can apply a function', () => { + (it('Can apply a function', () => { expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle()); }), it('Can apply a pattern of functions', () => { @@ -804,7 +804,7 @@ describe('Pattern', () => { expect(sequence('a', 'b').apply(fast(2), fast(3)).firstCycle()).toStrictEqual( sequence('a', 'b').fast(2, 3).firstCycle(), ); - }); + })); }); describe('layer', () => { it('Can layer up multiple functions', () => { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 5dc5dc08a..71c4a15dc 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -14,9 +14,10 @@ import { noises, webAudioTimeout, } from './helpers.mjs'; +import { logger } from './logger.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; -const waveforms = ['triangle', 'square', 'sawtooth', 'sine']; +const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user']; const waveformAliases = [ ['tri', 'triangle'], ['sqr', 'square'], @@ -413,9 +414,13 @@ export function registerSynthSounds() { waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] })); } -export function waveformN(partials, type) { - const real = new Float32Array(partials + 1); - const imag = new Float32Array(partials + 1); +const PI2 = 2 * Math.PI; +export function waveformN(partials, phases, type) { + const isList = typeof partials === 'object'; + partials = isList ? partials : new Float32Array(partials).fill(1); + const len = partials.length; + const real = new Float32Array(len + 1); + const imag = new Float32Array(len + 1); const ac = getAudioContext(); const osc = ac.createOscillator(); @@ -423,20 +428,29 @@ export function waveformN(partials, type) { sawtooth: (n) => [0, -1 / n], square: (n) => [0, n % 2 === 0 ? 0 : 1 / n], triangle: (n) => [n % 2 === 0 ? 0 : 1 / (n * n), 0], + user: (_n) => [0, 1], }; if (!terms[type]) { throw new Error(`unknown wave type ${type}`); } - real[0] = 0; // dc offset - imag[0] = 0; - let n = 1; - while (n <= partials) { - const [r, i] = terms[type](n); - real[n] = r; - imag[n] = i; - n++; + for (let n = 0; n < len; n++) { + const mag = partials[n]; + const [r, i] = terms[type](n + 1); // we skip n === 0 as this is dc offset + const phase = phases?.[n] ?? 0; + // Scale by `partials` + let R = r * mag; + let I = i * mag; + // Apply rotation by the phase + if (phase !== 0) { + const c = Math.cos(PI2 * phase); + const s = Math.sin(PI2 * phase); + R = c * R - s * I; + I = s * R + c * I; + } + real[n + 1] = R; + imag[n + 1] = I; } const wave = ac.createPeriodicWave(real, imag); @@ -446,16 +460,24 @@ export function waveformN(partials, type) { // expects one of waveforms as s export function getOscillator(s, t, value) { - let { n: partials, duration, noise = 0 } = value; + const { duration, noise = 0 } = value; + const partials = value.partials ?? value.n; let o; + if (s === 'user' && !partials) { + logger( + `[superdough] Synth 'user' was selected, but partials not specified. Defaulting to triangle. Use pat.partials to setup custom waveform`, + ); + s = 'triangle'; + } + s = s === 'user' && !partials ? 'triangle' : s; // If no partials are given, use stock waveforms - if (!partials || s === 'sine') { + if (!partials || !partials?.length || s === 'sine') { o = getAudioContext().createOscillator(); o.type = s || 'triangle'; } // generate custom waveform if partials are given else { - o = waveformN(partials, s); + o = waveformN(partials, value.phases, s); } // set frequency o.frequency.value = getFrequencyFromValue(value); diff --git a/website/src/components/Header/Search.css b/website/src/components/Header/Search.css index 456ef9f6b..b14146cbd 100644 --- a/website/src/components/Header/Search.css +++ b/website/src/components/Header/Search.css @@ -18,8 +18,8 @@ --docsearch-modal-background: var(--background); --docsearch-muted-color: color-mix(in srgb, var(--foreground), #fff 30%); --docsearch-key-gradient: var(--foreground); - --docsearch-key-shadow: inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), - 0 1px 2px 1px var(--gutterBackground); + --docsearch-key-shadow: + inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), 0 1px 2px 1px var(--gutterBackground); } .dark { --docsearch-muted-color: color-mix(in srgb, var(--foreground), #000 30%); diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index eb79cccf7..201d57dfa 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -381,7 +381,7 @@ Sampler effects are functions that can be used to change the behaviour of sample ### scrub -{' '} + ### speed From 0fe7edc15756919b73564ba84f54695d9f2fcb67 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 15 Oct 2025 14:53:07 -0500 Subject: [PATCH 006/110] Some examples --- packages/core/controls.mjs | 30 ++- packages/core/signal.mjs | 11 ++ test/__snapshots__/examples.test.mjs.snap | 212 ++++++++++++++++++++++ 3 files changed, 251 insertions(+), 2 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 30b3c14f3..327f8df51 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2072,8 +2072,6 @@ export const { tsdelay } = registerControl('tsdelay'); export const { real } = registerControl('real'); export const { imag } = registerControl('imag'); export const { enhance } = registerControl('enhance'); -export const { partials } = registerControl('partials'); -export const { phases } = registerControl('phases'); export const { comb } = registerControl('comb'); export const { smear } = registerControl('smear'); export const { scram } = registerControl('scram'); @@ -2375,3 +2373,31 @@ export const scrub = register( }, false, ); + +/** + * Scale the magnitude of the harmonics of one of the core synths ('sine', 'tri', 'saw', ..) + * + * Can also be used to create a new synth via `s('user').partials(...)` + * + * @name partials + * @param {number[] | Pattern} partials List of magnitudes for partials. 0th entry is the first harmonic (i.e. DC offset is skipped) + * @example + * s("user").seg(16).n(irand(8)).scale("A:major") + * .partials([1, 0, 1, 0, 0, 1]) + * @example + * s("saw").seg(8).n(irand(12)).scale("G#:minor") + * .partials(binaryL(256)) + */ +export const { partials } = registerControl('partials'); + +/** + * Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases + * + * @name phases + * @param {number[] | Pattern} phases List of phases for partials. 0th entry is the first phase (i.e. DC offset is skipped) + * @example + * s("saw").seg(8).n(irand(12)).scale("G#:minor") + * .partials(binaryL(256)) + * .phases(randL(20)) + */ +export const { phases } = registerControl('phases'); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index b4dc825fc..604d1c805 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -263,6 +263,8 @@ export const binaryN = (n, nBits = 16) => { * * @name binaryL * @param {number} n - input number to convert to binary + * s("saw").seg(8) + * .partials(binaryL(irand(4096).add(1))) */ export const binaryL = (n) => { const nBits = reify(n).log2(0).floor().add(1); @@ -288,6 +290,15 @@ export const binaryNL = (n, nBits = 16) => { .appLeft(reify(nBits)); }; +/** + * Creates a list of random numbers of the given length + * + * @name randL + * @param {number} n Number of random numbers to sample + * @example + * s("saw").seg(16).n(irand(12)).scale("F1:minor") + * .partials(randL(8)) + */ export const randL = (n) => { return signal((t) => (nVal) => timeToRands(t, nVal).map(Math.abs)).appLeft(reify(n)); }; diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 937d42797..17c25cc5b 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -7078,6 +7078,112 @@ exports[`runs examples > example "panchor" example index 0 1`] = ` ] `; +exports[`runs examples > example "partials" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 1/16 → 1/8 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 1/8 → 3/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 3/16 → 1/4 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 1/4 → 5/16 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 5/16 → 3/8 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 3/8 → 7/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 7/16 → 1/2 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 1/2 → 9/16 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 9/16 → 5/8 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 5/8 → 11/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 11/16 → 3/4 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 3/4 → 13/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 13/16 → 7/8 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 7/8 → 15/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 15/16 → 1/1 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 1/1 → 17/16 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 17/16 → 9/8 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 9/8 → 19/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 19/16 → 5/4 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 5/4 → 21/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 21/16 → 11/8 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 11/8 → 23/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 23/16 → 3/2 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 3/2 → 25/16 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 25/16 → 13/8 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 13/8 → 27/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 27/16 → 7/4 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 7/4 → 29/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 29/16 → 15/8 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 15/8 → 31/16 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 31/16 → 2/1 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 2/1 → 33/16 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 33/16 → 17/8 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 17/8 → 35/16 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 35/16 → 9/4 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 9/4 → 37/16 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 37/16 → 19/8 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 19/8 → 39/16 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 39/16 → 5/2 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 5/2 → 41/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 41/16 → 21/8 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 21/8 → 43/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 43/16 → 11/4 | s:user note:E4 partials:[1 0 1 0 0 1] ]", + "[ 11/4 → 45/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 45/16 → 23/8 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 23/8 → 47/16 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 47/16 → 3/1 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 3/1 → 49/16 | s:user note:B3 partials:[1 0 1 0 0 1] ]", + "[ 49/16 → 25/8 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 25/8 → 51/16 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 51/16 → 13/4 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 13/4 → 53/16 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 53/16 → 27/8 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", + "[ 27/8 → 55/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 55/16 → 7/2 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 7/2 → 57/16 | s:user note:D4 partials:[1 0 1 0 0 1] ]", + "[ 57/16 → 29/8 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 29/8 → 59/16 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 59/16 → 15/4 | s:user note:A4 partials:[1 0 1 0 0 1] ]", + "[ 15/4 → 61/16 | s:user note:G#4 partials:[1 0 1 0 0 1] ]", + "[ 61/16 → 31/8 | s:user note:A3 partials:[1 0 1 0 0 1] ]", + "[ 31/8 → 63/16 | s:user note:F#4 partials:[1 0 1 0 0 1] ]", + "[ 63/16 → 4/1 | s:user note:C#4 partials:[1 0 1 0 0 1] ]", +] +`; + +exports[`runs examples > example "partials" example index 1 1`] = ` +[ + "[ 0/1 → 1/8 | s:saw note:G#3 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 1/8 → 1/4 | s:saw note:A#4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 1/4 → 3/8 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 3/8 → 1/2 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 1/2 → 5/8 | s:saw note:C#4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 5/8 → 3/4 | s:saw note:A#3 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 3/4 → 7/8 | s:saw note:B3 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 7/8 → 1/1 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 1/1 → 9/8 | s:saw note:F#4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 9/8 → 5/4 | s:saw note:A#4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 5/4 → 11/8 | s:saw note:A#4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 11/8 → 3/2 | s:saw note:B3 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 3/2 → 13/8 | s:saw note:G#4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 13/8 → 7/4 | s:saw note:E4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 7/4 → 15/8 | s:saw note:B3 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 15/8 → 2/1 | s:saw note:G#4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 2/1 → 17/8 | s:saw note:D#5 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 17/8 → 9/4 | s:saw note:C#5 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 9/4 → 19/8 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 19/8 → 5/2 | s:saw note:C#5 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 5/2 → 21/8 | s:saw note:E4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 21/8 → 11/4 | s:saw note:A#3 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 11/4 → 23/8 | s:saw note:G#4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 23/8 → 3/1 | s:saw note:G#3 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 3/1 → 25/8 | s:saw note:B3 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 25/8 → 13/4 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 13/4 → 27/8 | s:saw note:D#5 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 27/8 → 7/2 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 7/2 → 29/8 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 29/8 → 15/4 | s:saw note:D#5 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 15/4 → 31/8 | s:saw note:B4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 31/8 → 4/1 | s:saw note:A#4 partials:[1 0 0 0 0 0 0 0 0] ]", +] +`; + exports[`runs examples > example "pattack" example index 0 1`] = ` [ "[ 0/1 → 1/2 | note:c pattack:0 ]", @@ -7331,6 +7437,43 @@ exports[`runs examples > example "phasersweep" example index 0 1`] = ` ] `; +exports[`runs examples > example "phases" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:saw note:G#3 partials:[1 0 0 0 0 0 0 0 0] phases:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] ]", + "[ 1/8 → 1/4 | s:saw note:A#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.6852155700325966 0.17723728902637959 0.8521428182721138 0.5022399611771107 0.9780306946486235 0.3186125475913286 0.1851645242422819 0.8495976086705923 0.9832699615508318 0.8203876558691263 0.7365445382893085 0.6727468091994524 0.4794053863734007 0.35482912696897984 0.5666771996766329 0.4527092967182398 0.5699347071349621 0.7358296867460012 0.5444891396909952 0.11270620673894882] ]", + "[ 1/4 → 3/8 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.36975969187915325 0.18273563869297504 0.012781793251633644 0.5423613861203194 0.12467948533594608 0.7188410349190235 0.856887087225914 0.8818203993141651 0.8917219173163176 0.110306391492486 0.07148400321602821 0.8493648078292608 0.9100127145648003 0.4820226952433586 0.037094997242093086 0.9857278112322092 0.23439379036426544 0.46701666340231895 0.15832693874835968 0.6921216659247875] ]", + "[ 3/8 → 1/2 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.40139251574873924 0.15799915604293346 0.9604704882949591 0.03873417526483536 0.4157217647880316 0.27756719291210175 0.8800551909953356 0.5692523363977671 0.11026228219270706 0.7190891150385141 0.404962956905365 0.50828124769032 0.9681975357234478 0.34809120930731297 0.9345223866403103 0.9851128943264484 0.11768617853522301 0.538263589143753 0.6650313660502434 0.02540053054690361] ]", + "[ 1/2 → 5/8 | s:saw note:C#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.2604806162416935 0.3654713351279497 0.9900747090578079 0.22572625242173672 0.9328999016433954 0.5273839123547077 0.7047769222408533 0.7311522178351879 0.42673745937645435 0.7099553178995848 0.5597201306372881 0.8968746215105057 0.15695763938128948 0.3792166206985712 0.4147114269435406 0.38739512488245964 0.7585489805787802 0.3875726908445358 0.867314238101244 0.1866922564804554] ]", + "[ 5/8 → 3/4 | s:saw note:A#3 partials:[1 0 0 0 0 0 0 0 0] phases:[0.1356358677148819 0.5403102282434702 0.07321739941835403 0.9646544624119997 0.7707359045743942 0.46888123638927937 0.9489036612212658 0.366748945787549 0.5029341224581003 0.7194344792515039 0.282692551612854 0.7303404528647661 0.7155798356980085 0.637981578707695 0.39654020965099335 0.008769871667027473 0.5857728160917759 0.3513293471187353 0.43080931156873703 0.518209295347333] ]", + "[ 3/4 → 7/8 | s:saw note:B3 partials:[1 0 0 0 0 0 0 0 0] phases:[0.19582648016512394 0.0024610888212919235 0.08272589556872845 0.09592138417065144 0.9678201265633106 0.3249557167291641 0.9463537875562906 0.20766610652208328 0.19126702845096588 0.4482936095446348 0.23314787074923515 0.07724925130605698 0.8329483829438686 0.39804019033908844 0.2529231123626232 0.889951054006815 0.716364661231637 0.3905949704349041 0.990752438083291 0.275751456618309] ]", + "[ 7/8 → 1/1 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.3976310808211565 0.13476407527923584 0.963376859202981 0.6579397786408663 0.5203975513577461 0.865439185872674 0.5583905670791864 0.2736878804862499 0.17021040990948677 0.3456706069409847 0.5848060417920351 0.9889458417892456 0.9307208992540836 0.7148868907243013 0.7419579364359379 0.8103639334440231 0.3331365995109081 0.19130694679915905 0.03261224366724491 0.808204049244523] ]", + "[ 1/1 → 9/8 | s:saw note:F#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.5195421651005745 0.9349692352116108 0.1718774326145649 0.9601866770535707 0.2802200373262167 0.8732441551983356 0.01810968853533268 0.42737805284559727 0.26281314715743065 0.7521175239235163 0.568607984110713 0.5046361442655325 0.03172125294804573 0.3706745784729719 0.9402780849486589 0.3995086643844843 0.35769628174602985 0.023460431024432182 0.043030962347984314 0.6526827793568373] ]", + "[ 9/8 → 5/4 | s:saw note:A#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.6724895145744085 0.21410975232720375 0.5229047331959009 0.20957534946501255 0.7402758821845055 0.2681974694132805 0.4059371296316385 0.9422509074211121 0.6584139950573444 0.26589646376669407 0.16059227287769318 0.17166719026863575 0.6234225388616323 0.020276052877306938 0.4836352560669184 0.8991366866976023 0.1860280428081751 0.06238717399537563 0.4383583478629589 0.4902789145708084] ]", + "[ 5/4 → 11/8 | s:saw note:A#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.7287282031029463 0.9341024849563837 0.5098182689398527 0.5608645845204592 0.6812942810356617 0.5015129633247852 0.4602120481431484 0.927369873970747 0.19313151575624943 0.5240397155284882 0.444337897002697 0.19362097792327404 0.7068767864257097 0.920799495652318 0.2648108974099159 0.9700228478759527 0.20331068895757198 0.38390261493623257 0.0038731005042791367 0.9156702514737844] ]", + "[ 11/8 → 3/2 | s:saw note:B3 partials:[1 0 0 0 0 0 0 0 0] phases:[0.18280375190079212 0.40527783520519733 0.30504362285137177 0.7622128054499626 0.842598931863904 0.053796686232089996 0.5124214049428701 0.9178454764187336 0.09979427233338356 0.8280061967670918 0.5572535842657089 0.0622002687305212 0.9205668028444052 0.7738517206162214 0.04644870012998581 0.2117986585944891 0.26436166651546955 0.26421429589390755 0.4649084359407425 0.10490566119551659] ]", + "[ 3/2 → 13/8 | s:saw note:G#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.6084080748260021 0.4424846563488245 0.755185678601265 0.04328125901520252 0.04244415462017059 0.3994515985250473 0.787989066913724 0.3180440980941057 0.30072982981801033 0.24217353947460651 0.7830625418573618 0.7733921892940998 0.7720277719199657 0.4402343425899744 0.21046430803835392 0.38635879568755627 0.5671729445457458 0.9510521050542593 0.010756833478808403 0.5728995501995087] ]", + "[ 13/8 → 7/4 | s:saw note:E4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.49675471149384975 0.2965749856084585 0.09848833456635475 0.19172007404267788 0.22677889838814735 0.9841996673494577 0.11156083643436432 0.6079028844833374 0.9412728808820248 0.9376902114599943 0.3611182738095522 0.5901201106607914 0.26860327646136284 0.9782364591956139 0.18694544956088066 0.004641396924853325 0.42020696587860584 0.06209231913089752 0.4790260009467602 0.21088780276477337] ]", + "[ 7/4 → 15/8 | s:saw note:B3 partials:[1 0 0 0 0 0 0 0 0] phases:[0.20473789609968662 0.7480021081864834 0.30757393687963486 0.5827204789966345 0.22094514779746532 0.6370698790997267 0.16573002003133297 0.0638319905847311 0.12625465169548988 0.4565841108560562 0.46095744147896767 0.646710304543376 0.045173922553658485 0.4833248760551214 0.1326297614723444 0.5275116711854935 0.9871038906276226 0.424171743914485 0.761672617867589 0.08582597225904465] ]", + "[ 15/8 → 2/1 | s:saw note:G#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.5945571791380644 0.3962489552795887 0.563431927934289 0.4742323160171509 0.4185752831399441 0.9703940469771624 0.5282467231154442 0.6896266285330057 0.6941124945878983 0.2423656489700079 0.225077323615551 0.5445358455181122 0.014737963676452637 0.2848400343209505 0.14955217391252518 0.9782383926212788 0.6327074971050024 0.23327310755848885 0.4324392694979906 0.4326172359287739] ]", + "[ 2/1 → 17/8 | s:saw note:D#5 partials:[1 0 0 0 0 0 0 0 0] phases:[0.9595271199941635 0.5427457969635725 0.45864437520504 0.8057199101895094 0.18388565629720688 0.17221237532794476 0.8838487900793552 0.4976818822324276 0.7543560564517975 0.34416236728429794 0.8638174068182707 0.5697487238794565 0.005243342369794846 0.6361143626272678 0.6039986703544855 0.38708674535155296 0.5463927835226059 0.5717255529016256 0.15141930803656578 0.22688637301325798] ]", + "[ 17/8 → 9/4 | s:saw note:C#5 partials:[1 0 0 0 0 0 0 0 0] phases:[0.9033902939409018 0.22919894196093082 0.8388008493930101 0.9108077362179756 0.32246968522667885 0.16122018359601498 0.31932324543595314 0.18524732999503613 0.19110161997377872 0.5346284378319979 0.38531880639493465 0.7401201985776424 0.07450124807655811 0.18417961709201336 0.5768439751118422 0.7161206863820553 0.5836263168603182 0.9591280855238438 0.7728104889392853 0.0006709620356559753] ]", + "[ 9/4 → 19/8 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.34548251144587994 0.30419893004000187 0.10205957666039467 0.9906709585338831 0.3156223688274622 0.3745057098567486 0.7668170686811209 0.9379972033202648 0.689277408644557 0.8690325897186995 0.36728161945939064 0.9668608456850052 0.7341883443295956 0.32575040124356747 0.8095720671117306 0.3182998225092888 0.38158727809786797 0.5534052699804306 0.5973556581884623 0.752589326351881] ]", + "[ 19/8 → 5/2 | s:saw note:C#5 partials:[1 0 0 0 0 0 0 0 0] phases:[0.8365066405385733 0.5369812995195389 0.2263860274106264 0.21759207546710968 0.2172116208821535 0.09454222768545151 0.8075542915612459 0.9017798062413931 0.46291174553334713 0.01372767798602581 0.3509599883109331 0.4871185813099146 0.3525365497916937 0.6883120182901621 0.19577431678771973 0.287217665463686 0.025726469233632088 0.11798650585114956 0.8464976660907269 0.981348654255271] ]", + "[ 5/2 → 21/8 | s:saw note:E4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.45861607417464256 0.6807645913213491 0.07009582966566086 0.1450389064848423 0.2579921595752239 0.612881500273943 0.5503941811621189 0.7027011960744858 0.681745121255517 0.24246043153107166 0.2981361001729965 0.02195165678858757 0.10680225491523743 0.9413154497742653 0.6108828671276569 0.9479686040431261 0.17599895782768726 0.6629563421010971 0.5875700414180756 0.05703482776880264] ]", + "[ 21/8 → 11/4 | s:saw note:A#3 partials:[1 0 0 0 0 0 0 0 0] phases:[0.16019348427653313 0.47758268006145954 0.9749126061797142 0.8025561925023794 0.3600110989063978 0.324309878051281 0.5655391272157431 0.13364790193736553 0.3045873437076807 0.3814875278621912 0.14350778982043266 0.6017840709537268 0.11792952008545399 0.7185723781585693 0.6789924055337906 0.43416675738990307 0.9378792773932219 0.48162082210183144 0.37555896304547787 0.4686833508312702] ]", + "[ 11/4 → 23/8 | s:saw note:G#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.6332327704876661 0.7342763151973486 0.16935866326093674 0.4754706546664238 0.528122790157795 0.21934260986745358 0.7476693484932184 0.2995396424084902 0.46688378043472767 0.9741295631974936 0.9878341723233461 0.9837898630648851 0.15651432052254677 0.2896903567016125 0.7395600061863661 0.3490046579390764 0.014511989429593086 0.6857758145779371 0.7405510656535625 0.4224672969430685] ]", + "[ 23/8 → 3/1 | s:saw note:G#3 partials:[1 0 0 0 0 0 0 0 0] phases:[0.01625417172908783 0.28409438766539097 0.3075305689126253 0.40343056432902813 0.5556836389005184 0.6926821582019329 0.5635769125074148 0.017873913049697876 0.5601800158619881 0.28681862354278564 0.8022358678281307 0.13467839546501637 0.02351163513958454 0.1987263821065426 0.4515750799328089 0.0009761657565832138 0.11555273085832596 0.25053937546908855 0.29571316950023174 0.15751986764371395] ]", + "[ 3/1 → 25/8 | s:saw note:B3 partials:[1 0 0 0 0 0 0 0 0] phases:[0.21728911064565182 0.7605195846408606 0.11240843310952187 0.4372697602957487 0.15630115941166878 0.1653979979455471 0.5303416550159454 0.07218753732740879 0.47459222190082073 0.04051801189780235 0.8742353077977896 0.9356274232268333 0.6662059463560581 0.4364917427301407 0.4163493011146784 0.9820694588124752 0.09098831936717033 0.41974459774792194 0.05833998881280422 0.2601191997528076] ]", + "[ 25/8 → 13/4 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.34324387833476067 0.14098095521330833 0.012934491038322449 0.827436288818717 0.5283997394144535 0.8942463714629412 0.4924008697271347 0.9298017006367445 0.18076439201831818 0.5243716649711132 0.015810951590538025 0.7476964127272367 0.6734520178288221 0.3267945274710655 0.9760967157781124 0.9397075213491917 0.7225867230445147 0.938586825504899 0.9692913070321083 0.5398030783981085] ]", + "[ 13/4 → 27/8 | s:saw note:D#5 partials:[1 0 0 0 0 0 0 0 0] phases:[0.9923650715500116 0.7797339502722025 0.1992435697466135 0.9005183521658182 0.718992218375206 0.23745290748775005 0.28872333094477654 0.35448253713548183 0.6209003105759621 0.3761858306825161 0.019171399995684624 0.7563913837075233 0.1559751983731985 0.4236980527639389 0.08094430714845657 0.4560011047869921 0.5958948992192745 0.37061405926942825 0.18633292242884636 0.346891064196825] ]", + "[ 27/8 → 7/2 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.40869180113077164 0.4018078800290823 0.34696186147630215 0.9572948440909386 0.400035472586751 0.3531211093068123 0.7240961641073227 0.8125612027943134 0.3587487991899252 0.1545814611017704 0.8934940584003925 0.9094721674919128 0.2336172368377447 0.17333386465907097 0.227950319647789 0.12384821102023125 0.5896956156939268 0.3293947223573923 0.619540523737669 0.8746719229966402] ]", + "[ 7/2 → 29/8 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.40994881466031075 0.6455810181796551 0.35704658553004265 0.2732649203389883 0.31587288342416286 0.23243548162281513 0.22569947130978107 0.17241661623120308 0.7624858524650335 0.02111036144196987 0.6966175157576799 0.32112877257168293 0.2512516509741545 0.08602019399404526 0.16260642930865288 0.2627844326198101 0.1954369619488716 0.16302869468927383 0.4046020060777664 0.0904023926705122] ]", + "[ 29/8 → 15/4 | s:saw note:D#5 partials:[1 0 0 0 0 0 0 0 0] phases:[0.9391485787928104 0.3380728308111429 0.7723016366362572 0.998674126341939 0.7193406894803047 0.6963680125772953 0.9260678570717573 0.7829763628542423 0.42278125509619713 0.913721015676856 0.38873230293393135 0.7281809840351343 0.952108234167099 0.649707704782486 0.9456792045384645 0.4331315904855728 0.63712502643466 0.9180345926433802 0.7379776351153851 0.7914005517959595] ]", + "[ 15/4 → 31/8 | s:saw note:B4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.8121673222631216 0.25548945367336273 0.5759696904569864 0.6668333616107702 0.9233786761760712 0.11143362522125244 0.9734930321574211 0.369325939565897 0.6248745918273926 0.719582824036479 0.24051696807146072 0.20336504466831684 0.6860735435038805 0.10649923048913479 0.8736641593277454 0.042930178344249725 0.09769343212246895 0.7966452036052942 0.5882443580776453 0.8942952174693346] ]", + "[ 31/8 → 4/1 | s:saw note:A#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.7361665386706591 0.7657648548483849 0.21474426984786987 0.8228576295077801 0.5647660344839096 0.08148551359772682 0.9714624118059874 0.13992334716022015 0.9609981551766396 0.6844500284641981 0.23610286228358746 0.5974335502833128 0.20678778178989887 0.5261937081813812 0.7417268306016922 0.9810918122529984 0.8138748407363892 0.9389897901564837 0.7420910261571407 0.48839940316975117] ]", +] +`; + exports[`runs examples > example "pianoroll" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]", @@ -8052,6 +8195,75 @@ exports[`runs examples > example "rand" example index 0 1`] = ` ] `; +exports[`runs examples > example "randL" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:saw note:F1 partials:[0 0 0 0 0 0 0 0] ]", + "[ 1/16 → 1/8 | s:saw note:Bb2 partials:[0.8426077850162983 0.0886186733841896 0.9342893119901419 0.06848422810435295 0.6248505394905806 0.5372848063707352 0.8283301629126072 0.7969411127269268] ]", + "[ 1/8 → 3/16 | s:saw note:G2 partials:[0.6852155700325966 0.17723728902637959 0.8521428182721138 0.5022399611771107 0.9780306946486235 0.3186125475913286 0.1851645242422819 0.8495976086705923] ]", + "[ 3/16 → 1/4 | s:saw note:Ab1 partials:[0.20066574029624462 0.17195586115121841 0.2159541044384241 0.17005567252635956 0.6841662060469389 0.07906394638121128 0.004815371707081795 0.027107616886496544] ]", + "[ 1/4 → 5/16 | s:saw note:C2 partials:[0.36975969187915325 0.18273563869297504 0.012781793251633644 0.5423613861203194 0.12467948533594608 0.7188410349190235 0.856887087225914 0.8818203993141651] ]", + "[ 5/16 → 3/8 | s:saw note:Eb2 partials:[0.5675661638379097 0.15932588279247284 0.31376867927610874 0.32359412126243114 0.3281281068921089 0.011714376509189606 0.4433113746345043 0.68459103256464] ]", + "[ 3/8 → 7/16 | s:saw note:C2 partials:[0.40139251574873924 0.15799915604293346 0.9604704882949591 0.03873417526483536 0.4157217647880316 0.27756719291210175 0.8800551909953356 0.5692523363977671] ]", + "[ 7/16 → 1/2 | s:saw note:Bb1 partials:[0.3015887886285782 0.22311350144445896 0.47645803540945053 0.001545613631606102 0.53841289319098 0.330710094422102 0.2747743520885706 0.9471044968813658] ]", + "[ 1/2 → 9/16 | s:saw note:Bb1 partials:[0.2604806162416935 0.3654713351279497 0.9900747090578079 0.22572625242173672 0.9328999016433954 0.5273839123547077 0.7047769222408533 0.7311522178351879] ]", + "[ 9/16 → 5/8 | s:saw note:G1 partials:[0.16399178467690945 0.9559339117258787 0.21830322034657001 0.4903192054480314 0.2504696864634752 0.7302105724811554 0.6580934692174196 0.9855979979038239] ]", + "[ 5/8 → 11/16 | s:saw note:G1 partials:[0.1356358677148819 0.5403102282434702 0.07321739941835403 0.9646544624119997 0.7707359045743942 0.46888123638927937 0.9489036612212658 0.366748945787549] ]", + "[ 11/16 → 3/4 | s:saw note:C2 partials:[0.408811716362834 0.9613851886242628 0.7683626655489206 0.5862949229776859 0.9568102769553661 0.7154356613755226 0.6283384170383215 0.2193678840994835] ]", + "[ 3/4 → 13/16 | s:saw note:Ab1 partials:[0.19582648016512394 0.0024610888212919235 0.08272589556872845 0.09592138417065144 0.9678201265633106 0.3249557167291641 0.9463537875562906 0.20766610652208328] ]", + "[ 13/16 → 7/8 | s:saw note:Ab2 partials:[0.7516226731240749 0.15610851533710957 0.9039078876376152 0.8602268267422915 0.928601048886776 0.4479671400040388 0.5080656576901674 0.562442347407341] ]", + "[ 7/8 → 15/16 | s:saw note:C2 partials:[0.3976310808211565 0.13476407527923584 0.963376859202981 0.6579397786408663 0.5203975513577461 0.865439185872674 0.5583905670791864 0.2736878804862499] ]", + "[ 15/16 → 1/1 | s:saw note:G2 partials:[0.7029578909277916 0.6351367030292749 0.26143294386565685 0.6411824338138103 0.5676082745194435 0.4803342465311289 0.5140306428074837 0.9477859679609537] ]", + "[ 1/1 → 17/16 | s:saw note:Eb2 partials:[0.5195421651005745 0.9349692352116108 0.1718774326145649 0.9601866770535707 0.2802200373262167 0.8732441551983356 0.01810968853533268 0.42737805284559727] ]", + "[ 17/16 → 9/8 | s:saw note:Eb2 partials:[0.5486328881233931 0.7088070474565029 0.07877279818058014 0.9422610364854336 0.7696988433599472 0.8289324026554823 0.5925844628363848 0.3920764606446028] ]", + "[ 9/8 → 19/16 | s:saw note:G2 partials:[0.6724895145744085 0.21410975232720375 0.5229047331959009 0.20957534946501255 0.7402758821845055 0.2681974694132805 0.4059371296316385 0.9422509074211121] ]", + "[ 19/16 → 5/4 | s:saw note:F1 partials:[0.08174665085971355 0.2762963864952326 0.5428560189902782 0.29452037811279297 0.17946280725300312 0.366960596293211 0.26327736489474773 0.5002023074775934] ]", + "[ 5/4 → 21/16 | s:saw note:G2 partials:[0.7287282031029463 0.9341024849563837 0.5098182689398527 0.5608645845204592 0.6812942810356617 0.5015129633247852 0.4602120481431484 0.927369873970747] ]", + "[ 21/16 → 11/8 | s:saw note:F1 partials:[0.08104278706014156 0.5200720727443695 0.5363391060382128 0.9824271816760302 0.10652091167867184 0.23473932035267353 0.7656102329492569 0.4850195776671171] ]", + "[ 11/8 → 23/16 | s:saw note:Ab1 partials:[0.18280375190079212 0.40527783520519733 0.30504362285137177 0.7622128054499626 0.842598931863904 0.053796686232089996 0.5124214049428701 0.9178454764187336] ]", + "[ 23/16 → 3/2 | s:saw note:Eb2 partials:[0.5081270858645439 0.1421387754380703 0.8852288406342268 0.9066353775560856 0.7700740322470665 0.35437702015042305 0.20032598450779915 0.963155921548605] ]", + "[ 3/2 → 25/16 | s:saw note:F2 partials:[0.6084080748260021 0.4424846563488245 0.755185678601265 0.04328125901520252 0.04244415462017059 0.3994515985250473 0.787989066913724 0.3180440980941057] ]", + "[ 25/16 → 13/8 | s:saw note:Ab1 partials:[0.17092766426503658 0.7808954659849405 0.8234915658831596 0.8643151633441448 0.9781719036400318 0.5102015696465969 0.018259897828102112 0.25844234600663185] ]", + "[ 13/8 → 27/16 | s:saw note:Db2 partials:[0.49675471149384975 0.2965749856084585 0.09848833456635475 0.19172007404267788 0.22677889838814735 0.9841996673494577 0.11156083643436432 0.6079028844833374] ]", + "[ 27/16 → 7/4 | s:saw note:Bb1 partials:[0.29589061066508293 0.13011129200458527 0.8021425940096378 0.38040103763341904 0.33274981752038 0.29745868407189846 0.9898250997066498 0.743482418358326] ]", + "[ 7/4 → 29/16 | s:saw note:Ab1 partials:[0.20473789609968662 0.7480021081864834 0.30757393687963486 0.5827204789966345 0.22094514779746532 0.6370698790997267 0.16573002003133297 0.0638319905847311] ]", + "[ 29/16 → 15/8 | s:saw note:Db2 partials:[0.4695742893964052 0.6690364442765713 0.6216684486716986 0.41342394426465034 0.9508822970092297 0.8403679896146059 0.91759910620749 0.6591395419090986] ]", + "[ 15/8 → 31/16 | s:saw note:F2 partials:[0.5945571791380644 0.3962489552795887 0.563431927934289 0.4742323160171509 0.4185752831399441 0.9703940469771624 0.5282467231154442 0.6896266285330057] ]", + "[ 31/16 → 2/1 | s:saw note:Bb2 partials:[0.8672592639923096 0.3438429981470108 0.9795673936605453 0.46547594852745533 0.9770395755767822 0.36851615831255913 0.7725539114326239 0.876962523907423] ]", + "[ 2/1 → 33/16 | s:saw note:C3 partials:[0.9595271199941635 0.5427457969635725 0.45864437520504 0.8057199101895094 0.18388565629720688 0.17221237532794476 0.8838487900793552 0.4976818822324276] ]", + "[ 33/16 → 17/8 | s:saw note:Bb1 partials:[0.3324784208089113 0.6005446836352348 0.1564352661371231 0.765217661857605 0.12899602390825748 0.20880493707954884 0.9156261626631021 0.5550615340471268] ]", + "[ 17/8 → 35/16 | s:saw note:Bb2 partials:[0.9033902939409018 0.22919894196093082 0.8388008493930101 0.9108077362179756 0.32246968522667885 0.16122018359601498 0.31932324543595314 0.18524732999503613] ]", + "[ 35/16 → 9/4 | s:saw note:F1 partials:[0.037000780925154686 0.30360451713204384 0.7516817897558212 0.17065968923270702 0.24669718369841576 0.978821286931634 0.9620356522500515 0.12417634017765522] ]", + "[ 9/4 → 37/16 | s:saw note:C2 partials:[0.34548251144587994 0.30419893004000187 0.10205957666039467 0.9906709585338831 0.3156223688274622 0.3745057098567486 0.7668170686811209 0.9379972033202648] ]", + "[ 37/16 → 19/8 | s:saw note:F2 partials:[0.6649543270468712 0.41613837145268917 0.9078915324062109 0.2232209537178278 0.2918607946485281 0.4931973237544298 0.7035325299948454 0.29280414804816246] ]", + "[ 19/8 → 39/16 | s:saw note:Bb2 partials:[0.8365066405385733 0.5369812995195389 0.2263860274106264 0.21759207546710968 0.2172116208821535 0.09454222768545151 0.8075542915612459 0.9017798062413931] ]", + "[ 39/16 → 5/2 | s:saw note:G1 partials:[0.14231421053409576 0.6801821701228619 0.7958894111216068 0.9494249671697617 0.179213372990489 0.22531690262258053 0.7458387855440378 0.3595987129956484] ]", + "[ 5/2 → 41/16 | s:saw note:Db2 partials:[0.45861607417464256 0.6807645913213491 0.07009582966566086 0.1450389064848423 0.2579921595752239 0.612881500273943 0.5503941811621189 0.7027011960744858] ]", + "[ 41/16 → 21/8 | s:saw note:F2 partials:[0.6068673655390739 0.8489279896020889 0.027898555621504784 0.90520747192204 0.18605150654911995 0.30939464271068573 0.16843407973647118 0.5127317048609257] ]", + "[ 21/8 → 43/16 | s:saw note:G1 partials:[0.16019348427653313 0.47758268006145954 0.9749126061797142 0.8025561925023794 0.3600110989063978 0.324309878051281 0.5655391272157431 0.13364790193736553] ]", + "[ 43/16 → 11/4 | s:saw note:Eb2 partials:[0.538035349920392 0.4103843830525875 0.6443832442164421 0.7532152868807316 0.32128027081489563 0.29064710810780525 0.5972099266946316 0.8252892810851336] ]", + "[ 11/4 → 45/16 | s:saw note:F2 partials:[0.6332327704876661 0.7342763151973486 0.16935866326093674 0.4754706546664238 0.528122790157795 0.21934260986745358 0.7476693484932184 0.2995396424084902] ]", + "[ 45/16 → 23/8 | s:saw note:Bb2 partials:[0.8912940509617329 0.5256196465343237 0.25639201514422894 0.46860250271856785 0.08611978217959404 0.6175916157662868 0.990638293325901 0.11189854890108109] ]", + "[ 23/8 → 47/16 | s:saw note:F1 partials:[0.01625417172908783 0.28409438766539097 0.3075305689126253 0.40343056432902813 0.5556836389005184 0.6926821582019329 0.5635769125074148 0.017873913049697876] ]", + "[ 47/16 → 3/1 | s:saw note:Bb1 partials:[0.2830589488148689 0.3613663297146559 0.6216250211000443 0.6083405166864395 0.27202711440622807 0.771099541336298 0.3156145606189966 0.736228458583355] ]", + "[ 3/1 → 49/16 | s:saw note:Ab1 partials:[0.21728911064565182 0.7605195846408606 0.11240843310952187 0.4372697602957487 0.15630115941166878 0.1653979979455471 0.5303416550159454 0.07218753732740879] ]", + "[ 49/16 → 25/8 | s:saw note:F1 partials:[0.012518879026174545 0.6565517522394657 0.783835593611002 0.2481316588819027 0.27786846831440926 0.6153149046003819 0.39914070069789886 0.2175555843859911] ]", + "[ 25/8 → 51/16 | s:saw note:C2 partials:[0.34324387833476067 0.14098095521330833 0.012934491038322449 0.827436288818717 0.5283997394144535 0.8942463714629412 0.4924008697271347 0.9298017006367445] ]", + "[ 51/16 → 13/4 | s:saw note:Bb2 partials:[0.9057559575885534 0.5731389932334423 0.06561482697725296 0.02156665176153183 0.4685337021946907 0.000788738951086998 0.6995994579046965 0.9893404543399811] ]", + "[ 13/4 → 53/16 | s:saw note:C3 partials:[0.9923650715500116 0.7797339502722025 0.1992435697466135 0.9005183521658182 0.718992218375206 0.23745290748775005 0.28872333094477654 0.35448253713548183] ]", + "[ 53/16 → 27/8 | s:saw note:Bb1 partials:[0.3235324025154114 0.5165992900729179 0.6231792941689491 0.8034896682947874 0.6663950439542532 0.4207208137959242 0.9748435858637094 0.27676148526370525] ]", + "[ 27/8 → 55/16 | s:saw note:C2 partials:[0.40869180113077164 0.4018078800290823 0.34696186147630215 0.9572948440909386 0.400035472586751 0.3531211093068123 0.7240961641073227 0.8125612027943134] ]", + "[ 55/16 → 7/2 | s:saw note:Ab2 partials:[0.775677714496851 0.0502743236720562 0.3246541116386652 0.503994770348072 0.8252716399729252 0.89872563816607 0.060319963842630386 0.25497629307210445] ]", + "[ 7/2 → 57/16 | s:saw note:C2 partials:[0.40994881466031075 0.6455810181796551 0.35704658553004265 0.2732649203389883 0.31587288342416286 0.23243548162281513 0.22569947130978107 0.17241661623120308] ]", + "[ 57/16 → 29/8 | s:saw note:Ab2 partials:[0.8133465722203255 0.8327706772834063 0.32914630882442 0.23131784796714783 0.7526696771383286 0.1361286472529173 0.11441296897828579 0.36366880126297474] ]", + "[ 29/8 → 59/16 | s:saw note:C3 partials:[0.9391485787928104 0.3380728308111429 0.7723016366362572 0.998674126341939 0.7193406894803047 0.6963680125772953 0.9260678570717573 0.7829763628542423] ]", + "[ 59/16 → 15/4 | s:saw note:C3 partials:[0.9731374885886908 0.0494034755975008 0.990720022469759 0.9819309785962105 0.22877203114330769 0.7265191469341516 0.4922411348670721 0.7549834884703159] ]", + "[ 15/4 → 61/16 | s:saw note:Ab2 partials:[0.8121673222631216 0.25548945367336273 0.5759696904569864 0.6668333616107702 0.9233786761760712 0.11143362522125244 0.9734930321574211 0.369325939565897] ]", + "[ 61/16 → 31/8 | s:saw note:G1 partials:[0.1008925698697567 0.7558664139360189 0.1523460578173399 0.6523381881415844 0.9690635204315186 0.7343240566551685 0.9544064309448004 0.9150135722011328] ]", + "[ 31/8 → 63/16 | s:saw note:G2 partials:[0.7361665386706591 0.7657648548483849 0.21474426984786987 0.8228576295077801 0.5647660344839096 0.08148551359772682 0.9714624118059874 0.13992334716022015] ]", + "[ 63/16 → 4/1 | s:saw note:Bb1 partials:[0.3056422360241413 0.8881660811603069 0.7683485243469477 0.08641545102000237 0.5427133180201054 0.20803124643862247 0.5347504671663046 0.5977730695158243] ]", +] +`; + exports[`runs examples > example "range" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh cutoff:2250 ]", From 35016b58476b17d6b9a2c98b922df2d1fa9e76f6 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 15 Oct 2025 14:56:20 -0500 Subject: [PATCH 007/110] Mention range in docstring --- packages/core/controls.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 327f8df51..77e77caa7 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2380,7 +2380,7 @@ export const scrub = register( * Can also be used to create a new synth via `s('user').partials(...)` * * @name partials - * @param {number[] | Pattern} partials List of magnitudes for partials. 0th entry is the first harmonic (i.e. DC offset is skipped) + * @param {number[] | Pattern} partials List of [0, 1] magnitudes for partials. 0th entry is the first harmonic (i.e. DC offset is skipped) * @example * s("user").seg(16).n(irand(8)).scale("A:major") * .partials([1, 0, 1, 0, 0, 1]) @@ -2394,7 +2394,7 @@ export const { partials } = registerControl('partials'); * Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases * * @name phases - * @param {number[] | Pattern} phases List of phases for partials. 0th entry is the first phase (i.e. DC offset is skipped) + * @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the first phase (i.e. DC offset is skipped) * @example * s("saw").seg(8).n(irand(12)).scale("G#:minor") * .partials(binaryL(256)) From fd03d1fdfbc57d7b1e4b8c14bdb414d44bdc0ec8 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 15 Oct 2025 15:14:31 -0500 Subject: [PATCH 008/110] Update conditional to allow to control user waveforms --- packages/superdough/synth.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 0294762fb..10df1776b 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -472,7 +472,7 @@ export function getOscillator(s, t, value) { } s = s === 'user' && !partials ? 'triangle' : s; // If no partials are given, use stock waveforms - if (!partials || !partials?.length || s === 'sine') { + if (!partials || partials?.length === 0 || s === 'sine') { o = getAudioContext().createOscillator(); o.type = s || 'triangle'; } From 47c85f8540386d36c42c5bc64f952bde12ac79d5 Mon Sep 17 00:00:00 2001 From: jeromew Date: Thu, 16 Oct 2025 14:06:33 +0200 Subject: [PATCH 009/110] fix interoperability of `all` with hydra Hydra add methods on Array.prototype which breaks the Array enumeration with for...in. Use for...of instead cf https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of#difference_between_for...of_and_for...in for details on the issue without this fix, it is impossible to use both `await initHydra()` and `all` in the same strudel code. Strudel errors with "Error: got "undefined" instead of pattern." example code: https://strudel.cc/#YXdhaXQgaW5pdEh5ZHJhKCkKCmFsbChwaWFub3JvbGwpCgokOiBzKCJiZCIp --- packages/core/repl.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 171697eb9..064ef1eb8 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -227,8 +227,8 @@ export function repl({ pattern = eachTransform(pattern); } if (allTransforms.length) { - for (let i in allTransforms) { - pattern = allTransforms[i](pattern); + for (const transform of allTransforms) { + pattern = transform(pattern); } } From 1be9f40574aa213d3ef501024e5a0e18cb04fef9 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 18 Oct 2025 10:42:35 +0100 Subject: [PATCH 010/110] timeline feature --- packages/core/impure.mjs | 60 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 packages/core/impure.mjs diff --git a/packages/core/impure.mjs b/packages/core/impure.mjs new file mode 100644 index 000000000..66fc1dd5f --- /dev/null +++ b/packages/core/impure.mjs @@ -0,0 +1,60 @@ +/* +stateful.mjs - File of shame for stateful, impure and otherwise illegal pattern methods +Copyright (C) 2025 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +import { register, reify } from './pattern.mjs'; + +let timelines = {}; + +export const reset_timelines = function () { + timelines = {}; +}; + +export const timeline = register( + 'timeline', + function (tpat, pat) { + tpat = reify(tpat); + const f = function (state) { + let scheduler = !!state.controls._cps; + + const timehaps = tpat.query(state); + const result = []; + for (const timehap of timehaps) { + const tlid = timehap.value; + const ignore = false; + let offset; + if (tlid in timelines) { + offset = timelines[tlid]; + } else { + const timearc = timehap.wholeOrPart(); + if (!scheduler || state.span.begin.lt(timearc.midpoint())) { + offset = timearc.begin; + } else { + // Sync to end of timearc if we first see it over halfway into its + // timespan. Allows 'cuing up' next timeline when live coding. + offset = timearc.end; + } + } + if (scheduler) { + // update state + timelines[tlid] = offset; + const negative = 0 - tlid; + if (negative in timelines) { + delete timelines[negative]; + } + } + + const pathaps = pat + .late(offset) + .query(state.setSpan(timehap.part)) + .map((h) => h.setContext(h.combineContext(timehap))); + result.push(...pathaps); + } + return result; + }; + return new Pattern(f, pat._steps); + }, + false, +); From b2ce4591d9bd4260511bb08a06f791f515452456 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 18 Oct 2025 10:42:55 +0100 Subject: [PATCH 011/110] sort exports and add impure.mjs --- packages/core/index.mjs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/core/index.mjs b/packages/core/index.mjs index e4daf445a..9260e3671 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -1,6 +1,6 @@ /* index.mjs - -Copyright (C) 2022 Strudel contributors - see +Copyright (C) 2025 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ @@ -11,20 +11,21 @@ import createClock from './zyklus.mjs'; import { logger } from './logger.mjs'; export { Fraction, controls, createClock }; export * from './controls.mjs'; -export * from './hap.mjs'; -export * from './pattern.mjs'; -export * from './signal.mjs'; -export * from './pick.mjs'; -export * from './state.mjs'; -export * from './timespan.mjs'; -export * from './util.mjs'; -export * from './speak.mjs'; -export * from './evaluate.mjs'; -export * from './repl.mjs'; export * from './cyclist.mjs'; +export * from './evaluate.mjs'; +export * from './hap.mjs'; +export * from './impure.mjs'; export * from './logger.mjs'; +export * from './pattern.mjs'; +export * from './pick.mjs'; +export * from './repl.mjs'; +export * from './signal.mjs'; +export * from './speak.mjs'; +export * from './state.mjs'; export * from './time.mjs'; +export * from './timespan.mjs'; export * from './ui.mjs'; +export * from './util.mjs'; export { default as drawLine } from './drawLine.mjs'; // below won't work with runtime.mjs (json import fails) /* import * as p from './package.json'; From 92a82ce4bdf951ae80a34c0e609aa3515951fa6f Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 18 Oct 2025 10:49:15 +0100 Subject: [PATCH 012/110] delint --- packages/core/impure.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/impure.mjs b/packages/core/impure.mjs index 66fc1dd5f..e16cfbe45 100644 --- a/packages/core/impure.mjs +++ b/packages/core/impure.mjs @@ -4,7 +4,7 @@ Copyright (C) 2025 Strudel contributors - see . */ -import { register, reify } from './pattern.mjs'; +import { register, reify, Pattern } from './pattern.mjs'; let timelines = {}; From 4c49cd9297326cf4991f1dca879bef3ed657be51 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 18 Oct 2025 11:24:56 -0500 Subject: [PATCH 013/110] 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 bfa8fa3c757b2b457ee6aa5ef1a55f8bf14902e2 Mon Sep 17 00:00:00 2001 From: jeromew Date: Sun, 16 Nov 2025 09:23:11 +0000 Subject: [PATCH 014/110] [perf] fix `connect leak` when .noise() is in the mix --- packages/superdough/helpers.mjs | 14 +++++++++++++- packages/superdough/noise.mjs | 3 ++- packages/superdough/synth.mjs | 23 +++++++++++++---------- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 47161ed61..de64df0e8 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -262,7 +262,15 @@ export function drywet(dry, wet, wetAmount = 0) { let mix = ac.createGain(); dry_gain.connect(mix); wet_gain.connect(mix); - return mix; + return { + node: mix, + onended: () => { + dry_gain.disconnect(mix); + wet_gain.disconnect(mix); + dry.disconnect(dry_gain); + wet.disconnect(wet_gain); + }, + }; } let curves = ['linear', 'exponential']; @@ -297,6 +305,10 @@ export function getVibratoOscillator(param, value, t) { gain.gain.value = vibmod * 100; vibratoOscillator.connect(gain); gain.connect(param); + vibratoOscillator.onended = () => { + gain.disconnect(param); + vibratoOscillator.disconnect(gain); + }; vibratoOscillator.start(t); return vibratoOscillator; } diff --git a/packages/superdough/noise.mjs b/packages/superdough/noise.mjs index 816dd252b..3e41515df 100644 --- a/packages/superdough/noise.mjs +++ b/packages/superdough/noise.mjs @@ -65,8 +65,9 @@ export function getNoiseOscillator(type = 'white', t, density = 0.02) { export function getNoiseMix(inputNode, wet, t) { const noiseOscillator = getNoiseOscillator('pink', t); const noiseMix = drywet(inputNode, noiseOscillator.node, wet); + noiseOscillator.node.onended = () => noiseMix.onended(); return { - node: noiseMix, + node: noiseMix.node, stop: (time) => noiseOscillator?.stop(time), }; } diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index e35b98806..a9ec7975c 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -47,19 +47,17 @@ export function registerSynthSounds() { [0.001, 0.05, 0.6, 0.01], ); - let sound = getOscillator(s, t, value); - let { node: o, stop, triggerRelease } = sound; - // turn down const g = gainNode(0.3); - const { duration } = value; - - o.onended = () => { - o.disconnect(); + let sound = getOscillator(s, t, value, () => { g.disconnect(); onended(); - }; + }); + + let { node: o, stop, triggerRelease } = sound; + + const { duration } = value; const envGain = gainNode(1); let node = o.connect(g).connect(envGain); @@ -446,7 +444,7 @@ export function waveformN(partials, type) { } // expects one of waveforms as s -export function getOscillator(s, t, value) { +export function getOscillator(s, t, value, onended) { let { n: partials, duration, noise = 0 } = value; let o; // If no partials are given, use stock waveforms @@ -460,7 +458,6 @@ export function getOscillator(s, t, value) { } // set frequency o.frequency.value = getFrequencyFromValue(value); - o.start(t); let vibratoOscillator = getVibratoOscillator(o.detune, value, t); @@ -473,6 +470,12 @@ export function getOscillator(s, t, value) { noiseMix = getNoiseMix(o, noise, t); } + o.onended = () => { + noiseMix?.node.disconnect(); + onended(); + }; + o.start(t); + return { node: noiseMix?.node || o, stop: (time) => { From cc10122b7184d4271c4a166673a76726854ac1c7 Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 16 Nov 2025 19:20:42 -0600 Subject: [PATCH 015/110] Testing out approach to allow list patterns in partials --- packages/core/controls.mjs | 28 --------------- packages/core/pattern.mjs | 51 ++++++++++++++++++++++++++ website/src/pages/learn/synths.mdx | 57 ++++++++++++++++++++++++++---- 3 files changed, 101 insertions(+), 35 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 1d19a94b6..9a2be7f44 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2583,31 +2583,3 @@ export const scrub = register( }, false, ); - -/** - * Scale the magnitude of the harmonics of one of the core synths ('sine', 'tri', 'saw', ..) - * - * Can also be used to create a new synth via `s('user').partials(...)` - * - * @name partials - * @param {number[] | Pattern} partials List of [0, 1] magnitudes for partials. 0th entry is the first harmonic (i.e. DC offset is skipped) - * @example - * s("user").seg(16).n(irand(8)).scale("A:major") - * .partials([1, 0, 1, 0, 0, 1]) - * @example - * s("saw").seg(8).n(irand(12)).scale("G#:minor") - * .partials(binaryL(256)) - */ -export const { partials } = registerControl('partials'); - -/** - * Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases - * - * @name phases - * @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the first phase (i.e. DC offset is skipped) - * @example - * s("saw").seg(8).n(irand(12)).scale("G#:minor") - * .partials(binaryL(256)) - * .phases(randL(20)) - */ -export const { phases } = registerControl('phases'); diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index e29769900..f08667cf7 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3624,3 +3624,54 @@ for (const name of distAlgoNames) { return this.distort(argsPat); }; } + +/** + * Turns a list of patterns into a single pattern which outputs list-values + * + * @name parray + * @returns Pattern + */ +export const parray = (pats) => { + const pack = (...xs) => xs; + let acc = pure(curry(pack, null, pats.length)); + for (const p of pats) acc = acc.appBoth(reify(p)); + return acc; +}; + +/** + * Scale the magnitude of the harmonics of one of the core synths ('sine', 'tri', 'saw', ..) + * + * Can also be used to create a new synth via `s('user').partials(...)` + * + * @name partials + * @param {number[] | Pattern} partials List of [0, 1] magnitudes for partials. 0th entry is the first harmonic (i.e. DC offset is skipped) + * @example + * s("user").seg(16).n(irand(8)).scale("A:major") + * .partials([1, 0, 1, 0, 0, 1]) + * @example + * s("saw").seg(8).n(irand(12)).scale("G#:minor") + * .partials(binaryL(256)) + */ +export const { partials } = register('partials', (list, pat) => { + if (Array.isArray(list)) { + list = parray(list); + } + return pat.withValue((v) => ({...v, partials: list})); +}); + +/** + * Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases + * + * @name phases + * @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the first phase (i.e. DC offset is skipped) + * @example + * s("saw").seg(8).n(irand(12)).scale("G#:minor") + * .partials(binaryL(256)) + * .phases(randL(20)) + */ +export const { phases } = register('phases', (list, pat) => { + if (Array.isArray(list)) { + list = parray(list); + } + return pat.withValue((v) => ({...v, phases: list})); +}); diff --git a/website/src/pages/learn/synths.mdx b/website/src/pages/learn/synths.mdx index 0fcc41363..1a65568df 100644 --- a/website/src/pages/learn/synths.mdx +++ b/website/src/pages/learn/synths.mdx @@ -48,28 +48,71 @@ You can also use the `crackle` type to play some subtle noise crackles. You can ### Additive Synthesis -To tame the harsh sound of the basic waveforms, we can set the `n` control to limit the overtones of the waveform: +Waveforms are often composed of several [harmonics](https://en.wikipedia.org/wiki/Harmonic) above a fundamental frequency, lying at integer multiples. These overtones combine to give a sound its unique timbral quality. + +For the basic waveforms, we offer you control over these harmonics with the `partials` and `phases` functions. + +#### Partials + +`partials` refers to the magnitude of each harmonic relative to the fundamental frequency. They can thus be used to spectrally filter these waveforms and tame some of their harshness: >".fast(2)) .sound("sawtooth") -.n("<32 16 8 4>") +.partials([1, 1, 0, 1]) ._scope()`} /> -When the `n` control is used on a basic waveform, it defines the number of harmonic partials the sound is getting. -You can also set `n` directly in mini notation with `sound`: +`partials` can also be used to construct _new_ waveforms not present in our basic set with the 'user' sound source: >".fast(2)) -.sound("sawtooth:<32 16 8 4>") +.sound("user") +.partials([1, 0, 0.3, 0, 0.1, 0, 0, 0.3]) ._scope()`} /> -Note for tidal users: `n` in tidal is synonymous to `note` for synths only. -In strudel, this is not the case, where `n` will always change timbre, be it though different samples or different waveforms. +We may algorithmically construct lists of partials with Javascript code like: + +>".fast(2)) +.sound("user") +.partials(new Array(numHarmonics).fill(1)) +._scope()`} +/> + +This approach can act as a form of bandlimiting. `partials` is also compatible with pattern functions designed to produce lists, like `randL` or `binaryL`: + +>".fast(2)) +.sound("user") +.partials(randL(8)) +._scope()`} +/> + +Note that the first value in the `partials` array controls the magnitude of the fundamental harmonic rather than the DC offset, which is fixed at 0. + +#### Phases + +We mentioned that our sounds can be broken into a constituent set of harmonics above a fundamental frequency. These are defined by two values: their magnitude (how loud they are) and their [phase](https://en.wikipedia.org/wiki/Phase_(waves)), which can be thought of as which point in its cycle each sine wave is initialized at when we begin adding them. + +These phases too can be declared in Strudel and can give your sounds interesting depth. + +>".fast(2)) +.sound("user") +.partials(randL(8)) +.phases(randL(8).late(0.3)) +._scope()`} +/> ## Vibrato From 7a3bea6f9087105ed0be0982798b9dd26c9ed089 Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 16 Nov 2025 19:38:59 -0600 Subject: [PATCH 016/110] Working version --- packages/core/pattern.mjs | 5 +++-- website/src/pages/learn/synths.mdx | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index f08667cf7..cd8c5871a 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3653,10 +3653,11 @@ export const parray = (pats) => { * .partials(binaryL(256)) */ export const { partials } = register('partials', (list, pat) => { + debugger; if (Array.isArray(list)) { list = parray(list); } - return pat.withValue((v) => ({...v, partials: list})); + return pat.withValue((v) => (l) => ({...v, partials: l})).appLeft(list); }); /** @@ -3673,5 +3674,5 @@ export const { phases } = register('phases', (list, pat) => { if (Array.isArray(list)) { list = parray(list); } - return pat.withValue((v) => ({...v, phases: list})); + return pat.withValue((v) => (l) => ({...v, phases: l})).appLeft(list); }); diff --git a/website/src/pages/learn/synths.mdx b/website/src/pages/learn/synths.mdx index 1a65568df..b209dc9c0 100644 --- a/website/src/pages/learn/synths.mdx +++ b/website/src/pages/learn/synths.mdx @@ -96,6 +96,16 @@ note("c2 >".fast(2)) ._scope()`} /> +and with lists _of_ patterns: + +>".fast(4)) +.sound("user") +.partials([1, 0, "0 1", "0 1 0.3", rand]) +._scope()`} +/> + Note that the first value in the `partials` array controls the magnitude of the fundamental harmonic rather than the DC offset, which is fixed at 0. #### Phases From f5ed6dbe79a683dac229ed095ac0c4ff592c72ea Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 16 Nov 2025 20:28:22 -0600 Subject: [PATCH 017/110] Top level functions --- packages/core/pattern.mjs | 36 +++++++++++++++------ packages/core/test/pattern.test.mjs | 4 +-- website/src/components/Header/Search.css | 4 +-- website/src/pages/learn/synths.mdx | 40 +++++++++++++++--------- 4 files changed, 56 insertions(+), 28 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index cd8c5871a..3c21bd1b2 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3644,35 +3644,51 @@ export const parray = (pats) => { * Can also be used to create a new synth via `s('user').partials(...)` * * @name partials - * @param {number[] | Pattern} partials List of [0, 1] magnitudes for partials. 0th entry is the first harmonic (i.e. DC offset is skipped) + * @param {number[] | Pattern} magnitudes List of [0, 1] magnitudes for partials. 0th entry is the fundamental harmonic (i.e. DC offset is skipped) * @example * s("user").seg(16).n(irand(8)).scale("A:major") * .partials([1, 0, 1, 0, 0, 1]) * @example * s("saw").seg(8).n(irand(12)).scale("G#:minor") - * .partials(binaryL(256)) + * .partials(randL(16)) */ -export const { partials } = register('partials', (list, pat) => { - debugger; +register('partials', (list, pat) => { if (Array.isArray(list)) { list = parray(list); } - return pat.withValue((v) => (l) => ({...v, partials: l})).appLeft(list); + return pat.withValue((v) => (l) => ({ ...v, partials: l })).appLeft(list); }); +// Also create a top-level function +export const partials = (list) => { + if (Array.isArray(list)) { + list = parray(list); + } + return list.as('partials'); +}; + /** * Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases * * @name phases - * @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the first phase (i.e. DC offset is skipped) + * @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the fundamental phase (i.e. DC offset is skipped) * @example * s("saw").seg(8).n(irand(12)).scale("G#:minor") - * .partials(binaryL(256)) - * .phases(randL(20)) + * .partials(randL(16)) + * .phases(randL(16)) */ -export const { phases } = register('phases', (list, pat) => { +register('phases', (list, pat) => { if (Array.isArray(list)) { list = parray(list); } - return pat.withValue((v) => (l) => ({...v, phases: l})).appLeft(list); + pat = pat ?? pure({}); + return pat.withValue((v) => (l) => ({ ...v, phases: l })).appLeft(list); }); + +// Also create a top-level function +export const phases = (list) => { + if (Array.isArray(list)) { + list = parray(list); + } + return list.as('phases'); +}; diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 625f40756..1df5c8776 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -796,7 +796,7 @@ describe('Pattern', () => { }); }); describe('apply', () => { - (it('Can apply a function', () => { + it('Can apply a function', () => { expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle()); }), it('Can apply a pattern of functions', () => { @@ -804,7 +804,7 @@ describe('Pattern', () => { expect(sequence('a', 'b').apply(fast(2), fast(3)).firstCycle()).toStrictEqual( sequence('a', 'b').fast(2, 3).firstCycle(), ); - })); + }); }); describe('layer', () => { it('Can layer up multiple functions', () => { diff --git a/website/src/components/Header/Search.css b/website/src/components/Header/Search.css index b14146cbd..456ef9f6b 100644 --- a/website/src/components/Header/Search.css +++ b/website/src/components/Header/Search.css @@ -18,8 +18,8 @@ --docsearch-modal-background: var(--background); --docsearch-muted-color: color-mix(in srgb, var(--foreground), #fff 30%); --docsearch-key-gradient: var(--foreground); - --docsearch-key-shadow: - inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), 0 1px 2px 1px var(--gutterBackground); + --docsearch-key-shadow: inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), + 0 1px 2px 1px var(--gutterBackground); } .dark { --docsearch-muted-color: color-mix(in srgb, var(--foreground), #000 30%); diff --git a/website/src/pages/learn/synths.mdx b/website/src/pages/learn/synths.mdx index b209dc9c0..6fc5b6925 100644 --- a/website/src/pages/learn/synths.mdx +++ b/website/src/pages/learn/synths.mdx @@ -48,7 +48,7 @@ You can also use the `crackle` type to play some subtle noise crackles. You can ### Additive Synthesis -Waveforms are often composed of several [harmonics](https://en.wikipedia.org/wiki/Harmonic) above a fundamental frequency, lying at integer multiples. These overtones combine to give a sound its unique timbral quality. +Periodic waveforms are composed of several [harmonics](https://en.wikipedia.org/wiki/Harmonic) above a fundamental frequency, lying at integer multiples. These overtones combine to give a sound its unique timbral quality. For the basic waveforms, we offer you control over these harmonics with the `partials` and `phases` functions. @@ -60,7 +60,7 @@ For the basic waveforms, we offer you control over these harmonics with the `par client:idle tune={`note("c2 >".fast(2)) .sound("sawtooth") -.partials([1, 1, 0, 1]) +.partials([1, 1, "<1 0>", "<1 0>", "<1 0>", "<1 0>", "<1 0>"]) ._scope()`} /> @@ -74,25 +74,38 @@ For the basic waveforms, we offer you control over these harmonics with the `par ._scope()`} /> -We may algorithmically construct lists of partials with Javascript code like: +We may algorithmically construct lists of magnitudes with Javascript code like: >".fast(2)) -.sound("user") +.sound("saw") .partials(new Array(numHarmonics).fill(1)) ._scope()`} /> -This approach can act as a form of bandlimiting. `partials` is also compatible with pattern functions designed to produce lists, like `randL` or `binaryL`: +which acts as a spectral filter or >".fast(2)) + tune={`note("c2 >").fast(2) .sound("user") -.partials(randL(8)) +.partials(new Array(50).fill(0) + .map((_, idx) => ((-1) ** (idx + 1)) / (idx + 1)) +) +._scope()`} +/> + +which recovers a familiar waveform. + +`partials` is also compatible with pattern functions designed to produce lists, like `randL` or `binaryL`: + +>").fast(2) +.sound("user") +.partials(randL(10)) ._scope()`} /> @@ -110,17 +123,16 @@ Note that the first value in the `partials` array controls the magnitude of the #### Phases -We mentioned that our sounds can be broken into a constituent set of harmonics above a fundamental frequency. These are defined by two values: their magnitude (how loud they are) and their [phase](https://en.wikipedia.org/wiki/Phase_(waves)), which can be thought of as which point in its cycle each sine wave is initialized at when we begin adding them. +Earlier, we mentioned that periodic waveforms can be broken into a set of harmonics above a fundamental frequency. Each harmonic has two defining properties: its magnitude (how loud it is) and its phase, which determines where in its cycle that sine wave starts when the waveform is built. These phases too can be declared in Strudel and can give your sounds interesting depth. >".fast(2)) -.sound("user") -.partials(randL(8)) -.phases(randL(8).late(0.3)) + tune={`note("c2 >".fast(2)) +.sound("saw") +.partials(randL(100)) +.phases(randL(100)) ._scope()`} /> From 641bd2f356f16072dd69e75509697294e479ac38 Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 16 Nov 2025 22:17:53 -0600 Subject: [PATCH 018/110] Properly patternify everything --- packages/core/pattern.mjs | 45 +++--- test/__snapshots__/examples.test.mjs.snap | 160 +++++++++++++--------- website/src/pages/learn/synths.mdx | 13 +- 3 files changed, 123 insertions(+), 95 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 3c21bd1b2..4737f0db0 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3638,6 +3638,13 @@ export const parray = (pats) => { return acc; }; +const _ensureListPattern = (list) => { + if (Array.isArray(list)) { + return parray(list); + } + return reify(list); +}; + /** * Scale the magnitude of the harmonics of one of the core synths ('sine', 'tri', 'saw', ..) * @@ -3650,21 +3657,15 @@ export const parray = (pats) => { * .partials([1, 0, 1, 0, 0, 1]) * @example * s("saw").seg(8).n(irand(12)).scale("G#:minor") - * .partials(randL(16)) + * .partials(binaryL(irand(256).add("1"))) */ -register('partials', (list, pat) => { - if (Array.isArray(list)) { - list = parray(list); - } - return pat.withValue((v) => (l) => ({ ...v, partials: l })).appLeft(list); -}); +Pattern.prototype.partials = function (list) { + return this.withValue((v) => (l) => ({ ...v, partials: l })).appLeft(_ensureListPattern(list)); +}; // Also create a top-level function export const partials = (list) => { - if (Array.isArray(list)) { - list = parray(list); - } - return list.as('partials'); + return _ensureListPattern(list).as('partials'); }; /** @@ -3673,22 +3674,16 @@ export const partials = (list) => { * @name phases * @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the fundamental phase (i.e. DC offset is skipped) * @example - * s("saw").seg(8).n(irand(12)).scale("G#:minor") - * .partials(randL(16)) - * .phases(randL(16)) + * // Phase cancellation + * s("saw").seg(8).n(irand(12)).scale("G#1:minor") + * .partials(partials([1, 1, 1])) + * .superimpose(x => x.phases([0.5, 0.5, 0.5])) */ -register('phases', (list, pat) => { - if (Array.isArray(list)) { - list = parray(list); - } - pat = pat ?? pure({}); - return pat.withValue((v) => (l) => ({ ...v, phases: l })).appLeft(list); -}); +Pattern.prototype.phases = function (list) { + return this.withValue((v) => (l) => ({ ...v, phases: l })).appLeft(_ensureListPattern(list)); +}; // Also create a top-level function export const phases = (list) => { - if (Array.isArray(list)) { - list = parray(list); - } - return list.as('phases'); + return _ensureListPattern(list).as('phases'); }; diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 4b4c36e98..32e61c31d 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -7290,38 +7290,38 @@ exports[`runs examples > example "partials" example index 0 1`] = ` exports[`runs examples > example "partials" example index 1 1`] = ` [ - "[ 0/1 → 1/8 | s:saw note:G#3 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 1/8 → 1/4 | s:saw note:A#4 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 1/4 → 3/8 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 3/8 → 1/2 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 1/2 → 5/8 | s:saw note:C#4 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 5/8 → 3/4 | s:saw note:A#3 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 3/4 → 7/8 | s:saw note:B3 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 7/8 → 1/1 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 1/1 → 9/8 | s:saw note:F#4 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 9/8 → 5/4 | s:saw note:A#4 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 5/4 → 11/8 | s:saw note:A#4 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 11/8 → 3/2 | s:saw note:B3 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 3/2 → 13/8 | s:saw note:G#4 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 13/8 → 7/4 | s:saw note:E4 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 7/4 → 15/8 | s:saw note:B3 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 15/8 → 2/1 | s:saw note:G#4 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 2/1 → 17/8 | s:saw note:D#5 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 17/8 → 9/4 | s:saw note:C#5 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 9/4 → 19/8 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 19/8 → 5/2 | s:saw note:C#5 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 5/2 → 21/8 | s:saw note:E4 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 21/8 → 11/4 | s:saw note:A#3 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 11/4 → 23/8 | s:saw note:G#4 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 23/8 → 3/1 | s:saw note:G#3 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 3/1 → 25/8 | s:saw note:B3 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 25/8 → 13/4 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 13/4 → 27/8 | s:saw note:D#5 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 27/8 → 7/2 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 7/2 → 29/8 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 29/8 → 15/4 | s:saw note:D#5 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 15/4 → 31/8 | s:saw note:B4 partials:[1 0 0 0 0 0 0 0 0] ]", - "[ 31/8 → 4/1 | s:saw note:A#4 partials:[1 0 0 0 0 0 0 0 0] ]", + "[ 0/1 → 1/8 | s:saw note:G#3 partials:[1] ]", + "[ 1/8 → 1/4 | s:saw note:A#4 partials:[1 0 1 1 0 0 0 0] ]", + "[ 1/4 → 3/8 | s:saw note:D#4 partials:[1 0 1 1 1 1 1] ]", + "[ 3/8 → 1/2 | s:saw note:D#4 partials:[1 1 0 0 1 1 1] ]", + "[ 1/2 → 5/8 | s:saw note:C#4 partials:[1 0 0 0 0 1 1] ]", + "[ 5/8 → 3/4 | s:saw note:A#3 partials:[1 0 0 0 1 1] ]", + "[ 3/4 → 7/8 | s:saw note:B3 partials:[1 1 0 0 1 1] ]", + "[ 7/8 → 1/1 | s:saw note:D#4 partials:[1 1 0 0 1 1 0] ]", + "[ 1/1 → 9/8 | s:saw note:F#4 partials:[1 0 0 0 0 1 1 0] ]", + "[ 9/8 → 5/4 | s:saw note:A#4 partials:[1 0 1 0 1 1 0 1] ]", + "[ 5/4 → 11/8 | s:saw note:A#4 partials:[1 0 1 1 1 0 1 1] ]", + "[ 11/8 → 3/2 | s:saw note:B3 partials:[1 0 1 1 1 1] ]", + "[ 3/2 → 13/8 | s:saw note:G#4 partials:[1 0 0 1 1 1 0 0] ]", + "[ 13/8 → 7/4 | s:saw note:E4 partials:[1 0 0 0 0 0 0 0] ]", + "[ 7/4 → 15/8 | s:saw note:B3 partials:[1 1 0 1 0 1] ]", + "[ 15/8 → 2/1 | s:saw note:G#4 partials:[1 0 0 1 1 0 0 1] ]", + "[ 2/1 → 17/8 | s:saw note:D#5 partials:[1 1 1 1 0 1 1 0] ]", + "[ 17/8 → 9/4 | s:saw note:C#5 partials:[1 1 1 0 1 0 0 0] ]", + "[ 9/4 → 19/8 | s:saw note:D#4 partials:[1 0 1 1 0 0 1] ]", + "[ 19/8 → 5/2 | s:saw note:C#5 partials:[1 1 0 1 0 1 1 1] ]", + "[ 5/2 → 21/8 | s:saw note:E4 partials:[1 1 1 0 1 1 0] ]", + "[ 21/8 → 11/4 | s:saw note:A#3 partials:[1 0 1 0 1 0] ]", + "[ 11/4 → 23/8 | s:saw note:G#4 partials:[1 0 1 0 0 0 1 1] ]", + "[ 23/8 → 3/1 | s:saw note:G#3 partials:[1 0 1] ]", + "[ 3/1 → 25/8 | s:saw note:B3 partials:[1 1 1 0 0 0] ]", + "[ 25/8 → 13/4 | s:saw note:D#4 partials:[1 0 1 1 0 0 0] ]", + "[ 13/4 → 27/8 | s:saw note:D#5 partials:[1 1 1 1 1 1 1 1] ]", + "[ 27/8 → 7/2 | s:saw note:D#4 partials:[1 1 0 1 0 0 1] ]", + "[ 7/2 → 29/8 | s:saw note:D#4 partials:[1 1 0 1 0 0 1] ]", + "[ 29/8 → 15/4 | s:saw note:D#5 partials:[1 1 1 1 0 0 0 1] ]", + "[ 15/4 → 31/8 | s:saw note:B4 partials:[1 1 0 1 0 0 0 0] ]", + "[ 31/8 → 4/1 | s:saw note:A#4 partials:[1 0 1 1 1 1 0 1] ]", ] `; @@ -7580,38 +7580,70 @@ exports[`runs examples > example "phasersweep" example index 0 1`] = ` exports[`runs examples > example "phases" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:saw note:G#3 partials:[1 0 0 0 0 0 0 0 0] phases:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] ]", - "[ 1/8 → 1/4 | s:saw note:A#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.6852155700325966 0.17723728902637959 0.8521428182721138 0.5022399611771107 0.9780306946486235 0.3186125475913286 0.1851645242422819 0.8495976086705923 0.9832699615508318 0.8203876558691263 0.7365445382893085 0.6727468091994524 0.4794053863734007 0.35482912696897984 0.5666771996766329 0.4527092967182398 0.5699347071349621 0.7358296867460012 0.5444891396909952 0.11270620673894882] ]", - "[ 1/4 → 3/8 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.36975969187915325 0.18273563869297504 0.012781793251633644 0.5423613861203194 0.12467948533594608 0.7188410349190235 0.856887087225914 0.8818203993141651 0.8917219173163176 0.110306391492486 0.07148400321602821 0.8493648078292608 0.9100127145648003 0.4820226952433586 0.037094997242093086 0.9857278112322092 0.23439379036426544 0.46701666340231895 0.15832693874835968 0.6921216659247875] ]", - "[ 3/8 → 1/2 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.40139251574873924 0.15799915604293346 0.9604704882949591 0.03873417526483536 0.4157217647880316 0.27756719291210175 0.8800551909953356 0.5692523363977671 0.11026228219270706 0.7190891150385141 0.404962956905365 0.50828124769032 0.9681975357234478 0.34809120930731297 0.9345223866403103 0.9851128943264484 0.11768617853522301 0.538263589143753 0.6650313660502434 0.02540053054690361] ]", - "[ 1/2 → 5/8 | s:saw note:C#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.2604806162416935 0.3654713351279497 0.9900747090578079 0.22572625242173672 0.9328999016433954 0.5273839123547077 0.7047769222408533 0.7311522178351879 0.42673745937645435 0.7099553178995848 0.5597201306372881 0.8968746215105057 0.15695763938128948 0.3792166206985712 0.4147114269435406 0.38739512488245964 0.7585489805787802 0.3875726908445358 0.867314238101244 0.1866922564804554] ]", - "[ 5/8 → 3/4 | s:saw note:A#3 partials:[1 0 0 0 0 0 0 0 0] phases:[0.1356358677148819 0.5403102282434702 0.07321739941835403 0.9646544624119997 0.7707359045743942 0.46888123638927937 0.9489036612212658 0.366748945787549 0.5029341224581003 0.7194344792515039 0.282692551612854 0.7303404528647661 0.7155798356980085 0.637981578707695 0.39654020965099335 0.008769871667027473 0.5857728160917759 0.3513293471187353 0.43080931156873703 0.518209295347333] ]", - "[ 3/4 → 7/8 | s:saw note:B3 partials:[1 0 0 0 0 0 0 0 0] phases:[0.19582648016512394 0.0024610888212919235 0.08272589556872845 0.09592138417065144 0.9678201265633106 0.3249557167291641 0.9463537875562906 0.20766610652208328 0.19126702845096588 0.4482936095446348 0.23314787074923515 0.07724925130605698 0.8329483829438686 0.39804019033908844 0.2529231123626232 0.889951054006815 0.716364661231637 0.3905949704349041 0.990752438083291 0.275751456618309] ]", - "[ 7/8 → 1/1 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.3976310808211565 0.13476407527923584 0.963376859202981 0.6579397786408663 0.5203975513577461 0.865439185872674 0.5583905670791864 0.2736878804862499 0.17021040990948677 0.3456706069409847 0.5848060417920351 0.9889458417892456 0.9307208992540836 0.7148868907243013 0.7419579364359379 0.8103639334440231 0.3331365995109081 0.19130694679915905 0.03261224366724491 0.808204049244523] ]", - "[ 1/1 → 9/8 | s:saw note:F#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.5195421651005745 0.9349692352116108 0.1718774326145649 0.9601866770535707 0.2802200373262167 0.8732441551983356 0.01810968853533268 0.42737805284559727 0.26281314715743065 0.7521175239235163 0.568607984110713 0.5046361442655325 0.03172125294804573 0.3706745784729719 0.9402780849486589 0.3995086643844843 0.35769628174602985 0.023460431024432182 0.043030962347984314 0.6526827793568373] ]", - "[ 9/8 → 5/4 | s:saw note:A#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.6724895145744085 0.21410975232720375 0.5229047331959009 0.20957534946501255 0.7402758821845055 0.2681974694132805 0.4059371296316385 0.9422509074211121 0.6584139950573444 0.26589646376669407 0.16059227287769318 0.17166719026863575 0.6234225388616323 0.020276052877306938 0.4836352560669184 0.8991366866976023 0.1860280428081751 0.06238717399537563 0.4383583478629589 0.4902789145708084] ]", - "[ 5/4 → 11/8 | s:saw note:A#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.7287282031029463 0.9341024849563837 0.5098182689398527 0.5608645845204592 0.6812942810356617 0.5015129633247852 0.4602120481431484 0.927369873970747 0.19313151575624943 0.5240397155284882 0.444337897002697 0.19362097792327404 0.7068767864257097 0.920799495652318 0.2648108974099159 0.9700228478759527 0.20331068895757198 0.38390261493623257 0.0038731005042791367 0.9156702514737844] ]", - "[ 11/8 → 3/2 | s:saw note:B3 partials:[1 0 0 0 0 0 0 0 0] phases:[0.18280375190079212 0.40527783520519733 0.30504362285137177 0.7622128054499626 0.842598931863904 0.053796686232089996 0.5124214049428701 0.9178454764187336 0.09979427233338356 0.8280061967670918 0.5572535842657089 0.0622002687305212 0.9205668028444052 0.7738517206162214 0.04644870012998581 0.2117986585944891 0.26436166651546955 0.26421429589390755 0.4649084359407425 0.10490566119551659] ]", - "[ 3/2 → 13/8 | s:saw note:G#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.6084080748260021 0.4424846563488245 0.755185678601265 0.04328125901520252 0.04244415462017059 0.3994515985250473 0.787989066913724 0.3180440980941057 0.30072982981801033 0.24217353947460651 0.7830625418573618 0.7733921892940998 0.7720277719199657 0.4402343425899744 0.21046430803835392 0.38635879568755627 0.5671729445457458 0.9510521050542593 0.010756833478808403 0.5728995501995087] ]", - "[ 13/8 → 7/4 | s:saw note:E4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.49675471149384975 0.2965749856084585 0.09848833456635475 0.19172007404267788 0.22677889838814735 0.9841996673494577 0.11156083643436432 0.6079028844833374 0.9412728808820248 0.9376902114599943 0.3611182738095522 0.5901201106607914 0.26860327646136284 0.9782364591956139 0.18694544956088066 0.004641396924853325 0.42020696587860584 0.06209231913089752 0.4790260009467602 0.21088780276477337] ]", - "[ 7/4 → 15/8 | s:saw note:B3 partials:[1 0 0 0 0 0 0 0 0] phases:[0.20473789609968662 0.7480021081864834 0.30757393687963486 0.5827204789966345 0.22094514779746532 0.6370698790997267 0.16573002003133297 0.0638319905847311 0.12625465169548988 0.4565841108560562 0.46095744147896767 0.646710304543376 0.045173922553658485 0.4833248760551214 0.1326297614723444 0.5275116711854935 0.9871038906276226 0.424171743914485 0.761672617867589 0.08582597225904465] ]", - "[ 15/8 → 2/1 | s:saw note:G#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.5945571791380644 0.3962489552795887 0.563431927934289 0.4742323160171509 0.4185752831399441 0.9703940469771624 0.5282467231154442 0.6896266285330057 0.6941124945878983 0.2423656489700079 0.225077323615551 0.5445358455181122 0.014737963676452637 0.2848400343209505 0.14955217391252518 0.9782383926212788 0.6327074971050024 0.23327310755848885 0.4324392694979906 0.4326172359287739] ]", - "[ 2/1 → 17/8 | s:saw note:D#5 partials:[1 0 0 0 0 0 0 0 0] phases:[0.9595271199941635 0.5427457969635725 0.45864437520504 0.8057199101895094 0.18388565629720688 0.17221237532794476 0.8838487900793552 0.4976818822324276 0.7543560564517975 0.34416236728429794 0.8638174068182707 0.5697487238794565 0.005243342369794846 0.6361143626272678 0.6039986703544855 0.38708674535155296 0.5463927835226059 0.5717255529016256 0.15141930803656578 0.22688637301325798] ]", - "[ 17/8 → 9/4 | s:saw note:C#5 partials:[1 0 0 0 0 0 0 0 0] phases:[0.9033902939409018 0.22919894196093082 0.8388008493930101 0.9108077362179756 0.32246968522667885 0.16122018359601498 0.31932324543595314 0.18524732999503613 0.19110161997377872 0.5346284378319979 0.38531880639493465 0.7401201985776424 0.07450124807655811 0.18417961709201336 0.5768439751118422 0.7161206863820553 0.5836263168603182 0.9591280855238438 0.7728104889392853 0.0006709620356559753] ]", - "[ 9/4 → 19/8 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.34548251144587994 0.30419893004000187 0.10205957666039467 0.9906709585338831 0.3156223688274622 0.3745057098567486 0.7668170686811209 0.9379972033202648 0.689277408644557 0.8690325897186995 0.36728161945939064 0.9668608456850052 0.7341883443295956 0.32575040124356747 0.8095720671117306 0.3182998225092888 0.38158727809786797 0.5534052699804306 0.5973556581884623 0.752589326351881] ]", - "[ 19/8 → 5/2 | s:saw note:C#5 partials:[1 0 0 0 0 0 0 0 0] phases:[0.8365066405385733 0.5369812995195389 0.2263860274106264 0.21759207546710968 0.2172116208821535 0.09454222768545151 0.8075542915612459 0.9017798062413931 0.46291174553334713 0.01372767798602581 0.3509599883109331 0.4871185813099146 0.3525365497916937 0.6883120182901621 0.19577431678771973 0.287217665463686 0.025726469233632088 0.11798650585114956 0.8464976660907269 0.981348654255271] ]", - "[ 5/2 → 21/8 | s:saw note:E4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.45861607417464256 0.6807645913213491 0.07009582966566086 0.1450389064848423 0.2579921595752239 0.612881500273943 0.5503941811621189 0.7027011960744858 0.681745121255517 0.24246043153107166 0.2981361001729965 0.02195165678858757 0.10680225491523743 0.9413154497742653 0.6108828671276569 0.9479686040431261 0.17599895782768726 0.6629563421010971 0.5875700414180756 0.05703482776880264] ]", - "[ 21/8 → 11/4 | s:saw note:A#3 partials:[1 0 0 0 0 0 0 0 0] phases:[0.16019348427653313 0.47758268006145954 0.9749126061797142 0.8025561925023794 0.3600110989063978 0.324309878051281 0.5655391272157431 0.13364790193736553 0.3045873437076807 0.3814875278621912 0.14350778982043266 0.6017840709537268 0.11792952008545399 0.7185723781585693 0.6789924055337906 0.43416675738990307 0.9378792773932219 0.48162082210183144 0.37555896304547787 0.4686833508312702] ]", - "[ 11/4 → 23/8 | s:saw note:G#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.6332327704876661 0.7342763151973486 0.16935866326093674 0.4754706546664238 0.528122790157795 0.21934260986745358 0.7476693484932184 0.2995396424084902 0.46688378043472767 0.9741295631974936 0.9878341723233461 0.9837898630648851 0.15651432052254677 0.2896903567016125 0.7395600061863661 0.3490046579390764 0.014511989429593086 0.6857758145779371 0.7405510656535625 0.4224672969430685] ]", - "[ 23/8 → 3/1 | s:saw note:G#3 partials:[1 0 0 0 0 0 0 0 0] phases:[0.01625417172908783 0.28409438766539097 0.3075305689126253 0.40343056432902813 0.5556836389005184 0.6926821582019329 0.5635769125074148 0.017873913049697876 0.5601800158619881 0.28681862354278564 0.8022358678281307 0.13467839546501637 0.02351163513958454 0.1987263821065426 0.4515750799328089 0.0009761657565832138 0.11555273085832596 0.25053937546908855 0.29571316950023174 0.15751986764371395] ]", - "[ 3/1 → 25/8 | s:saw note:B3 partials:[1 0 0 0 0 0 0 0 0] phases:[0.21728911064565182 0.7605195846408606 0.11240843310952187 0.4372697602957487 0.15630115941166878 0.1653979979455471 0.5303416550159454 0.07218753732740879 0.47459222190082073 0.04051801189780235 0.8742353077977896 0.9356274232268333 0.6662059463560581 0.4364917427301407 0.4163493011146784 0.9820694588124752 0.09098831936717033 0.41974459774792194 0.05833998881280422 0.2601191997528076] ]", - "[ 25/8 → 13/4 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.34324387833476067 0.14098095521330833 0.012934491038322449 0.827436288818717 0.5283997394144535 0.8942463714629412 0.4924008697271347 0.9298017006367445 0.18076439201831818 0.5243716649711132 0.015810951590538025 0.7476964127272367 0.6734520178288221 0.3267945274710655 0.9760967157781124 0.9397075213491917 0.7225867230445147 0.938586825504899 0.9692913070321083 0.5398030783981085] ]", - "[ 13/4 → 27/8 | s:saw note:D#5 partials:[1 0 0 0 0 0 0 0 0] phases:[0.9923650715500116 0.7797339502722025 0.1992435697466135 0.9005183521658182 0.718992218375206 0.23745290748775005 0.28872333094477654 0.35448253713548183 0.6209003105759621 0.3761858306825161 0.019171399995684624 0.7563913837075233 0.1559751983731985 0.4236980527639389 0.08094430714845657 0.4560011047869921 0.5958948992192745 0.37061405926942825 0.18633292242884636 0.346891064196825] ]", - "[ 27/8 → 7/2 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.40869180113077164 0.4018078800290823 0.34696186147630215 0.9572948440909386 0.400035472586751 0.3531211093068123 0.7240961641073227 0.8125612027943134 0.3587487991899252 0.1545814611017704 0.8934940584003925 0.9094721674919128 0.2336172368377447 0.17333386465907097 0.227950319647789 0.12384821102023125 0.5896956156939268 0.3293947223573923 0.619540523737669 0.8746719229966402] ]", - "[ 7/2 → 29/8 | s:saw note:D#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.40994881466031075 0.6455810181796551 0.35704658553004265 0.2732649203389883 0.31587288342416286 0.23243548162281513 0.22569947130978107 0.17241661623120308 0.7624858524650335 0.02111036144196987 0.6966175157576799 0.32112877257168293 0.2512516509741545 0.08602019399404526 0.16260642930865288 0.2627844326198101 0.1954369619488716 0.16302869468927383 0.4046020060777664 0.0904023926705122] ]", - "[ 29/8 → 15/4 | s:saw note:D#5 partials:[1 0 0 0 0 0 0 0 0] phases:[0.9391485787928104 0.3380728308111429 0.7723016366362572 0.998674126341939 0.7193406894803047 0.6963680125772953 0.9260678570717573 0.7829763628542423 0.42278125509619713 0.913721015676856 0.38873230293393135 0.7281809840351343 0.952108234167099 0.649707704782486 0.9456792045384645 0.4331315904855728 0.63712502643466 0.9180345926433802 0.7379776351153851 0.7914005517959595] ]", - "[ 15/4 → 31/8 | s:saw note:B4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.8121673222631216 0.25548945367336273 0.5759696904569864 0.6668333616107702 0.9233786761760712 0.11143362522125244 0.9734930321574211 0.369325939565897 0.6248745918273926 0.719582824036479 0.24051696807146072 0.20336504466831684 0.6860735435038805 0.10649923048913479 0.8736641593277454 0.042930178344249725 0.09769343212246895 0.7966452036052942 0.5882443580776453 0.8942952174693346] ]", - "[ 31/8 → 4/1 | s:saw note:A#4 partials:[1 0 0 0 0 0 0 0 0] phases:[0.7361665386706591 0.7657648548483849 0.21474426984786987 0.8228576295077801 0.5647660344839096 0.08148551359772682 0.9714624118059874 0.13992334716022015 0.9609981551766396 0.6844500284641981 0.23610286228358746 0.5974335502833128 0.20678778178989887 0.5261937081813812 0.7417268306016922 0.9810918122529984 0.8138748407363892 0.9389897901564837 0.7420910261571407 0.48839940316975117] ]", + "[ 0/1 → 1/8 | s:saw note:G#1 partials:{partials:1} ]", + "[ 0/1 → 1/8 | s:saw note:G#1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 1/8 → 1/4 | s:saw note:A#2 partials:{partials:1} ]", + "[ 1/8 → 1/4 | s:saw note:A#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 1/4 → 3/8 | s:saw note:D#2 partials:{partials:1} ]", + "[ 1/4 → 3/8 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 3/8 → 1/2 | s:saw note:D#2 partials:{partials:1} ]", + "[ 3/8 → 1/2 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 1/2 → 5/8 | s:saw note:C#2 partials:{partials:1} ]", + "[ 1/2 → 5/8 | s:saw note:C#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 5/8 → 3/4 | s:saw note:A#1 partials:{partials:1} ]", + "[ 5/8 → 3/4 | s:saw note:A#1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 3/4 → 7/8 | s:saw note:B1 partials:{partials:1} ]", + "[ 3/4 → 7/8 | s:saw note:B1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 7/8 → 1/1 | s:saw note:D#2 partials:{partials:1} ]", + "[ 7/8 → 1/1 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 1/1 → 9/8 | s:saw note:F#2 partials:{partials:1} ]", + "[ 1/1 → 9/8 | s:saw note:F#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 9/8 → 5/4 | s:saw note:A#2 partials:{partials:1} ]", + "[ 9/8 → 5/4 | s:saw note:A#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 5/4 → 11/8 | s:saw note:A#2 partials:{partials:1} ]", + "[ 5/4 → 11/8 | s:saw note:A#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 11/8 → 3/2 | s:saw note:B1 partials:{partials:1} ]", + "[ 11/8 → 3/2 | s:saw note:B1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 3/2 → 13/8 | s:saw note:G#2 partials:{partials:1} ]", + "[ 3/2 → 13/8 | s:saw note:G#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 13/8 → 7/4 | s:saw note:E2 partials:{partials:1} ]", + "[ 13/8 → 7/4 | s:saw note:E2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 7/4 → 15/8 | s:saw note:B1 partials:{partials:1} ]", + "[ 7/4 → 15/8 | s:saw note:B1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 15/8 → 2/1 | s:saw note:G#2 partials:{partials:1} ]", + "[ 15/8 → 2/1 | s:saw note:G#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 2/1 → 17/8 | s:saw note:D#3 partials:{partials:1} ]", + "[ 2/1 → 17/8 | s:saw note:D#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 17/8 → 9/4 | s:saw note:C#3 partials:{partials:1} ]", + "[ 17/8 → 9/4 | s:saw note:C#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 9/4 → 19/8 | s:saw note:D#2 partials:{partials:1} ]", + "[ 9/4 → 19/8 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 19/8 → 5/2 | s:saw note:C#3 partials:{partials:1} ]", + "[ 19/8 → 5/2 | s:saw note:C#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 5/2 → 21/8 | s:saw note:E2 partials:{partials:1} ]", + "[ 5/2 → 21/8 | s:saw note:E2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 21/8 → 11/4 | s:saw note:A#1 partials:{partials:1} ]", + "[ 21/8 → 11/4 | s:saw note:A#1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 11/4 → 23/8 | s:saw note:G#2 partials:{partials:1} ]", + "[ 11/4 → 23/8 | s:saw note:G#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 23/8 → 3/1 | s:saw note:G#1 partials:{partials:1} ]", + "[ 23/8 → 3/1 | s:saw note:G#1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 3/1 → 25/8 | s:saw note:B1 partials:{partials:1} ]", + "[ 3/1 → 25/8 | s:saw note:B1 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 25/8 → 13/4 | s:saw note:D#2 partials:{partials:1} ]", + "[ 25/8 → 13/4 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 13/4 → 27/8 | s:saw note:D#3 partials:{partials:1} ]", + "[ 13/4 → 27/8 | s:saw note:D#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 27/8 → 7/2 | s:saw note:D#2 partials:{partials:1} ]", + "[ 27/8 → 7/2 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 7/2 → 29/8 | s:saw note:D#2 partials:{partials:1} ]", + "[ 7/2 → 29/8 | s:saw note:D#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 29/8 → 15/4 | s:saw note:D#3 partials:{partials:1} ]", + "[ 29/8 → 15/4 | s:saw note:D#3 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 15/4 → 31/8 | s:saw note:B2 partials:{partials:1} ]", + "[ 15/4 → 31/8 | s:saw note:B2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", + "[ 31/8 → 4/1 | s:saw note:A#2 partials:{partials:1} ]", + "[ 31/8 → 4/1 | s:saw note:A#2 partials:{partials:1} phases:[0.5 0.5 0.5] ]", ] `; diff --git a/website/src/pages/learn/synths.mdx b/website/src/pages/learn/synths.mdx index 6fc5b6925..56b28c0e4 100644 --- a/website/src/pages/learn/synths.mdx +++ b/website/src/pages/learn/synths.mdx @@ -85,7 +85,7 @@ note("c2 >".fast(2)) ._scope()`} /> -which acts as a spectral filter or +which acts as a spectral filter. Or: >".fast(2)) -.sound("saw") -.partials(randL(100)) -.phases(randL(100)) -._scope()`} + tune={`s("saw").seg(16).n(irand(12)).scale("F1:minor") + .penv(48).panchor(0).pdec(0.05) + .delay(0.25).room(0.25) + .compressor(-20).vib(0.3) + .partials(randL(200)) + .phases(randL(200))`} /> ## Vibrato From 30cc46ea66a4cd3d9b5b7dca3ab6446fc76d6845 Mon Sep 17 00:00:00 2001 From: scrappy_fiddler Date: Mon, 17 Nov 2025 20:04:40 +0100 Subject: [PATCH 019/110] wchooseCycles has now notes in an example --- packages/core/signal.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 604d1c805..62fc26ad8 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -524,7 +524,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs); * @example * wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8) * @example - * wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s() + * wchooseCycles(["c c c",5], ["a a a",3], ["f f f",1]).fast(4).note() * @example * // The probability can itself be a pattern * wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s() From 99beb4454deb9099a40f355ba4dc3898f341adbe Mon Sep 17 00:00:00 2001 From: scrappy_fiddler Date: Mon, 17 Nov 2025 20:49:26 +0100 Subject: [PATCH 020/110] update snapshot with note example --- test/__snapshots__/examples.test.mjs.snap | 96 +++++++++++------------ 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 32e61c31d..31e4bddd3 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -12484,54 +12484,54 @@ exports[`runs examples > example "wchooseCycles" example index 0 1`] = ` exports[`runs examples > example "wchooseCycles" example index 1 1`] = ` [ - "[ 0/1 → 1/12 | s:bd ]", - "[ 1/12 → 1/6 | s:bd ]", - "[ 1/6 → 1/4 | s:bd ]", - "[ 1/4 → 1/3 | s:bd ]", - "[ 1/3 → 5/12 | s:bd ]", - "[ 5/12 → 1/2 | s:bd ]", - "[ 1/2 → 7/12 | s:sd ]", - "[ 7/12 → 2/3 | s:sd ]", - "[ 2/3 → 3/4 | s:sd ]", - "[ 3/4 → 5/6 | s:bd ]", - "[ 5/6 → 11/12 | s:bd ]", - "[ 11/12 → 1/1 | s:bd ]", - "[ 1/1 → 13/12 | s:bd ]", - "[ 13/12 → 7/6 | s:bd ]", - "[ 7/6 → 5/4 | s:bd ]", - "[ 5/4 → 4/3 | s:bd ]", - "[ 4/3 → 17/12 | s:bd ]", - "[ 17/12 → 3/2 | s:bd ]", - "[ 3/2 → 19/12 | s:hh ]", - "[ 19/12 → 5/3 | s:hh ]", - "[ 5/3 → 7/4 | s:hh ]", - "[ 7/4 → 11/6 | s:bd ]", - "[ 11/6 → 23/12 | s:bd ]", - "[ 23/12 → 2/1 | s:bd ]", - "[ 2/1 → 25/12 | s:hh ]", - "[ 25/12 → 13/6 | s:hh ]", - "[ 13/6 → 9/4 | s:hh ]", - "[ 9/4 → 7/3 | s:hh ]", - "[ 7/3 → 29/12 | s:hh ]", - "[ 29/12 → 5/2 | s:hh ]", - "[ 5/2 → 31/12 | s:bd ]", - "[ 31/12 → 8/3 | s:bd ]", - "[ 8/3 → 11/4 | s:bd ]", - "[ 11/4 → 17/6 | s:bd ]", - "[ 17/6 → 35/12 | s:bd ]", - "[ 35/12 → 3/1 | s:bd ]", - "[ 3/1 → 37/12 | s:bd ]", - "[ 37/12 → 19/6 | s:bd ]", - "[ 19/6 → 13/4 | s:bd ]", - "[ 13/4 → 10/3 | s:sd ]", - "[ 10/3 → 41/12 | s:sd ]", - "[ 41/12 → 7/2 | s:sd ]", - "[ 7/2 → 43/12 | s:hh ]", - "[ 43/12 → 11/3 | s:hh ]", - "[ 11/3 → 15/4 | s:hh ]", - "[ 15/4 → 23/6 | s:bd ]", - "[ 23/6 → 47/12 | s:bd ]", - "[ 47/12 → 4/1 | s:bd ]", + "[ 0/1 → 1/12 | note:c ]", + "[ 1/12 → 1/6 | note:c ]", + "[ 1/6 → 1/4 | note:c ]", + "[ 1/4 → 1/3 | note:c ]", + "[ 1/3 → 5/12 | note:c ]", + "[ 5/12 → 1/2 | note:c ]", + "[ 1/2 → 7/12 | note:f ]", + "[ 7/12 → 2/3 | note:f ]", + "[ 2/3 → 3/4 | note:f ]", + "[ 3/4 → 5/6 | note:c ]", + "[ 5/6 → 11/12 | note:c ]", + "[ 11/12 → 1/1 | note:c ]", + "[ 1/1 → 13/12 | note:c ]", + "[ 13/12 → 7/6 | note:c ]", + "[ 7/6 → 5/4 | note:c ]", + "[ 5/4 → 4/3 | note:c ]", + "[ 4/3 → 17/12 | note:c ]", + "[ 17/12 → 3/2 | note:c ]", + "[ 3/2 → 19/12 | note:a ]", + "[ 19/12 → 5/3 | note:a ]", + "[ 5/3 → 7/4 | note:a ]", + "[ 7/4 → 11/6 | note:c ]", + "[ 11/6 → 23/12 | note:c ]", + "[ 23/12 → 2/1 | note:c ]", + "[ 2/1 → 25/12 | note:a ]", + "[ 25/12 → 13/6 | note:a ]", + "[ 13/6 → 9/4 | note:a ]", + "[ 9/4 → 7/3 | note:a ]", + "[ 7/3 → 29/12 | note:a ]", + "[ 29/12 → 5/2 | note:a ]", + "[ 5/2 → 31/12 | note:c ]", + "[ 31/12 → 8/3 | note:c ]", + "[ 8/3 → 11/4 | note:c ]", + "[ 11/4 → 17/6 | note:c ]", + "[ 17/6 → 35/12 | note:c ]", + "[ 35/12 → 3/1 | note:c ]", + "[ 3/1 → 37/12 | note:c ]", + "[ 37/12 → 19/6 | note:c ]", + "[ 19/6 → 13/4 | note:c ]", + "[ 13/4 → 10/3 | note:f ]", + "[ 10/3 → 41/12 | note:f ]", + "[ 41/12 → 7/2 | note:f ]", + "[ 7/2 → 43/12 | note:a ]", + "[ 43/12 → 11/3 | note:a ]", + "[ 11/3 → 15/4 | note:a ]", + "[ 15/4 → 23/6 | note:c ]", + "[ 23/6 → 47/12 | note:c ]", + "[ 47/12 → 4/1 | note:c ]", ] `; From 654c1a86f08550586049fcef4a6b4c9ac5007755 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 18 Nov 2025 16:02:33 -0600 Subject: [PATCH 021/110] 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 dd2c808cd71a7f5892f7e57c0f0a6fe7540d332c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 19 Nov 2025 21:55:35 +0100 Subject: [PATCH 022/110] hotfix: reduce sounds-tab click to play latency --- website/src/repl/components/panel/SoundsTab.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index a9f10bf12..3d363f8a1 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -126,7 +126,7 @@ export function SoundsTab() { 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 time = ctx.currentTime + 0.05; const ref = await onTrigger(time, params, onended); trigRef.current = ref; if (ref?.node) { From 8144ec46bd7eeaf1d0cfbbe5c59a54a347941aeb Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 19 Nov 2025 18:35:35 -0600 Subject: [PATCH 023/110] 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 024/110] 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 8ed72b4573503890d04fffafee50e1f28cca9990 Mon Sep 17 00:00:00 2001 From: jeromew Date: Thu, 20 Nov 2025 13:05:41 +0000 Subject: [PATCH 025/110] [perf] fix `connect-leak` in `delay` effect --- packages/superdough/superdough.mjs | 3 ++- packages/superdough/superdoughoutput.mjs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 65c444120..c548e7396 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -692,7 +692,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // delay if (delay > 0 && delaytime > 0 && delayfeedback > 0) { orbitBus.getDelay(delaytime, delayfeedback, t); - orbitBus.sendDelay(post, delay); + const send = orbitBus.sendDelay(post, delay); + audioNodes.push(send) } // reverb if (room > 0) { diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs index a4235efd0..d8ead7d06 100644 --- a/packages/superdough/superdoughoutput.mjs +++ b/packages/superdough/superdoughoutput.mjs @@ -82,7 +82,7 @@ export class Orbit { } sendDelay(node, amount) { - effectSend(node, this.delayNode, amount); + return effectSend(node, this.delayNode, amount); } duck(t, onsettime = 0, attacktime = 0.1, depth = 1) { From d9f63b8dfa28112192ed5b6a2d2cfe7cb41cc41d Mon Sep 17 00:00:00 2001 From: jeromew Date: Sun, 16 Nov 2025 09:23:11 +0000 Subject: [PATCH 026/110] [perf] fix `connect leak` when .noise() is in the mix --- packages/superdough/helpers.mjs | 14 +++++++++++++- packages/superdough/noise.mjs | 3 ++- packages/superdough/synth.mjs | 23 +++++++++++++---------- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 47161ed61..de64df0e8 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -262,7 +262,15 @@ export function drywet(dry, wet, wetAmount = 0) { let mix = ac.createGain(); dry_gain.connect(mix); wet_gain.connect(mix); - return mix; + return { + node: mix, + onended: () => { + dry_gain.disconnect(mix); + wet_gain.disconnect(mix); + dry.disconnect(dry_gain); + wet.disconnect(wet_gain); + }, + }; } let curves = ['linear', 'exponential']; @@ -297,6 +305,10 @@ export function getVibratoOscillator(param, value, t) { gain.gain.value = vibmod * 100; vibratoOscillator.connect(gain); gain.connect(param); + vibratoOscillator.onended = () => { + gain.disconnect(param); + vibratoOscillator.disconnect(gain); + }; vibratoOscillator.start(t); return vibratoOscillator; } diff --git a/packages/superdough/noise.mjs b/packages/superdough/noise.mjs index 816dd252b..3e41515df 100644 --- a/packages/superdough/noise.mjs +++ b/packages/superdough/noise.mjs @@ -65,8 +65,9 @@ export function getNoiseOscillator(type = 'white', t, density = 0.02) { export function getNoiseMix(inputNode, wet, t) { const noiseOscillator = getNoiseOscillator('pink', t); const noiseMix = drywet(inputNode, noiseOscillator.node, wet); + noiseOscillator.node.onended = () => noiseMix.onended(); return { - node: noiseMix, + node: noiseMix.node, stop: (time) => noiseOscillator?.stop(time), }; } diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 10df1776b..2dc9e2be8 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -48,19 +48,17 @@ export function registerSynthSounds() { [0.001, 0.05, 0.6, 0.01], ); - let sound = getOscillator(s, t, value); - let { node: o, stop, triggerRelease } = sound; - // turn down const g = gainNode(0.3); - const { duration } = value; - - o.onended = () => { - o.disconnect(); + let sound = getOscillator(s, t, value, () => { g.disconnect(); onended(); - }; + }); + + let { node: o, stop, triggerRelease } = sound; + + const { duration } = value; const envGain = gainNode(1); let node = o.connect(g).connect(envGain); @@ -460,7 +458,7 @@ export function waveformN(partials, phases, type) { } // expects one of waveforms as s -export function getOscillator(s, t, value) { +export function getOscillator(s, t, value, onended) { const { duration, noise = 0 } = value; const partials = value.partials ?? value.n; let o; @@ -482,7 +480,6 @@ export function getOscillator(s, t, value) { } // set frequency o.frequency.value = getFrequencyFromValue(value); - o.start(t); let vibratoOscillator = getVibratoOscillator(o.detune, value, t); @@ -495,6 +492,12 @@ export function getOscillator(s, t, value) { noiseMix = getNoiseMix(o, noise, t); } + o.onended = () => { + noiseMix?.node.disconnect(); + onended(); + }; + o.start(t); + return { node: noiseMix?.node || o, stop: (time) => { From 7267990e20ed07c86b95a5c17985461e4f77f8d6 Mon Sep 17 00:00:00 2001 From: jeromew Date: Thu, 20 Nov 2025 13:35:37 +0000 Subject: [PATCH 027/110] Fix codeformat --- packages/superdough/superdough.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index c548e7396..650f9c2ce 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -693,7 +693,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) if (delay > 0 && delaytime > 0 && delayfeedback > 0) { orbitBus.getDelay(delaytime, delayfeedback, t); const send = orbitBus.sendDelay(post, delay); - audioNodes.push(send) + audioNodes.push(send); } // reverb if (room > 0) { From 0435711051ee5d890f15a11b8c2fdd0def231996 Mon Sep 17 00:00:00 2001 From: jeromew Date: Thu, 20 Nov 2025 17:36:00 +0000 Subject: [PATCH 028/110] [perf] fix `connect-leak` added by #1742 when noise() is not used --- packages/superdough/synth.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 2dc9e2be8..768638f04 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -493,6 +493,7 @@ export function getOscillator(s, t, value, onended) { } o.onended = () => { + o.disconnect(); noiseMix?.node.disconnect(); onended(); }; From 907f03f3bfe35bc4b6695490030e6f4822747629 Mon Sep 17 00:00:00 2001 From: jeromew Date: Thu, 20 Nov 2025 17:40:51 +0000 Subject: [PATCH 029/110] [perf] fix `connect-leak` in fm modulation --- packages/superdough/helpers.mjs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index de64df0e8..58c4aa35e 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -362,9 +362,9 @@ const mod = (freq, range = 1, type = 'sine') => { } osc.start(); - const g = new GainNode(ctx, { gain: range }); + const g = gainNode(range); osc.connect(g); // -range, range - return { node: g, stop: (t) => osc.stop(t) }; + return { node: g, stop: (t) => osc.stop(t), osc: osc }; }; const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => { const carrfreq = frequencyparam.value; @@ -416,6 +416,11 @@ export function applyFM(param, value, begin) { modulator.connect(envGain); envGain.connect(param); } + fmmod.osc.onended = () => { + envGain.disconnect(); + modulator.disconnect(); + fmmod.osc.disconnect(); + }; } return { stop }; } From 4758effdc6386fc6e1e38aeb0445db33a264b126 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 20 Nov 2025 18:22:44 -0600 Subject: [PATCH 030/110] Use frac due to negative frequencies from FM --- packages/superdough/worklets.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 359bc833a..32367b64c 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -532,7 +532,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(n)); // We must wrap this here because it is passed into sawblep below which // has domain [0, 1] - const dt = ffrac(freqVoice * INVSR); + const dt = frac(freqVoice * INVSR); this.phase[n] = this.phase[n] ?? Math.random(); const v = waveshapes.sawblep(this.phase[n], dt); From 21543956c6b26f3ae96dcf43f80f377e1a51e755 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 20 Nov 2025 18:26:12 -0600 Subject: [PATCH 031/110] Also for wavetable --- packages/superdough/worklets.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 32367b64c..e60cd223e 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1338,7 +1338,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const tablePos = clamp(pv(parameters.position, i), 0, 1); const idx = tablePos * (this.numFrames - 1); const fIdx = idx | 0; - const frac = idx - fIdx; + const interpT = idx - fIdx; const warpAmount = clamp(pv(parameters.warp, i), 0, 1); const warpMode = pv(parameters.warpMode, i); const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1); @@ -1368,13 +1368,13 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const ph = this._warpPhase(this.phase[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 = s0 + (s1 - s0) * frac; + let s = lerp(s0, s1, interpT); if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) { s = -s; } outL[i] += s * gainL * normalizer; outR[i] += s * gainR * normalizer; - this.phase[n] = ffrac(this.phase[n] + dPhase); + this.phase[n] = frac(this.phase[n] + dPhase); } } return true; From f637b8b0dd0c3bbe29c53d128a943b9e6260be9b Mon Sep 17 00:00:00 2001 From: jeromew Date: Thu, 20 Nov 2025 17:40:51 +0000 Subject: [PATCH 032/110] [perf] fix `connect-leak` in fm modulation --- packages/superdough/helpers.mjs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index de64df0e8..58c4aa35e 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -362,9 +362,9 @@ const mod = (freq, range = 1, type = 'sine') => { } osc.start(); - const g = new GainNode(ctx, { gain: range }); + const g = gainNode(range); osc.connect(g); // -range, range - return { node: g, stop: (t) => osc.stop(t) }; + return { node: g, stop: (t) => osc.stop(t), osc: osc }; }; const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => { const carrfreq = frequencyparam.value; @@ -416,6 +416,11 @@ export function applyFM(param, value, begin) { modulator.connect(envGain); envGain.connect(param); } + fmmod.osc.onended = () => { + envGain.disconnect(); + modulator.disconnect(); + fmmod.osc.disconnect(); + }; } return { stop }; } From e72e26eb81a62bcb9a96dedfa05fb82fac857cca Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 22 Nov 2025 17:11:17 -0500 Subject: [PATCH 033/110] add comment --- packages/core/repl.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 6e3d97c92..bcb70eec9 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -217,6 +217,7 @@ export function repl({ let patterns = []; let soloActive = false; for (const [key, value] of Object.entries(pPatterns)) { + // handle soloed patterns ex: S$: s("bd!4") const isSolod = key.length > 1 && key.startsWith('S'); if (isSolod && soloActive === false) { // first time we see a soloed pattern, clear existing patterns From 19fd0fc649ba8b92271facd3ae9e9e5ac01ebd2c Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 22 Nov 2025 17:17:41 -0500 Subject: [PATCH 034/110] rm unessecary file --- packages/draw/text.mjs | 144 ----------------------------------------- 1 file changed, 144 deletions(-) delete mode 100644 packages/draw/text.mjs diff --git a/packages/draw/text.mjs b/packages/draw/text.mjs deleted file mode 100644 index ba76df079..000000000 --- a/packages/draw/text.mjs +++ /dev/null @@ -1,144 +0,0 @@ -import { Pattern, midiToFreq, getFrequency } from '@strudel/core'; -import { getTheme, getDrawContext } from './draw.mjs'; - -const c = midiToFreq(36); - -const circlePos = (cx, cy, radius, angle) => { - angle = angle * Math.PI * 2; - const x = Math.sin(angle) * radius + cx; - const y = Math.cos(angle) * radius + cy; - return [x, y]; -}; - -const freq2angle = (freq, root) => { - return 0.5 - (Math.log2(freq / root) % 1); -}; - -export function pitchwheel({ - haps, - ctx, - id, - hapcircles = 1, - circle = 0, - edo = 12, - root = c, - thickness = 3, - hapRadius = 6, - mode = 'flake', - margin = 10, -} = {}) { - const connectdots = mode === 'polygon'; - const centerlines = mode === 'flake'; - const w = ctx.canvas.width; - const h = ctx.canvas.height; - ctx.clearRect(0, 0, w, h); - const color = getTheme().foreground; - - const size = Math.min(w, h); - const radius = size / 2 - thickness / 2 - hapRadius - margin; - const centerX = w / 2; - const centerY = h / 2; - - if (id) { - haps = haps.filter((hap) => hap.hasTag(id)); - } - ctx.strokeStyle = color; - ctx.fillStyle = color; - ctx.globalAlpha = 1; - ctx.lineWidth = thickness; - - if (circle) { - ctx.beginPath(); - ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI); - ctx.stroke(); - } - - if (edo) { - Array.from({ length: edo }, (_, i) => { - const angle = freq2angle(root * Math.pow(2, i / edo), root); - const [x, y] = circlePos(centerX, centerY, radius, angle); - ctx.beginPath(); - ctx.arc(x, y, hapRadius, 0, 2 * Math.PI); - ctx.fill(); - }); - ctx.stroke(); - } - - let shape = []; - ctx.lineWidth = hapRadius; - haps.forEach((hap) => { - let freq; - try { - freq = getFrequency(hap); - } catch (err) { - return; - } - const angle = freq2angle(freq, root); - const [x, y] = circlePos(centerX, centerY, radius, angle); - const hapColor = hap.value.color || color; - ctx.strokeStyle = hapColor; - ctx.fillStyle = hapColor; - const { velocity = 1, gain = 1 } = hap.value || {}; - const alpha = velocity * gain; - ctx.globalAlpha = alpha; - shape.push([x, y, angle, hapColor, alpha]); - ctx.beginPath(); - if (hapcircles) { - ctx.moveTo(x + hapRadius, y); - ctx.arc(x, y, hapRadius, 0, 2 * Math.PI); - ctx.fill(); - } - if (centerlines) { - ctx.moveTo(centerX, centerY); - ctx.lineTo(x, y); - } - ctx.stroke(); - }); - - ctx.strokeStyle = color; - ctx.globalAlpha = 1; - if (connectdots && shape.length) { - shape = shape.sort((a, b) => a[2] - b[2]); - ctx.beginPath(); - ctx.moveTo(shape[0][0], shape[0][1]); - shape.forEach(([x, y, _, color, alpha]) => { - ctx.strokeStyle = color; - ctx.globalAlpha = alpha; - ctx.lineTo(x, y); - }); - ctx.lineTo(shape[0][0], shape[0][1]); - ctx.stroke(); - } - - return; -} - -/** - * Renders a pitch circle to visualize frequencies within one octave - * @name pitchwheel - * @param {number} hapcircles - * @param {number} circle - * @param {number} edo - * @param {string} root - * @param {number} thickness - * @param {number} hapRadius - * @param {string} mode - * @param {number} margin - * @example - * n("0 .. 12").scale("C:chromatic") - * .s("sawtooth") - * .lpf(500) - * ._pitchwheel() - */ -Pattern.prototype.pitchwheel = function (options = {}) { - let { ctx = getDrawContext(), id = 1 } = options; - return this.tag(id).onPaint((_, time, haps) => - pitchwheel({ - ...options, - time, - ctx, - haps: haps.filter((hap) => hap.isActive(time)), - id, - }), - ); -}; From 2efd56e33155096dd4454f9fe68e6b5f43bdc1e1 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 22 Nov 2025 18:10:28 -0500 Subject: [PATCH 035/110] add synonym --- packages/core/controls.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 9a2be7f44..8297746dc 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1840,7 +1840,7 @@ export const { octave } = registerControl('octave'); * s("~ sd ~ sd").delay(.5).delaytime(.125).orbit(2) * ) */ -export const { orbit } = registerControl('orbit'); +export const { orbit } = registerControl('orbit', 'o'); // TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that export const { overgain } = registerControl('overgain'); // TODO: what is this? not found in tidal doc. Similar to above, but limited to 1 From 1d5f3a4f30c415b53ea1b0a5d88d31d318725631 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 22 Nov 2025 18:21:24 -0500 Subject: [PATCH 036/110] prevent filter modulation pops --- packages/superdough/helpers.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index de64df0e8..8e45f7bca 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -238,7 +238,7 @@ export function createFilter(context, start, end, params, cps) { if (sync != null) { rate = cps * sync; } - const lfoValues = { depth, dcoffset, skew, shape, frequency: rate }; + const lfoValues = { depth, dcoffset, skew, shape, frequency: rate, min: 10, max: 20000 }; getParamLfo(context, frequencyParam, start, end, lfoValues); return filter; } From adc3a9ac6cb780c653c843c5c296c5fbb8f106b0 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 22 Nov 2025 20:51:30 -0600 Subject: [PATCH 037/110] Swap with temp variable --- packages/superdough/worklets.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index e60cd223e..bf633a63b 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -543,8 +543,9 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { if (pn >= 1.0) pn -= 1.0; this.phase[n] = pn; // invert right and left gain + const tmp = gainL; gainL = gainR; - gainR = gainL; + gainR = tmp; } } return true; From 30e672a0271bf3233859230ef96cfe8b9cecc1cd Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 22 Nov 2025 22:49:23 -0500 Subject: [PATCH 038/110] wip --- packages/superdough/helpers.mjs | 27 ++++++++++++++++++++++----- packages/superdough/superdough.mjs | 2 +- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 8e45f7bca..7bca637f2 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -191,8 +191,10 @@ export function applyParameterModulators(audioContext, param, start, end, envelo const lfo = getParamLfo(audioContext, param, start, end, lfoValues); return { lfo, disconnect: () => lfo?.disconnect() }; } - -export function createFilter(context, start, end, params, cps) { +function biloparExponential(x, k) { + return Math.sign(x) * 2 ** (Math.abs(x) * k); +} +export function createFilter(context, start, end, params, cps, cycle) { let { frequency, anchor, @@ -231,6 +233,7 @@ export function createFilter(context, start, end, params, cps) { const offset = envAbs * anchor; let min = clamp(2 ** -offset * frequency, 0, 20000); let max = clamp(2 ** (envAbs - offset) * frequency, 0, 20000); + if (env < 0) [min, max] = [max, min]; getParamADSR(frequencyParam, attack, decay, sustain, release, min, max, start, end, 'exponential'); } @@ -238,8 +241,22 @@ export function createFilter(context, start, end, params, cps) { if (sync != null) { rate = cps * sync; } - const lfoValues = { depth, dcoffset, skew, shape, frequency: rate, min: 10, max: 20000 }; - getParamLfo(context, frequencyParam, start, end, lfoValues); + const hasLFO = [depth, skew, shape, rate].some((v) => v !== undefined); + if (hasLFO) { + depth = depth ?? 1 + + const mag = Math.abs(depth); + const sign = Math.sign(depth); + + const shaped = 2 ** (mag * (1 - dcoffset)); + const dp = clamp((sign * shaped * frequency) - frequency, -20000, 20000); + console.info(dp) + const time = cycle / cps; + // let d = 2 ** -dcoffset + const lfoValues = { depth: dp, dcoffset, skew, shape, frequency: rate, min: 10, max: 20000, time }; + getParamLfo(context, frequencyParam, start, end, lfoValues); + } + return filter; } @@ -386,7 +403,7 @@ export function applyFM(param, value, begin) { duration, } = value; let modulator; - let stop = () => {}; + let stop = () => { }; if (fmModulationIndex) { const ac = getAudioContext(); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 650f9c2ce..4e1a145c0 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -559,7 +559,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) }; const lpParams = pickAndRename(value, lpMap); lpParams.type = 'lowpass'; - let lp = () => createFilter(ac, t, end, lpParams, cps); + let lp = () => createFilter(ac, t, end, lpParams, cps, cycle); chain.push(lp()); if (ftype === '24db') { chain.push(lp()); From 7bbb653963ace62fdf7f5aba33600da576927d02 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 22 Nov 2025 23:42:20 -0500 Subject: [PATCH 039/110] fixed --- packages/core/controls.mjs | 7 +++++++ packages/superdough/helpers.mjs | 28 +++++++++++++--------------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 9a2be7f44..299169973 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1294,6 +1294,8 @@ export const { fanchor } = registerControl('fanchor'); * * @name lprate * @param {number | Pattern} rate rate in hertz + * @example + * note("*16").s("sawtooth").lpf(600).lprate("<4 8 2 1>") */ export const { lprate } = registerControl('lprate'); @@ -1302,6 +1304,8 @@ export const { lprate } = registerControl('lprate'); * * @name lpsync * @param {number | Pattern} rate rate in cycles + * @example + * note("*16").s("sawtooth").lpf(600).lpsync("<4 8 2 1>") */ export const { lpsync } = registerControl('lpsync'); @@ -1310,7 +1314,10 @@ export const { lpsync } = registerControl('lpsync'); * * @name lpdepth * @param {number | Pattern} depth depth of modulation + * @example + * note("*16").s("sawtooth").lpf(600).lpdepth("<1 .5 1.8 0>") */ + export const { lpdepth } = registerControl('lpdepth'); /** diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 7bca637f2..65f5cc36d 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -191,9 +191,6 @@ export function applyParameterModulators(audioContext, param, start, end, envelo const lfo = getParamLfo(audioContext, param, start, end, lfoValues); return { lfo, disconnect: () => lfo?.disconnect() }; } -function biloparExponential(x, k) { - return Math.sign(x) * 2 ** (Math.abs(x) * k); -} export function createFilter(context, start, end, params, cps, cycle) { let { frequency, @@ -233,7 +230,6 @@ export function createFilter(context, start, end, params, cps, cycle) { const offset = envAbs * anchor; let min = clamp(2 ** -offset * frequency, 0, 20000); let max = clamp(2 ** (envAbs - offset) * frequency, 0, 20000); - if (env < 0) [min, max] = [max, min]; getParamADSR(frequencyParam, attack, decay, sustain, release, min, max, start, end, 'exponential'); } @@ -243,17 +239,19 @@ export function createFilter(context, start, end, params, cps, cycle) { } const hasLFO = [depth, skew, shape, rate].some((v) => v !== undefined); if (hasLFO) { - depth = depth ?? 1 - - const mag = Math.abs(depth); - const sign = Math.sign(depth); - - const shaped = 2 ** (mag * (1 - dcoffset)); - const dp = clamp((sign * shaped * frequency) - frequency, -20000, 20000); - console.info(dp) + depth = depth ?? 1; const time = cycle / cps; - // let d = 2 ** -dcoffset - const lfoValues = { depth: dp, dcoffset, skew, shape, frequency: rate, min: 10, max: 20000, time }; + const lfoValues = { + depth: (depth ?? 1) * frequency, + dcoffset, + skew, + shape, + frequency: rate ?? cps, + min: -frequency + 30, + max: 20000 - frequency, + time, + curve: 1, + }; getParamLfo(context, frequencyParam, start, end, lfoValues); } @@ -403,7 +401,7 @@ export function applyFM(param, value, begin) { duration, } = value; let modulator; - let stop = () => { }; + let stop = () => {}; if (fmModulationIndex) { const ac = getAudioContext(); From bde17594ceae18ed4a483715932b124638b62d72 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 23 Nov 2025 00:33:52 -0500 Subject: [PATCH 040/110] test --- test/__snapshots__/examples.test.mjs.snap | 207 ++++++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 31e4bddd3..4345eedbf 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -5969,6 +5969,75 @@ exports[`runs examples > example "lpdecay" example index 0 1`] = ` ] `; +exports[`runs examples > example "lpdepth" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 1/16 → 1/8 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 1/8 → 3/16 | note:c# s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 3/16 → 1/4 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 1/4 → 5/16 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 5/16 → 3/8 | note:c4 s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 7/16 → 1/2 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 1/2 → 9/16 | note:c# s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 9/16 → 5/8 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 11/16 → 3/4 | note:c4 s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 3/4 → 13/16 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 13/16 → 7/8 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 7/8 → 15/16 | note:c# s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 15/16 → 1/1 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 1/1 → 17/16 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 17/16 → 9/8 | note:c4 s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 19/16 → 5/4 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 5/4 → 21/16 | note:c# s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 21/16 → 11/8 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 23/16 → 3/2 | note:c4 s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 3/2 → 25/16 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 25/16 → 13/8 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 13/8 → 27/16 | note:c# s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 27/16 → 7/4 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 7/4 → 29/16 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 29/16 → 15/8 | note:c4 s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 31/16 → 2/1 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 2/1 → 33/16 | note:c# s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 33/16 → 17/8 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 35/16 → 9/4 | note:c4 s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 9/4 → 37/16 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 37/16 → 19/8 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 19/8 → 39/16 | note:c# s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 39/16 → 5/2 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 5/2 → 41/16 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 41/16 → 21/8 | note:c4 s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 43/16 → 11/4 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 11/4 → 45/16 | note:c# s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 45/16 → 23/8 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 47/16 → 3/1 | note:c4 s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 3/1 → 49/16 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 49/16 → 25/8 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 25/8 → 51/16 | note:c# s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 51/16 → 13/4 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 13/4 → 53/16 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 53/16 → 27/8 | note:c4 s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 55/16 → 7/2 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 7/2 → 57/16 | note:c# s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 57/16 → 29/8 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 59/16 → 15/4 | note:c4 s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 15/4 → 61/16 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 61/16 → 31/8 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 31/8 → 63/16 | note:c# s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 63/16 → 4/1 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", +] +`; + exports[`runs examples > example "lpenv" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c2 s:sawtooth cutoff:300 lpattack:0.5 lpenv:4 ]", @@ -6157,6 +6226,75 @@ exports[`runs examples > example "lpq" example index 0 1`] = ` ] `; +exports[`runs examples > example "lprate" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 1/16 → 1/8 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 1/8 → 3/16 | note:c# s:sawtooth cutoff:600 lprate:4 ]", + "[ 3/16 → 1/4 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 1/4 → 5/16 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 5/16 → 3/8 | note:c4 s:sawtooth cutoff:600 lprate:4 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 7/16 → 1/2 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 1/2 → 9/16 | note:c# s:sawtooth cutoff:600 lprate:4 ]", + "[ 9/16 → 5/8 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 11/16 → 3/4 | note:c4 s:sawtooth cutoff:600 lprate:4 ]", + "[ 3/4 → 13/16 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 13/16 → 7/8 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 7/8 → 15/16 | note:c# s:sawtooth cutoff:600 lprate:4 ]", + "[ 15/16 → 1/1 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 1/1 → 17/16 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 17/16 → 9/8 | note:c4 s:sawtooth cutoff:600 lprate:8 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 19/16 → 5/4 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 5/4 → 21/16 | note:c# s:sawtooth cutoff:600 lprate:8 ]", + "[ 21/16 → 11/8 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 23/16 → 3/2 | note:c4 s:sawtooth cutoff:600 lprate:8 ]", + "[ 3/2 → 25/16 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 25/16 → 13/8 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 13/8 → 27/16 | note:c# s:sawtooth cutoff:600 lprate:8 ]", + "[ 27/16 → 7/4 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 7/4 → 29/16 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 29/16 → 15/8 | note:c4 s:sawtooth cutoff:600 lprate:8 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 31/16 → 2/1 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 2/1 → 33/16 | note:c# s:sawtooth cutoff:600 lprate:2 ]", + "[ 33/16 → 17/8 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 35/16 → 9/4 | note:c4 s:sawtooth cutoff:600 lprate:2 ]", + "[ 9/4 → 37/16 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 37/16 → 19/8 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 19/8 → 39/16 | note:c# s:sawtooth cutoff:600 lprate:2 ]", + "[ 39/16 → 5/2 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 5/2 → 41/16 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 41/16 → 21/8 | note:c4 s:sawtooth cutoff:600 lprate:2 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 43/16 → 11/4 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 11/4 → 45/16 | note:c# s:sawtooth cutoff:600 lprate:2 ]", + "[ 45/16 → 23/8 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 47/16 → 3/1 | note:c4 s:sawtooth cutoff:600 lprate:2 ]", + "[ 3/1 → 49/16 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 49/16 → 25/8 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 25/8 → 51/16 | note:c# s:sawtooth cutoff:600 lprate:1 ]", + "[ 51/16 → 13/4 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 13/4 → 53/16 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 53/16 → 27/8 | note:c4 s:sawtooth cutoff:600 lprate:1 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 55/16 → 7/2 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 7/2 → 57/16 | note:c# s:sawtooth cutoff:600 lprate:1 ]", + "[ 57/16 → 29/8 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 59/16 → 15/4 | note:c4 s:sawtooth cutoff:600 lprate:1 ]", + "[ 15/4 → 61/16 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 61/16 → 31/8 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 31/8 → 63/16 | note:c# s:sawtooth cutoff:600 lprate:1 ]", + "[ 63/16 → 4/1 | note:c s:sawtooth cutoff:600 lprate:1 ]", +] +`; + exports[`runs examples > example "lprelease" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c2 s:sawtooth clip:0.5 cutoff:300 lpenv:4 lprelease:0.5 release:0.5 ]", @@ -6199,6 +6337,75 @@ exports[`runs examples > example "lpsustain" example index 0 1`] = ` ] `; +exports[`runs examples > example "lpsync" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 1/16 → 1/8 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 1/8 → 3/16 | note:c# s:sawtooth cutoff:600 lpsync:4 ]", + "[ 3/16 → 1/4 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 1/4 → 5/16 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 5/16 → 3/8 | note:c4 s:sawtooth cutoff:600 lpsync:4 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 7/16 → 1/2 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 1/2 → 9/16 | note:c# s:sawtooth cutoff:600 lpsync:4 ]", + "[ 9/16 → 5/8 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 11/16 → 3/4 | note:c4 s:sawtooth cutoff:600 lpsync:4 ]", + "[ 3/4 → 13/16 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 13/16 → 7/8 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 7/8 → 15/16 | note:c# s:sawtooth cutoff:600 lpsync:4 ]", + "[ 15/16 → 1/1 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 1/1 → 17/16 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 17/16 → 9/8 | note:c4 s:sawtooth cutoff:600 lpsync:8 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 19/16 → 5/4 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 5/4 → 21/16 | note:c# s:sawtooth cutoff:600 lpsync:8 ]", + "[ 21/16 → 11/8 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 23/16 → 3/2 | note:c4 s:sawtooth cutoff:600 lpsync:8 ]", + "[ 3/2 → 25/16 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 25/16 → 13/8 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 13/8 → 27/16 | note:c# s:sawtooth cutoff:600 lpsync:8 ]", + "[ 27/16 → 7/4 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 7/4 → 29/16 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 29/16 → 15/8 | note:c4 s:sawtooth cutoff:600 lpsync:8 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 31/16 → 2/1 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 2/1 → 33/16 | note:c# s:sawtooth cutoff:600 lpsync:2 ]", + "[ 33/16 → 17/8 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 35/16 → 9/4 | note:c4 s:sawtooth cutoff:600 lpsync:2 ]", + "[ 9/4 → 37/16 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 37/16 → 19/8 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 19/8 → 39/16 | note:c# s:sawtooth cutoff:600 lpsync:2 ]", + "[ 39/16 → 5/2 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 5/2 → 41/16 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 41/16 → 21/8 | note:c4 s:sawtooth cutoff:600 lpsync:2 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 43/16 → 11/4 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 11/4 → 45/16 | note:c# s:sawtooth cutoff:600 lpsync:2 ]", + "[ 45/16 → 23/8 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 47/16 → 3/1 | note:c4 s:sawtooth cutoff:600 lpsync:2 ]", + "[ 3/1 → 49/16 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 49/16 → 25/8 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 25/8 → 51/16 | note:c# s:sawtooth cutoff:600 lpsync:1 ]", + "[ 51/16 → 13/4 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 13/4 → 53/16 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 53/16 → 27/8 | note:c4 s:sawtooth cutoff:600 lpsync:1 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 55/16 → 7/2 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 7/2 → 57/16 | note:c# s:sawtooth cutoff:600 lpsync:1 ]", + "[ 57/16 → 29/8 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 59/16 → 15/4 | note:c4 s:sawtooth cutoff:600 lpsync:1 ]", + "[ 15/4 → 61/16 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 61/16 → 31/8 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 31/8 → 63/16 | note:c# s:sawtooth cutoff:600 lpsync:1 ]", + "[ 63/16 → 4/1 | note:c s:sawtooth cutoff:600 lpsync:1 ]", +] +`; + exports[`runs examples > example "lrate" example index 0 1`] = ` [ "[ 0/1 → 1/1 | n:0 s:supersquare leslie:1 lrate:1 ]", From cf12e3e1d8892de00d20a5c6f4e1a5a164cbc3f5 Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 23 Nov 2025 12:21:55 -0600 Subject: [PATCH 041/110] 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 042/110] 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 043/110] 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 044/110] 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 ( -