From 058d950121d8ae89656cf21dfd9568a0275585f9 Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 15 Aug 2025 22:57:59 -0500 Subject: [PATCH 001/194] Working version of updated randomness --- packages/core/signal.mjs | 62 +++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 6bd6fde6..2d3385ef 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -188,36 +188,46 @@ export const mouseX = signal(() => _mouseX); // random signals -const xorwise = (x) => { - const a = (x << 13) ^ x; - const b = (a >> 17) ^ a; - return (b << 5) ^ b; -}; +// Produce "Avalanche effect" where flipping a single bit of x +// results in all output bits flipping with probability 0.5 +// See e.g. https://github.com/aappleby/smhasher/blob/0ff96f7835817a27d0487325b6c16033e2992eb5/src/MurmurHash3.cpp#L68-L77 +function _murmurHashFinalizer(x) { + x |= 0; + x ^= x >>> 16; + x = Math.imul(x, 0x85ebca6b); + x ^= x >>> 13; + x = Math.imul(x, 0xc2b2ae35); + x ^= x >>> 16; + return x >>> 0; // unsigned +} -// stretch 300 cycles over the range of [0,2**29 == 536870912) then apply the xorshift algorithm -const _frac = (x) => x - Math.trunc(x); +// Used to decorrelate nearby t and i prior to hashing +function _decorrelate(t, i = 0) { + // const T = Math.floor(t * 4096); // set 2^12 resolution + const T = Math.floor(t * 536870912); // set 2^29 resolution + const lowBits = (T >>> 0) >>> 0; + const highBits = Math.floor(T / 4294967296) >>> 0; // 2^32 + let key = lowBits ^ Math.imul(highBits ^ 0x85ebca6b, 0xc2b2ae35); + key ^= Math.imul(i ^ 0x7f4a7c15, 0x9e3779b9); + return key >>> 0; + // return (T ^ Math.imul(i ^ 0x7f4a7c15, 0x9e3779b9)) >>> 0; +} -const timeToIntSeed = (x) => xorwise(Math.trunc(_frac(x / 300) * 536870912)); +function randAt(t, i = 0) { + return _murmurHashFinalizer(_decorrelate(t, i)) / 4294967296; // 2^32 +} -const intSeedToRand = (x) => (x % 536870912) / 536870912; +// N samples at time t +function timeToRands(t, n) { + const out = new Array(n); + for (let i = 0; i < n; i++) out[i] = randAt(t, i); + return out; +} -const timeToRand = (x) => Math.abs(intSeedToRand(timeToIntSeed(x))); - -const timeToRandsPrime = (seed, n) => { - const result = []; - // eslint-disable-next-line - for (let i = 0; i < n; ++i) { - result.push(intSeedToRand(seed)); - seed = xorwise(seed); - } - return result; -}; - -const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n); - -/** - * - */ +// Single sample at time t +function timeToRand(t) { + return randAt(t, 0); +} /** * A discrete pattern of numbers from 0 to n-1 From 03271ec5fb10e37e09c11751bfeafe3b28401f20 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 19 Aug 2025 18:15:24 -0500 Subject: [PATCH 002/194] Remove stray comments --- packages/core/signal.mjs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 2d3385ef..1c3ae464 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -203,14 +203,12 @@ function _murmurHashFinalizer(x) { // Used to decorrelate nearby t and i prior to hashing function _decorrelate(t, i = 0) { - // const T = Math.floor(t * 4096); // set 2^12 resolution const T = Math.floor(t * 536870912); // set 2^29 resolution const lowBits = (T >>> 0) >>> 0; const highBits = Math.floor(T / 4294967296) >>> 0; // 2^32 let key = lowBits ^ Math.imul(highBits ^ 0x85ebca6b, 0xc2b2ae35); key ^= Math.imul(i ^ 0x7f4a7c15, 0x9e3779b9); return key >>> 0; - // return (T ^ Math.imul(i ^ 0x7f4a7c15, 0x9e3779b9)) >>> 0; } function randAt(t, i = 0) { From 4a70ac5a998f5382541c9c999b7f05581b3e8143 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 20 Aug 2025 00:38:25 -0500 Subject: [PATCH 003/194] Working version of LFOs --- packages/core/controls.mjs | 5 + packages/superdough/superdough.mjs | 146 +++++++++++++++++++++++++---- packages/superdough/synth.mjs | 7 ++ 3 files changed, 141 insertions(+), 17 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index e6181d45..ecb94ea5 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1430,6 +1430,11 @@ export const { panorient } = registerControl('panorient'); // ['portamento'], // TODO: LFO rate see https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare export const { rate } = registerControl('rate'); +export const { lfoTarget } = registerControl('lfoTarget'); +export const { lfoParam } = registerControl('lfoParam'); +export const { lfoNum } = registerControl('lfoNum'); +export const { lfoBipolar } = registerControl('lfoBipolar'); +export const { lfoShape } = registerControl('lfoShape'); // TODO: slide param for certain synths export const { slide } = registerControl('slide'); // TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index eb1466d9..c55cbe0a 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -510,6 +510,66 @@ export function resetGlobalEffects() { orbits = {}; analysers = {}; analysersData = {}; + lfos = {}; + nodes = {}; +} + +function _getNodeParam(node, name) { + // Worklet case + if (node?.parameters) { + const p = node.parameters.get(name); + if (p instanceof AudioParam) { + return p; + } + } + // Built-in node case + const p = node?.[name]; + if (p instanceof AudioParam) { + return p; + } + return undefined; +} + +function _getNodeParams(node) { + const params = new Set(); + // Worklet case + if (node?.parameters) { + node.parameters.forEach((_v, k) => params.add(k)); + } + // Guesses based on common parameters + ["gain", "frequency", "detune", "Q", "pan", "playbackRate", "delayTime"] + .forEach((k) => { if (node?.[k] instanceof AudioParam) params.add(k); }); + return Array.from(params); +} + +function connectLFO(lfoNum, target, param, frequency, depth, shape, bipolar, start, end) { + debugger; + let lfoNode = lfos[lfoNum]; + const params = { + frequency, + depth, + } + if (lfoNode == null) { + const ac = getAudioContext(); + const dcoffset = bipolar > 0.5 ? -0.5 : 0; + lfoNode = getLfo(ac, start, 1e9, { frequency, depth, shape, dcoffset}); + lfos[lfoNum] = lfoNode; + } + lfoNode.disconnect(); + const targetNodes = nodes[target]; + targetNodes.forEach((targetNode) => { + if (targetNode === undefined) { + const keys = Object.keys(nodes); + errorLogger(new Error(`Could not connect to target ${target} -- it does not exist. Available options are ${keys.join(", ")}`), 'superdough'); + return; + } + const targetParam = _getNodeParam(targetNode, param); + if (targetParam === undefined) { + const parameters = _getNodeParams(targetNode); + errorLogger(new Error(`Could not connect to parameter ${param} on node ${target}. Available parameters are ${parameters.join(", ")}`), 'superdough'); + } + lfoNode.connect(targetParam); + }); } let activeSoundSources = new Map(); @@ -519,6 +579,9 @@ function mapChannelNumbers(channels) { return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); } +let nodes = {}; +let lfos = {}; + export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { // new: t is always expected to be the absolute target onset time const ac = getAudioContext(); @@ -628,6 +691,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) compressorKnee, compressorAttack, compressorRelease, + lfo, + lfoNum, + rate, + lfoTarget, + lfoParam, + lfoBipolar, + lfoShape, } = value; delaytime = delaytime ?? cycleToSeconds(delaysync, cps); @@ -681,7 +751,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) let sourceNode; if (source) { sourceNode = source(t, value, hapDuration, cps); + nodes['source'] = [sourceNode]; } else if (getSound(s)) { + debugger; const { onTrigger } = getSound(s); const onEnded = () => { audioNodes.forEach((n) => n?.disconnect()); @@ -692,6 +764,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) if (soundHandle) { sourceNode = soundHandle.node; activeSoundSources.set(chainID, soundHandle); + nodes['source'] = [soundHandle.oscillator]; } } else { throw new Error(`sound ${s} not found! Is it loaded?`); @@ -711,12 +784,14 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch })); // gain stage - chain.push(gainNode(gain)); + const initialGain = gainNode(gain); + nodes['gain'] = [initialGain]; + chain.push(initialGain); //filter const ftype = getFilterType(value.ftype); if (cutoff !== undefined) { - let lp = () => + const lp = () => createFilter( ac, 'lowpass', @@ -733,14 +808,18 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) ftype, drive, ); - chain.push(lp()); + const lp1 = lp(); + nodes['lpf'] = [lp1]; + chain.push(lp1); if (ftype === '24db') { - chain.push(lp()); + const lp2 = lp(); + nodes['lpf'].push(lp2); + chain.push(lp2); } } if (hcutoff !== undefined) { - let hp = () => + const hp = () => createFilter( ac, 'highpass', @@ -755,31 +834,56 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) end, fanchor, ); - chain.push(hp()); + const hp1 = hp(); + nodes['hpf'] = [hp1]; + chain.push(hp1); if (ftype === '24db') { - chain.push(hp()); + const hp2 = hp(); + nodes['hpf'].push(hp1); + chain.push(hp2); } } if (bandf !== undefined) { let bp = () => createFilter(ac, 'bandpass', bandf, bandq, bpattack, bpdecay, bpsustain, bprelease, bpenv, t, end, fanchor); - chain.push(bp()); + const bp1 = bp(); + nodes['bpf'] = [bp1]; + chain.push(bp1); if (ftype === '24db') { - chain.push(bp()); + const bp2 = bp(); + nodes['bpf'].push(bp2); + chain.push(bp2); } } if (vowel !== undefined) { const vowelFilter = ac.createVowelFilter(vowel); + nodes['vowel'] = vowelFilter; chain.push(vowelFilter); } // effects - coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse })); - crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); - shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol })); - distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol })); + if (coarse !== undefined) { + const coarseNode = getWorklet(ac, 'coarse-processor', { coarse }); + nodes['coarse'] = coarseNode; + chain.push(coarseNode); + } + if (crush !== undefined) { + const crushNode = getWorklet(ac, 'crush-processor', { crush }); + nodes['crush'] = crushNode; + chain.push(crushNode); + } + if (shape !== undefined) { + const shapeNode = getWorklet(ac, 'shape-processor', { shape }); + nodes['shape'] = shapeNode; + chain.push(shapeNode); + } + if (distort !== undefined) { + const distortNode = getWorklet(ac, 'distort-processor', { distort }); + nodes['distort'] = distortNode; + chain.push(distortNode); + } if (tremolosync != null) { tremolo = cps * tremolosync; @@ -808,10 +912,11 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) chain.push(amGain); } - compressorThreshold !== undefined && - chain.push( - getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease), - ); + if (compressorThreshold !== undefined) { + const compressorNode = getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease); + nodes['compressor'] = compressorNode; + chain.push(compressorNode); + } // panning if (pan !== undefined) { @@ -822,17 +927,20 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // phaser if (phaser !== undefined && phaserdepth > 0) { const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep); + nodes['phaser'] = phaserFX; chain.push(phaserFX); } // last gain const post = new GainNode(ac, { gain: postgain }); + nodes['post'] = post; chain.push(post); // delay let delaySend; if (delay > 0 && delaytime > 0 && delayfeedback > 0) { const delayNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels); + nodes['delay'] = delayNode; delaySend = effectSend(post, delayNode, delay); audioNodes.push(delaySend); } @@ -851,6 +959,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) roomIR = await loadBuffer(url, ac, ir, 0); } const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, orbitChannels); + nodes['room'] = reverbNode; reverbSend = effectSend(post, reverbNode, room); audioNodes.push(reverbSend); } @@ -874,6 +983,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // connect chain elements together chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); audioNodes = audioNodes.concat(chain); + + // finally, now that `nodes` is populated, set up LFOs + connectLFO(lfoNum, lfoTarget, lfoParam, rate, lfo, lfoBipolar, lfoShape, t, endWithRelease); }; export const superdoughTrigger = (t, hap, ct, cps) => { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 71138ae4..3e1122b5 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -87,6 +87,7 @@ export function registerSynthSounds() { stop(envEnd); return { node, + oscillator: o, stop: (endTime) => { stop(endTime); }, @@ -156,6 +157,7 @@ export function registerSynthSounds() { return { node, + oscillator: o, stop: (endTime) => { o.stop(endTime); }, @@ -222,6 +224,7 @@ export function registerSynthSounds() { return { node: envGain, + oscillator: o, stop: (time) => { timeoutNode.stop(time); }, @@ -298,6 +301,7 @@ export function registerSynthSounds() { return { node: envGain, + oscillator: o, stop: (time) => { timeoutNode.stop(time); }, @@ -375,6 +379,7 @@ export function registerSynthSounds() { return { node: envGain, + oscillator: o, stop: (time) => { timeoutNode.stop(time); }, @@ -420,6 +425,7 @@ export function registerSynthSounds() { stop(envEnd); return { node, + oscillator: o, stop: (endTime) => { stop(endTime); }, @@ -492,6 +498,7 @@ export function getOscillator(s, t, value) { return { node: noiseMix?.node || o, + oscillator: o, stop: (time) => { fmModulator.stop(time); vibratoOscillator?.stop(time); From efb337677ced73c765f394efe749bb90298a9953 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 20 Aug 2025 10:08:36 -0500 Subject: [PATCH 004/194] Fully working version with multiple LFOs --- packages/core/controls.mjs | 12 ++- packages/superdough/helpers.mjs | 4 +- packages/superdough/superdough.mjs | 119 ++++++++++++++++++++--------- packages/superdough/synth.mjs | 2 +- packages/superdough/worklets.mjs | 14 ++-- 5 files changed, 103 insertions(+), 48 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index ecb94ea5..83c085d0 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1428,13 +1428,17 @@ export const { panorient } = registerControl('panorient'); // ['pitch2'], // ['pitch3'], // ['portamento'], -// TODO: LFO rate see https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare -export const { rate } = registerControl('rate'); +export const { lfoNum } = registerControl('lfoNum'); export const { lfoTarget } = registerControl('lfoTarget'); export const { lfoParam } = registerControl('lfoParam'); -export const { lfoNum } = registerControl('lfoNum'); -export const { lfoBipolar } = registerControl('lfoBipolar'); +export const { lfoRate } = registerControl('lfoRate'); +export const { lfoDepth } = registerControl('lfoDepth'); +export const { lfoDCOffset } = registerControl('lfoDCOffset'); export const { lfoShape } = registerControl('lfoShape'); +export const { lfoSkew } = registerControl('lfoSkew'); +export const { lfoCurve } = registerControl('lfoCurve'); +export const { lfoSynced } = registerControl('lfoSynced'); + // TODO: slide param for certain synths export const { slide } = registerControl('slide'); // TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index c09acf1a..31a8064c 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -21,7 +21,9 @@ const getSlope = (y1, y2, x1, x2) => { export function getWorklet(ac, processor, params, config) { const node = new AudioWorkletNode(ac, processor, config); Object.entries(params).forEach(([key, value]) => { - node.parameters.get(key).value = value; + if (value !== undefined) { + node.parameters.get(key).value = value; + } }); return node; } diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index c55cbe0a..67f86cb8 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -345,31 +345,21 @@ function getDelay(orbit, delaytime, delayfeedback, t, channels) { return orbits[orbit].delayNode; } -export function getLfo(audioContext, begin, end, properties = {}) { - const { shape = 0, ...props } = properties; - const { dcoffset = -0.5, depth = 1 } = properties; +export function getLfo(audioContext, properties = {}) { + // Extract some params we need for deriving other params + const { begin, shape = 0, ...props } = properties; const lfoprops = { - frequency: 1, - depth, - skew: 0.5, - phaseoffset: 0, time: begin, - begin, - end, shape: getModulationShapeInput(shape), - dcoffset, - min: dcoffset * depth, - max: dcoffset * depth + depth, - curve: 1, ...props, }; return getWorklet(audioContext, 'lfo-processor', lfoprops); } -function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) { +function getPhaser(begin, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) { const ac = getAudioContext(); - const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 }); + const lfoGain = getLfo(ac, { frequency, depth: sweep * 2, begin, end }); //filters const numStages = 2; //num of filters in series @@ -542,34 +532,69 @@ function _getNodeParams(node) { return Array.from(params); } -function connectLFO(lfoNum, target, param, frequency, depth, shape, bipolar, start, end) { - debugger; +function _connectLFO(params) { + const { + frequency = 1, + synced = 0, + cps = 0.5, + lfoNum = 1, // default to LFO 1 + lfoTarget, + lfoParam, + ...filteredParams + } = params; + filteredParams['frequency'] = synced ? frequency / cps : frequency; let lfoNode = lfos[lfoNum]; - const params = { - frequency, - depth, - } if (lfoNode == null) { const ac = getAudioContext(); - const dcoffset = bipolar > 0.5 ? -0.5 : 0; - lfoNode = getLfo(ac, start, 1e9, { frequency, depth, shape, dcoffset}); + lfoNode = getLfo(ac, filteredParams); lfos[lfoNum] = lfoNode; } lfoNode.disconnect(); - const targetNodes = nodes[target]; + const targetNodes = nodes[lfoTarget]; + if (targetNodes === undefined) { + const keys = Object.keys(nodes); + errorLogger(new Error(`Could not connect to target ${lfoTarget} -- it does not exist. Available options are ${keys.join(", ")}`), 'superdough'); + return; + } targetNodes.forEach((targetNode) => { - if (targetNode === undefined) { - const keys = Object.keys(nodes); - errorLogger(new Error(`Could not connect to target ${target} -- it does not exist. Available options are ${keys.join(", ")}`), 'superdough'); - return; - } - const targetParam = _getNodeParam(targetNode, param); + const targetParam = _getNodeParam(targetNode, lfoParam); if (targetParam === undefined) { const parameters = _getNodeParams(targetNode); - errorLogger(new Error(`Could not connect to parameter ${param} on node ${target}. Available parameters are ${parameters.join(", ")}`), 'superdough'); + errorLogger(new Error(`Could not connect to parameter ${lfoParam} on node ${lfoTarget}. Available parameters are ${parameters.join(", ")}`), 'superdough'); } lfoNode.connect(targetParam); }); + const time = filteredParams.begin; + for (const [name, value] of Object.entries(filteredParams)) { + if (value == null) continue; + const p = lfoNode.parameters?.get(name); + if (p.cancelAndHoldAtTime) p.cancelAndHoldAtTime(time); + else p.cancelScheduledValues(time); + p.setValueAtTime(value, time); + } +} + +function connectLFOs(time, params) { + // We break down params specifying multiple LFOs into a set of parameters for + // a single LFO + const numLFOs = [ + [params.lfoNum].flat().length, + [params.lfoTarget].flat().length, + [params.lfoParam].flat().length, + ].reduce((a, v) => Math.max(a, v)); // Number of LFOs is the max as implied by these values + for (let i = 0; i < numLFOs; i++) { + let singleParams = {}; + for (const k in params) { + const v = params[k]; + const flatV = [v].flat(); + if (flatV.length !== numLFOs && flatV.length !== 1) { + errorLogger(new Error(`Could not setup LFOs. We derived ${numLFOs} as the intended number of LFOs, but ${k}: ${flatV} does not have matching length nor length 1`)); + return; + } + singleParams[k] = flatV[i] ?? flatV[0]; + } + _connectLFO(singleParams); + } } let activeSoundSources = new Map(); @@ -691,13 +716,16 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) compressorKnee, compressorAttack, compressorRelease, - lfo, lfoNum, - rate, lfoTarget, lfoParam, - lfoBipolar, + lfoRate, + lfoDepth, + lfoDCOffset, lfoShape, + lfoSkew, + lfoCurve, + lfoSynced, } = value; delaytime = delaytime ?? cycleToSeconds(delaysync, cps); @@ -753,7 +781,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) sourceNode = source(t, value, hapDuration, cps); nodes['source'] = [sourceNode]; } else if (getSound(s)) { - debugger; const { onTrigger } = getSound(s); const onEnded = () => { audioNodes.forEach((n) => n?.disconnect()); @@ -896,7 +923,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) const amGain = new GainNode(ac, { gain }); const time = cycle / cps; - const lfo = getLfo(ac, t, endWithRelease, { + const lfo = getLfo(ac, { skew: tremoloskew ?? (tremoloshape != null ? 0.5 : 1), frequency: tremolo, depth: tremolodepth, @@ -907,6 +934,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) min: 0, max: 1, curve: 1.5, + begin: t, + end: endWithRelease, }); lfo.connect(amGain.gain); chain.push(amGain); @@ -985,7 +1014,23 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) audioNodes = audioNodes.concat(chain); // finally, now that `nodes` is populated, set up LFOs - connectLFO(lfoNum, lfoTarget, lfoParam, rate, lfo, lfoBipolar, lfoShape, t, endWithRelease); + if (lfoTarget !== undefined && lfoParam !== undefined) { + connectLFOs(t, { + lfoNum, + lfoTarget, + lfoParam, + frequency: lfoRate, + depth: lfoDepth, + dcoffset: lfoDCOffset ?? 0, // override default value of 0.5 + shape: lfoShape, + skew: lfoSkew, + curve: lfoCurve, + persistent: 1, + begin: t, + synced: lfoSynced, + cps: cps, + }); + } }; export const superdoughTrigger = (t, hap, ct, cps) => { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 3e1122b5..a242bfdf 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -360,7 +360,7 @@ export function registerSynthSounds() { getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); let lfo; if (pwsweep != 0) { - lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep }); + lfo = getLfo(ac, { frequency: pwrate, depth: pwsweep, begin, end }); lfo.connect(o.parameters.get('pulsewidth')); } let timeoutNode = webAudioTimeout( diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 2406d56d..f6d7bbec 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -108,6 +108,7 @@ class LFOProcessor extends AudioWorkletProcessor { { name: 'dcoffset', defaultValue: 0 }, { name: 'min', defaultValue: 0 }, { name: 'max', defaultValue: 1 }, + { name: 'persistent', defaultValue: 0, min: 0, max: 1 }, // whether to ignore end ]; } @@ -123,9 +124,11 @@ class LFOProcessor extends AudioWorkletProcessor { } } - process(inputs, outputs, parameters) { + process(_inputs, outputs, parameters) { const begin = parameters['begin'][0]; - if (currentTime >= parameters.end[0]) { + const end = parameters['end'][0]; + const persistent = parameters['persistent'][0]; + if ((persistent < 0.5) && currentTime >= end) { return false; } if (currentTime <= begin) { @@ -143,8 +146,9 @@ class LFOProcessor extends AudioWorkletProcessor { const curve = parameters['curve'][0]; const dcoffset = parameters['dcoffset'][0]; - const min = parameters['min'][0]; - const max = parameters['max'][0]; + + const min = dcoffset * depth; + const max = dcoffset * depth + depth; const shape = waveShapeNames[parameters['shape'][0]]; const blockSize = output[0].length ?? 0; @@ -161,7 +165,7 @@ class LFOProcessor extends AudioWorkletProcessor { } this.incrementPhase(dt); } - + return true; } } From e25eab11ad63ff15ebd6651e9f6f00bcad596fd1 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 20 Aug 2025 10:47:57 -0500 Subject: [PATCH 005/194] Working version of envelopes --- packages/core/controls.mjs | 11 ++++ packages/superdough/superdough.mjs | 100 +++++++++++++++++++++++++++-- 2 files changed, 106 insertions(+), 5 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 83c085d0..9deede79 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1439,6 +1439,17 @@ export const { lfoSkew } = registerControl('lfoSkew'); export const { lfoCurve } = registerControl('lfoCurve'); export const { lfoSynced } = registerControl('lfoSynced'); + +export const { envNum } = registerControl('envNum'); +export const { envTarget } = registerControl('envTarget'); +export const { envParam } = registerControl('envParam'); +export const { envAttack } = registerControl('envAttack'); +export const { envDecay } = registerControl('envDecay'); +export const { envSustain } = registerControl('envSustain'); +export const { envRelease } = registerControl('envRelease'); +export const { envCurve } = registerControl('envCurve'); +export const { envDepth } = registerControl('envDepth'); + // TODO: slide param for certain synths export const { slide } = registerControl('slide'); // TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 67f86cb8..e01655cd 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -9,7 +9,7 @@ import './reverb.mjs'; import './vowel.mjs'; import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs'; import workletsUrl from './worklets.mjs?audioworklet'; -import { createFilter, gainNode, getCompressor, getWorklet, webAudioTimeout } from './helpers.mjs'; +import { createFilter, gainNode, getCompressor, getWorklet, webAudioTimeout, getADSRValues, getParamADSR } from './helpers.mjs'; import { map } from 'nanostores'; import { logger, errorLogger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; @@ -532,6 +532,7 @@ function _getNodeParams(node) { return Array.from(params); } +let lfos = {}; function _connectLFO(params) { const { frequency = 1, @@ -574,7 +575,7 @@ function _connectLFO(params) { } } -function connectLFOs(time, params) { +function connectLFOs(params) { // We break down params specifying multiple LFOs into a set of parameters for // a single LFO const numLFOs = [ @@ -597,6 +598,72 @@ function connectLFOs(time, params) { } } +function _connectEnvelope(params) { + const { + envNum = 1, // default to envelope 1 + envTarget, + envParam, + envDepth, + begin, + end, + attack, + decay, + sustain, + release, + curve, + ...filteredParams + } = params; + const targetNodes = nodes[envTarget]; + if (targetNodes === undefined) { + const keys = Object.keys(nodes); + errorLogger(new Error(`Could not connect to target ${envTarget} -- it does not exist. Available options are ${keys.join(", ")}`), 'superdough'); + return; + } + targetNodes.forEach((targetNode) => { + const targetParam = _getNodeParam(targetNode, envParam); + if (targetParam === undefined) { + const parameters = _getNodeParams(targetNode); + errorLogger(new Error(`Could not connect to parameter ${envParam} on node ${envTarget}. Available parameters are ${parameters.join(", ")}`), 'superdough'); + } + const [att, dec, sus, rel] = getADSRValues( + [ + attack, + decay, + sustain, + release, + ], + curve, + [0.005, 0.14, 0, 0.1] + ); + const min = 0; + const max = envDepth; + getParamADSR(targetParam, att, dec, sus, rel, min, max, begin, end, curve); + }); +} + +function connectEnvelopes(params) { + // We break down params specifying multiple envelopes into a set of parameters for + // a single envelopes + const numEnvelopes = [ + [params.envNum].flat().length, + [params.envTarget].flat().length, + [params.envParam].flat().length, + ].reduce((a, v) => Math.max(a, v)); // Number of envelopes is the max as implied by these values + for (let i = 0; i < numEnvelopes; i++) { + let singleParams = {}; + for (const k in params) { + const v = params[k]; + const flatV = [v].flat(); + if (flatV.length !== numEnvelopes && flatV.length !== 1) { + errorLogger(new Error(`Could not setup envelopes. We derived ${numEnvelopes} as the intended number of envelopes, but ${k}: ${flatV} does not have matching length nor length 1`)); + return; + } + singleParams[k] = flatV[i] ?? flatV[0]; + } + _connectEnvelope(singleParams); + } +} + let activeSoundSources = new Map(); //music programs/audio gear usually increments inputs/outputs from 1, we need to subtract 1 from the input because the webaudio API channels start at 0 @@ -605,7 +672,6 @@ function mapChannelNumbers(channels) { } let nodes = {}; -let lfos = {}; export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { // new: t is always expected to be the absolute target onset time @@ -726,6 +792,15 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) lfoSkew, lfoCurve, lfoSynced, + envNum, + envTarget, + envParam, + envAttack, + envDecay, + envSustain, + envRelease, + envCurve, + envDepth, } = value; delaytime = delaytime ?? cycleToSeconds(delaysync, cps); @@ -1013,9 +1088,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); audioNodes = audioNodes.concat(chain); - // finally, now that `nodes` is populated, set up LFOs + // finally, now that `nodes` is populated, set up LFOs and envelopes if (lfoTarget !== undefined && lfoParam !== undefined) { - connectLFOs(t, { + connectLFOs({ lfoNum, lfoTarget, lfoParam, @@ -1031,6 +1106,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) cps: cps, }); } + if (envTarget !== undefined && envParam !== undefined) { + connectEnvelopes({ + envNum, + envTarget, + envParam, + envDepth, + attack: envAttack, + decay: envDecay, + sustain: envSustain, + release: envRelease, + curve: envCurve, + begin: t, + end: endWithRelease, + }); + } }; export const superdoughTrigger = (t, hap, ct, cps) => { From d8f0d70908705263cb69957a14e6331a79ce3dd8 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 20 Aug 2025 13:22:37 -0500 Subject: [PATCH 006/194] Big refactor; start envelope at current value --- packages/core/controls.mjs | 2 - packages/superdough/superdough.mjs | 237 ++++++++++++++++------------- 2 files changed, 130 insertions(+), 109 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 9deede79..ddbfe98e 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1439,8 +1439,6 @@ export const { lfoSkew } = registerControl('lfoSkew'); export const { lfoCurve } = registerControl('lfoCurve'); export const { lfoSynced } = registerControl('lfoSynced'); - -export const { envNum } = registerControl('envNum'); export const { envTarget } = registerControl('envTarget'); export const { envParam } = registerControl('envParam'); export const { envAttack } = registerControl('envAttack'); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index e01655cd..6145b398 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -532,15 +532,100 @@ function _getNodeParams(node) { return Array.from(params); } +/** + * Split parameters (which might be arrays -- implying multiple-parameter modulation -- or a single number) into independent + * objects which only account for single-parameter modulation + * + * @param {Object} params - Dictionary of modulation parameters. + * @returns {Object[]} - Array of parameter objects, one per parameter modulation + */ +function _splitParams(params, countKeys) { + const num = ["num", "target", "parameter"] // names used to indicate individual parameter modulations + .map((k) => [params[k] ?? 0].flat().length) + .reduce((a, v) => Math.max(a, v), 1); + + const individualParams = []; + for (let i = 0; i < num; i++) { + const paramsI = {}; + for (const k in params) { + const flatV = [params[k]].flat(); + if (flatV.length !== num && flatV.length !== 1) { + errorLogger( + new Error( + `Could not set up modulations. We derived ${num} items, but ${k}: ${JSON.stringify(flatV)} has length ${flatV.length} (needs 1 or ${num}).` + ), + 'superdough' + ); + return []; + } + paramsI[k] = flatV[i] ?? flatV[0]; + } + individualParams.push(paramsI); + } + return individualParams; +} + +/** + * Given a node name and the name of a parameter on that node, attempt to retrieve + * all nodes corresponding to the name and their associated parameters + * + * Note that we say nodes, plural, because some nodes have multiple sub-nodes, like a + * 24db filter which is two filters in series + * + * @param {string} targetName - Name of the node to modulate parameters on (e.g. `lpf`, `source`, etc.) + * @param {string} paramName - Name of the parameter to modulate on that node + * @returns {AudioParam[]} - Array of audio parameter objects for modulation + * + */ +function _getTargetParams(targetName, paramName) { + const targetNodes = nodes[targetName]; + if (!targetNodes) { + const keys = Object.keys(nodes); + errorLogger( + new Error(`Could not connect to target '${targetName}' — it does not exist. Available targets: ${keys.join(", ")}`), + 'superdough' + ); + return []; + } + + const audioParams = []; + targetNodes.forEach((targetNode) => { + const targetParam = _getNodeParam(targetNode, paramName); + if (!targetParam) { + const available = _getNodeParams(targetNode); + errorLogger( + new Error( + `Could not connect to parameter '${paramName}' on '${targetName}'. Available parameters: ${available.join(", ")}` + ), + 'superdough' + ); + return; + } + audioParams.push(targetParam); + }); + return audioParams; +} + +function _setWorkletParamsAtTime(audioParams, params, time) { + for (const [name, value] of params) { + if (value == null) continue; + const p = audioParams.get(name); + if (p.cancelAndHoldAtTime) p.cancelAndHoldAtTime(time); + else p.cancelScheduledValues(time); + p.setValueAtTime(value, time); + } +} + let lfos = {}; function _connectLFO(params) { const { frequency = 1, synced = 0, cps = 0.5, - lfoNum = 1, // default to LFO 1 - lfoTarget, - lfoParam, + num = 1, // default to LFO 1 + target, + param, + begin, ...filteredParams } = params; filteredParams['frequency'] = synced ? frequency / cps : frequency; @@ -548,61 +633,22 @@ function _connectLFO(params) { if (lfoNode == null) { const ac = getAudioContext(); lfoNode = getLfo(ac, filteredParams); - lfos[lfoNum] = lfoNode; + lfos[num] = lfoNode; } - lfoNode.disconnect(); - const targetNodes = nodes[lfoTarget]; - if (targetNodes === undefined) { - const keys = Object.keys(nodes); - errorLogger(new Error(`Could not connect to target ${lfoTarget} -- it does not exist. Available options are ${keys.join(", ")}`), 'superdough'); - return; - } - targetNodes.forEach((targetNode) => { - const targetParam = _getNodeParam(targetNode, lfoParam); - if (targetParam === undefined) { - const parameters = _getNodeParams(targetNode); - errorLogger(new Error(`Could not connect to parameter ${lfoParam} on node ${lfoTarget}. Available parameters are ${parameters.join(", ")}`), 'superdough'); - } - lfoNode.connect(targetParam); - }); - const time = filteredParams.begin; - for (const [name, value] of Object.entries(filteredParams)) { - if (value == null) continue; - const p = lfoNode.parameters?.get(name); - if (p.cancelAndHoldAtTime) p.cancelAndHoldAtTime(time); - else p.cancelScheduledValues(time); - p.setValueAtTime(value, time); - } -} - -function connectLFOs(params) { - // We break down params specifying multiple LFOs into a set of parameters for - // a single LFO - const numLFOs = [ - [params.lfoNum].flat().length, - [params.lfoTarget].flat().length, - [params.lfoParam].flat().length, - ].reduce((a, v) => Math.max(a, v)); // Number of LFOs is the max as implied by these values - for (let i = 0; i < numLFOs; i++) { - let singleParams = {}; - for (const k in params) { - const v = params[k]; - const flatV = [v].flat(); - if (flatV.length !== numLFOs && flatV.length !== 1) { - errorLogger(new Error(`Could not setup LFOs. We derived ${numLFOs} as the intended number of LFOs, but ${k}: ${flatV} does not have matching length nor length 1`)); - return; - } - singleParams[k] = flatV[i] ?? flatV[0]; - } - _connectLFO(singleParams); + try { + lfoNode.disconnect(); + } catch { + // pass } + const targets = _getTargetParams(target, param); + targets.forEach((target) => lfoNode.connect(target)); + _setWorkletParamsAtTime(lfoNode.parameters, Object.entries(filteredParams), begin); } function _connectEnvelope(params) { const { - envNum = 1, // default to envelope 1 - envTarget, - envParam, + target, + param, envDepth, begin, end, @@ -613,54 +659,33 @@ function _connectEnvelope(params) { curve, ...filteredParams } = params; - const targetNodes = nodes[envTarget]; - if (targetNodes === undefined) { - const keys = Object.keys(nodes); - errorLogger(new Error(`Could not connect to target ${envTarget} -- it does not exist. Available options are ${keys.join(", ")}`), 'superdough'); - return; - } - targetNodes.forEach((targetNode) => { - const targetParam = _getNodeParam(targetNode, envParam); - if (targetParam === undefined) { - const parameters = _getNodeParams(targetNode); - errorLogger(new Error(`Could not connect to parameter ${envParam} on node ${envTarget}. Available parameters are ${parameters.join(", ")}`), 'superdough'); - } - const [att, dec, sus, rel] = getADSRValues( - [ - attack, - decay, - sustain, - release, - ], - curve, - [0.005, 0.14, 0, 0.1] - ); - const min = 0; - const max = envDepth; + const targets = _getTargetParams(target, param); + const [att, dec, sus, rel] = getADSRValues( + [ + attack, + decay, + sustain, + release, + ], + curve, + [0.005, 0.14, 0, 0.1] + ); + targets.forEach((targetParam) => { + const currentValue = targetParam.value; + const min = currentValue; + const max = currentValue + envDepth; getParamADSR(targetParam, att, dec, sus, rel, min, max, begin, end, curve); }); } -function connectEnvelopes(params) { - // We break down params specifying multiple envelopes into a set of parameters for - // a single envelopes - const numEnvelopes = [ - [params.envNum].flat().length, - [params.envTarget].flat().length, - [params.envParam].flat().length, - ].reduce((a, v) => Math.max(a, v)); // Number of envelopes is the max as implied by these values - for (let i = 0; i < numEnvelopes; i++) { - let singleParams = {}; - for (const k in params) { - const v = params[k]; - const flatV = [v].flat(); - if (flatV.length !== numEnvelopes && flatV.length !== 1) { - errorLogger(new Error(`Could not setup envelopes. We derived ${numEnvelopes} as the intended number of envelopes, but ${k}: ${flatV} does not have matching length nor length 1`)); - return; - } - singleParams[k] = flatV[i] ?? flatV[0]; - } - _connectEnvelope(singleParams); +function connectModulators(params, modulatorType) { + // We break down params specifying multiple modulators into a set of parameters for + // a single one + const individualParams = _splitParams(params); + if (modulatorType === "lfo") { + individualParams.forEach(_connectLFO); + } else if (modulatorType === "envelope") { + individualParams.forEach(_connectEnvelope); } } @@ -792,7 +817,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) lfoSkew, lfoCurve, lfoSynced, - envNum, envTarget, envParam, envAttack, @@ -1090,10 +1114,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // finally, now that `nodes` is populated, set up LFOs and envelopes if (lfoTarget !== undefined && lfoParam !== undefined) { - connectLFOs({ - lfoNum, - lfoTarget, - lfoParam, + connectModulators({ + num: lfoNum, + target: lfoTarget, + param: lfoParam, frequency: lfoRate, depth: lfoDepth, dcoffset: lfoDCOffset ?? 0, // override default value of 0.5 @@ -1104,13 +1128,12 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) begin: t, synced: lfoSynced, cps: cps, - }); + }, "lfo"); } if (envTarget !== undefined && envParam !== undefined) { - connectEnvelopes({ - envNum, - envTarget, - envParam, + connectModulators({ + target: envTarget, + param: envParam, envDepth, attack: envAttack, decay: envDecay, @@ -1119,7 +1142,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) curve: envCurve, begin: t, end: endWithRelease, - }); + }, "envelope"); } }; From 6a9739bed509839b5e6a4d7b9673f2f6853c63a1 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 20 Aug 2025 13:39:24 -0500 Subject: [PATCH 007/194] Code format and a typo on lfoNum --- packages/superdough/superdough.mjs | 131 +++++++++++++++-------------- packages/superdough/worklets.mjs | 4 +- 2 files changed, 69 insertions(+), 66 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 6145b398..97f3900d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -9,7 +9,15 @@ import './reverb.mjs'; import './vowel.mjs'; import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs'; import workletsUrl from './worklets.mjs?audioworklet'; -import { createFilter, gainNode, getCompressor, getWorklet, webAudioTimeout, getADSRValues, getParamADSR } from './helpers.mjs'; +import { + createFilter, + gainNode, + getCompressor, + getWorklet, + webAudioTimeout, + getADSRValues, + getParamADSR, +} from './helpers.mjs'; import { map } from 'nanostores'; import { logger, errorLogger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; @@ -527,8 +535,9 @@ function _getNodeParams(node) { node.parameters.forEach((_v, k) => params.add(k)); } // Guesses based on common parameters - ["gain", "frequency", "detune", "Q", "pan", "playbackRate", "delayTime"] - .forEach((k) => { if (node?.[k] instanceof AudioParam) params.add(k); }); + ['gain', 'frequency', 'detune', 'Q', 'pan', 'playbackRate', 'delayTime'].forEach((k) => { + if (node?.[k] instanceof AudioParam) params.add(k); + }); return Array.from(params); } @@ -540,7 +549,7 @@ function _getNodeParams(node) { * @returns {Object[]} - Array of parameter objects, one per parameter modulation */ function _splitParams(params, countKeys) { - const num = ["num", "target", "parameter"] // names used to indicate individual parameter modulations + const num = ['num', 'target', 'parameter'] // names used to indicate individual parameter modulations .map((k) => [params[k] ?? 0].flat().length) .reduce((a, v) => Math.max(a, v), 1); @@ -552,9 +561,9 @@ function _splitParams(params, countKeys) { if (flatV.length !== num && flatV.length !== 1) { errorLogger( new Error( - `Could not set up modulations. We derived ${num} items, but ${k}: ${JSON.stringify(flatV)} has length ${flatV.length} (needs 1 or ${num}).` + `Could not set up modulations. We derived ${num} items, but ${k}: ${JSON.stringify(flatV)} has length ${flatV.length} (needs 1 or ${num}).`, ), - 'superdough' + 'superdough', ); return []; } @@ -582,8 +591,10 @@ function _getTargetParams(targetName, paramName) { if (!targetNodes) { const keys = Object.keys(nodes); errorLogger( - new Error(`Could not connect to target '${targetName}' — it does not exist. Available targets: ${keys.join(", ")}`), - 'superdough' + new Error( + `Could not connect to target '${targetName}' — it does not exist. Available targets: ${keys.join(', ')}`, + ), + 'superdough', ); return []; } @@ -595,9 +606,9 @@ function _getTargetParams(targetName, paramName) { const available = _getNodeParams(targetNode); errorLogger( new Error( - `Could not connect to parameter '${paramName}' on '${targetName}'. Available parameters: ${available.join(", ")}` + `Could not connect to parameter '${paramName}' on '${targetName}'. Available parameters: ${available.join(', ')}`, ), - 'superdough' + 'superdough', ); return; } @@ -629,7 +640,7 @@ function _connectLFO(params) { ...filteredParams } = params; filteredParams['frequency'] = synced ? frequency / cps : frequency; - let lfoNode = lfos[lfoNum]; + let lfoNode = lfos[num]; if (lfoNode == null) { const ac = getAudioContext(); lfoNode = getLfo(ac, filteredParams); @@ -646,30 +657,9 @@ function _connectLFO(params) { } function _connectEnvelope(params) { - const { - target, - param, - envDepth, - begin, - end, - attack, - decay, - sustain, - release, - curve, - ...filteredParams - } = params; + const { target, param, envDepth, begin, end, attack, decay, sustain, release, curve, ...filteredParams } = params; const targets = _getTargetParams(target, param); - const [att, dec, sus, rel] = getADSRValues( - [ - attack, - decay, - sustain, - release, - ], - curve, - [0.005, 0.14, 0, 0.1] - ); + const [att, dec, sus, rel] = getADSRValues([attack, decay, sustain, release], curve, [0.005, 0.14, 0, 0.1]); targets.forEach((targetParam) => { const currentValue = targetParam.value; const min = currentValue; @@ -682,9 +672,9 @@ function connectModulators(params, modulatorType) { // We break down params specifying multiple modulators into a set of parameters for // a single one const individualParams = _splitParams(params); - if (modulatorType === "lfo") { + if (modulatorType === 'lfo') { individualParams.forEach(_connectLFO); - } else if (modulatorType === "envelope") { + } else if (modulatorType === 'envelope') { individualParams.forEach(_connectEnvelope); } } @@ -1041,7 +1031,14 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } if (compressorThreshold !== undefined) { - const compressorNode = getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease); + const compressorNode = getCompressor( + ac, + compressorThreshold, + compressorRatio, + compressorKnee, + compressorAttack, + compressorRelease, + ); nodes['compressor'] = compressorNode; chain.push(compressorNode); } @@ -1114,35 +1111,41 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // finally, now that `nodes` is populated, set up LFOs and envelopes if (lfoTarget !== undefined && lfoParam !== undefined) { - connectModulators({ - num: lfoNum, - target: lfoTarget, - param: lfoParam, - frequency: lfoRate, - depth: lfoDepth, - dcoffset: lfoDCOffset ?? 0, // override default value of 0.5 - shape: lfoShape, - skew: lfoSkew, - curve: lfoCurve, - persistent: 1, - begin: t, - synced: lfoSynced, - cps: cps, - }, "lfo"); + connectModulators( + { + num: lfoNum, + target: lfoTarget, + param: lfoParam, + frequency: lfoRate, + depth: lfoDepth, + dcoffset: lfoDCOffset ?? 0, // override default value of 0.5 + shape: lfoShape, + skew: lfoSkew, + curve: lfoCurve, + persistent: 1, + begin: t, + synced: lfoSynced, + cps: cps, + }, + 'lfo', + ); } if (envTarget !== undefined && envParam !== undefined) { - connectModulators({ - target: envTarget, - param: envParam, - envDepth, - attack: envAttack, - decay: envDecay, - sustain: envSustain, - release: envRelease, - curve: envCurve, - begin: t, - end: endWithRelease, - }, "envelope"); + connectModulators( + { + target: envTarget, + param: envParam, + envDepth, + attack: envAttack, + decay: envDecay, + sustain: envSustain, + release: envRelease, + curve: envCurve, + begin: t, + end: endWithRelease, + }, + 'envelope', + ); } }; diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index f6d7bbec..9ab552d4 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -128,7 +128,7 @@ class LFOProcessor extends AudioWorkletProcessor { const begin = parameters['begin'][0]; const end = parameters['end'][0]; const persistent = parameters['persistent'][0]; - if ((persistent < 0.5) && currentTime >= end) { + if (persistent < 0.5 && currentTime >= end) { return false; } if (currentTime <= begin) { @@ -165,7 +165,7 @@ class LFOProcessor extends AudioWorkletProcessor { } this.incrementPhase(dt); } - + return true; } } From 5d5159a08f62c171ab4477ff4aeca87caa5f6ba9 Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 24 Aug 2025 11:56:11 -0500 Subject: [PATCH 008/194] Add option to use old random and a benchmark --- packages/core/bench/signal.bench.mjs | 46 ++++++++++++++++++++++++++++ packages/core/signal.mjs | 35 ++++++++++++++++++++- website/src/repl/tunes.mjs | 19 ++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 packages/core/bench/signal.bench.mjs diff --git a/packages/core/bench/signal.bench.mjs b/packages/core/bench/signal.bench.mjs new file mode 100644 index 00000000..c79cedc0 --- /dev/null +++ b/packages/core/bench/signal.bench.mjs @@ -0,0 +1,46 @@ +import { describe, bench } from 'vitest'; + +import { calculateSteps, rand, useOldRandom } from '../index.mjs'; + +const testingResolution = 1024; + +const _generateRandomPattern = () => rand.iter(testingResolution).fast(testingResolution).firstCycle(); + +describe('random', () => { + calculateSteps(true); + bench( + '+tactus', + _generateRandomPattern, + { time: 1000 }, + ); + + calculateSteps(false); + bench( + '-tactus', + _generateRandomPattern, + { time: 1000 }, + ); +}); + +describe('old random', () => { + calculateSteps(true); + bench( + '+tactus', + () => { + useOldRandom(); + _generateRandomPattern(); + }, + { time: 1000 }, + ); + + calculateSteps(false); + bench( + '-tactus', + () => { + useOldRandom(); + _generateRandomPattern(); + }, + { time: 1000 }, + ); +}); +calculateSteps(true); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 1c3ae464..91901f86 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -186,7 +186,7 @@ export const mouseY = signal(() => _mouseY); export const mousex = signal(() => _mouseX); export const mouseX = signal(() => _mouseX); -// random signals +// Random number generators // Produce "Avalanche effect" where flipping a single bit of x // results in all output bits flipping with probability 0.5 @@ -215,8 +215,38 @@ function randAt(t, i = 0) { return _murmurHashFinalizer(_decorrelate(t, i)) / 4294967296; // 2^32 } +// Deprecated: Old random signals. Configuration `useOldRandom` may be used for legacy songs + +// stretch 300 cycles over the range of [0,2**29 == 536870912) then apply the xorshift algorithm +const __xorwise = (x) => { + const a = (x << 13) ^ x; + const b = (a >> 17) ^ a; + return (b << 5) ^ b; +}; +const __frac = (x) => x - Math.trunc(x); +const __timeToIntSeed = (x) => __xorwise(Math.trunc(__frac(x / 300) * 536870912)); +const __intSeedToRand = (x) => (x % 536870912) / 536870912; +const __timeToRand = (x) => Math.abs(__intSeedToRand(__timeToIntSeed(x))); +const __timeToRandsPrime = (seed, n) => { + const result = []; + for (let i = 0; i < n; i++) { + result.push(__intSeedToRand(seed)); + seed = __xorwise(seed); + } + return result; +}; +const __timeToRands = (t, n) => __timeToRandsPrime(__timeToIntSeed(t), n); + +// End old random + +let useOldRandomBool = false; +export const useOldRandom = (b = true) => useOldRandomBool = b; + // N samples at time t function timeToRands(t, n) { + if (useOldRandomBool) { + return __timeToRands(t, n); + } const out = new Array(n); for (let i = 0; i < n; i++) out[i] = randAt(t, i); return out; @@ -224,6 +254,9 @@ function timeToRands(t, n) { // Single sample at time t function timeToRand(t) { + if (useOldRandomBool) { + return __timeToRand(t); + } return randAt(t, 0); } diff --git a/website/src/repl/tunes.mjs b/website/src/repl/tunes.mjs index f1c72b12..23efb606 100644 --- a/website/src/repl/tunes.mjs +++ b/website/src/repl/tunes.mjs @@ -393,6 +393,8 @@ samples({ bass: { d2: 'https://cdn.freesound.org/previews/608/608286_13074022-lq.mp3' } }) +useOldRandom(true) + stack( // bells n("0").euclidLegato(3,8) @@ -430,6 +432,7 @@ export const festivalOfFingers3 = `// "Festival of fingers 3" // @by Felix Roos setcps(1) +useOldRandom(true) n("[-7*3],0,2,6,[8 7]") .echoWith( @@ -454,6 +457,8 @@ export const meltingsubmarine = `// "Melting submarine" // @by Felix Roos samples('github:tidalcycles/dirt-samples') +useOldRandom(true) + stack( s("bd:5,[~ ],hh27(3,4,1)") // drums .speed(perlin.range(.7,.9)) // random sample speed variation @@ -602,6 +607,9 @@ export const belldub = `// "Belldub" samples({ bell: {b4:'https://cdn.freesound.org/previews/339/339809_5121236-lq.mp3'}}) // "Hand Bells, B, Single.wav" by InspectorJ (www.jshaw.co.uk) of Freesound.org + +useOldRandom(true) + stack( // bass note("[0 ~] [2 [0 2]] [4 4*2] [[4 ~] [2 ~] 0@2]".scale('g1 dorian').superimpose(x=>x.add(.02))) @@ -638,6 +646,7 @@ export const dinofunk = `// "Dinofunk" // @by Felix Roos setcps(1) +useOldRandom(true) samples({bass:'https://cdn.freesound.org/previews/614/614637_2434927-hq.mp3', dino:{b4:'https://cdn.freesound.org/previews/316/316403_5123851-hq.mp3'}}) @@ -666,6 +675,8 @@ export const sampleDemo = `// "Sample demo" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +useOldRandom(true) + stack( // percussion s("[woodblock:1 woodblock:2*2] snare_rim:0,gong/8,brakedrum:1(3,8),~@3 cowbell:3") @@ -684,6 +695,8 @@ export const holyflute = `// "Holy flute" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +useOldRandom(true) + "c3 eb3(3,8) c4/2 g3*2" .superimpose( x=>x.slow(2).add(12), @@ -699,6 +712,8 @@ export const flatrave = `// "Flatrave" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +useOldRandom(true) + stack( s("bd*2,~ [cp,sd]").bank('RolandTR909'), @@ -727,6 +742,8 @@ export const amensister = `// "Amensister" samples('github:tidalcycles/dirt-samples') +useOldRandom(true) + stack( // amen n("0 1 2 3 4 5 6 7") @@ -834,6 +851,8 @@ export const arpoon = `// "Arpoon" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +useOldRandom(true) + samples('github:tidalcycles/dirt-samples') n("[0,3] 2 [1,3] 2".fast(3).lastOf(4, fast(2))).clip(2) From b8423f0ed1a96891a2380d2436f6d929ea37c504 Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 24 Aug 2025 12:01:12 -0500 Subject: [PATCH 009/194] Add docstring --- packages/core/signal.mjs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 91901f86..78755bd9 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -240,6 +240,17 @@ const __timeToRands = (t, n) => __timeToRandsPrime(__timeToIntSeed(t), n); // End old random let useOldRandomBool = false; +/** + * Whether to use the old random number generator or not. Can be used to support legacy projects + * + * @name useOldRandom + * @param {boolean} b - Whether to use old RNG + * @example + * useOldRandom(true) + * // Repeats every 300 cycles + * $: n(irand(50)).seg(16).scale("C:minor").ribbon(88, 32)._punchcard() + * $: n(irand(50)).seg(16).scale("C:minor").ribbon(388, 32)._punchcard() + */ export const useOldRandom = (b = true) => useOldRandomBool = b; // N samples at time t From 5e4e678800315da210d75645a8e37c3d62b43d3e Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 24 Aug 2025 21:14:57 -0500 Subject: [PATCH 010/194] Test bleed, some docstrings --- packages/core/bench/signal.bench.mjs | 49 ++++++++++---------- packages/core/signal.mjs | 67 +++++++++++++++------------- packages/superdough/superdough.mjs | 22 +++++++++ vitest.config.mjs | 3 +- vitest.setup.mjs | 7 +++ 5 files changed, 92 insertions(+), 56 deletions(-) create mode 100644 vitest.setup.mjs diff --git a/packages/core/bench/signal.bench.mjs b/packages/core/bench/signal.bench.mjs index c79cedc0..d92840ef 100644 --- a/packages/core/bench/signal.bench.mjs +++ b/packages/core/bench/signal.bench.mjs @@ -2,45 +2,48 @@ import { describe, bench } from 'vitest'; import { calculateSteps, rand, useOldRandom } from '../index.mjs'; -const testingResolution = 1024; +const testingResolution = 128; const _generateRandomPattern = () => rand.iter(testingResolution).fast(testingResolution).firstCycle(); -describe('random', () => { - calculateSteps(true); - bench( - '+tactus', - _generateRandomPattern, - { time: 1000 }, - ); - - calculateSteps(false); - bench( - '-tactus', - _generateRandomPattern, - { time: 1000 }, - ); -}); - describe('old random', () => { calculateSteps(true); bench( '+tactus', () => { - useOldRandom(); - _generateRandomPattern(); + useOldRandom(); + _generateRandomPattern(); + }, + { + time: 1000, + teardown() { + useOldRandom(false); + }, }, - { time: 1000 }, ); calculateSteps(false); bench( '-tactus', () => { - useOldRandom(); - _generateRandomPattern(); + useOldRandom(); + _generateRandomPattern(); + }, + { + time: 1000, + teardown() { + useOldRandom(false); + }, }, - { time: 1000 }, ); }); + +describe('random', () => { + calculateSteps(true); + bench('+tactus', _generateRandomPattern, { time: 1000 }); + + calculateSteps(false); + bench('-tactus', _generateRandomPattern, { time: 1000 }); +}); + calculateSteps(true); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 78755bd9..492f6a7f 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -201,9 +201,13 @@ function _murmurHashFinalizer(x) { return x >>> 0; // unsigned } -// Used to decorrelate nearby t and i prior to hashing -function _decorrelate(t, i = 0) { - const T = Math.floor(t * 536870912); // set 2^29 resolution +// Convert t to a 32 bit integer, preserving temporal resolution down to 1/2^29 +function _tToT(t) { + return Math.floor(t * 536870912); +} + +// Used to decorrelate nearby T and i prior to hashing +function _decorrelate(T, i = 0) { const lowBits = (T >>> 0) >>> 0; const highBits = Math.floor(T / 4294967296) >>> 0; // 2^32 let key = lowBits ^ Math.imul(highBits ^ 0x85ebca6b, 0xc2b2ae35); @@ -211,8 +215,19 @@ function _decorrelate(t, i = 0) { return key >>> 0; } -function randAt(t, i = 0) { - return _murmurHashFinalizer(_decorrelate(t, i)) / 4294967296; // 2^32 +function randAt(T, i = 0) { + return _murmurHashFinalizer(_decorrelate(T, i)) / 4294967296; // 2^32 +} + +// n samples at time t +function timeToRands(t, n) { + const T = _tToT(t); + if (n === 1) { + return randAt(T, 0); + } + const out = new Array(n); + for (let i = 0; i < n; i++) out[i] = randAt(T, i); + return out; } // Deprecated: Old random signals. Configuration `useOldRandom` may be used for legacy songs @@ -226,8 +241,10 @@ const __xorwise = (x) => { const __frac = (x) => x - Math.trunc(x); const __timeToIntSeed = (x) => __xorwise(Math.trunc(__frac(x / 300) * 536870912)); const __intSeedToRand = (x) => (x % 536870912) / 536870912; -const __timeToRand = (x) => Math.abs(__intSeedToRand(__timeToIntSeed(x))); const __timeToRandsPrime = (seed, n) => { + if (n === 1) { + return Math.abs(__intSeedToRand(seed)); + } const result = []; for (let i = 0; i < n; i++) { result.push(__intSeedToRand(seed)); @@ -240,6 +257,10 @@ const __timeToRands = (t, n) => __timeToRandsPrime(__timeToIntSeed(t), n); // End old random let useOldRandomBool = false; +const getRandsAtTime = (t, n = 1) => { + return useOldRandomBool ? __timeToRands(t, n) : timeToRands(t, n); +}; + /** * Whether to use the old random number generator or not. Can be used to support legacy projects * @@ -248,28 +269,10 @@ let useOldRandomBool = false; * @example * useOldRandom(true) * // Repeats every 300 cycles - * $: n(irand(50)).seg(16).scale("C:minor").ribbon(88, 32)._punchcard() - * $: n(irand(50)).seg(16).scale("C:minor").ribbon(388, 32)._punchcard() + * $: n(irand(50)).seg(16).scale("C:minor").ribbon(88, 32) + * $: n(irand(50)).seg(16).scale("C:minor").ribbon(388, 32) */ -export const useOldRandom = (b = true) => useOldRandomBool = b; - -// N samples at time t -function timeToRands(t, n) { - if (useOldRandomBool) { - return __timeToRands(t, n); - } - const out = new Array(n); - for (let i = 0; i < n; i++) out[i] = randAt(t, i); - return out; -} - -// Single sample at time t -function timeToRand(t) { - if (useOldRandomBool) { - return __timeToRand(t); - } - return randAt(t, 0); -} +export const useOldRandom = (b = true) => (useOldRandomBool = b); /** * A discrete pattern of numbers from 0 to n-1 @@ -313,7 +316,7 @@ export const binaryN = (n, nBits = 16) => { export const randrun = (n) => { return signal((t) => { // Without adding 0.5, the first cycle is always 0,1,2,3,... - const rands = timeToRands(t.floor().add(0.5), n); + const rands = getRandsAtTime(t.floor().add(0.5), n); const nums = rands .map((n, i) => [n, i]) .sort((a, b) => (a[0] > b[0]) - (a[0] < b[0])) @@ -363,7 +366,7 @@ export const scramble = register('scramble', (n, pat) => { * s("bd*4,hh*8").cutoff(rand.range(500,8000)) * */ -export const rand = signal(timeToRand); +export const rand = signal(getRandsAtTime); /** * A continuous pattern of random numbers, between -1 and 1 */ @@ -545,7 +548,7 @@ function _perlin(t) { let tb = ta + 1; const smootherStep = (x) => 6.0 * x ** 5 - 15.0 * x ** 4 + 10.0 * x ** 3; const interp = (x) => (a) => (b) => a + smootherStep(x) * (b - a); - const v = interp(t - ta)(timeToRand(ta))(timeToRand(tb)); + const v = interp(t - ta)(getRandsAtTime(ta))(getRandsAtTime(tb)); return v; } export const perlinWith = (tpat) => { @@ -556,8 +559,8 @@ function _berlin(t) { const prevRidgeStartIndex = Math.floor(t); const nextRidgeStartIndex = prevRidgeStartIndex + 1; - const prevRidgeBottomPoint = timeToRand(prevRidgeStartIndex); - const nextRidgeTopPoint = timeToRand(nextRidgeStartIndex) + prevRidgeBottomPoint; + const prevRidgeBottomPoint = getRandsAtTime(prevRidgeStartIndex); + const nextRidgeTopPoint = getRandsAtTime(nextRidgeStartIndex) + prevRidgeBottomPoint; const currentPercent = (t - prevRidgeStartIndex) / (nextRidgeStartIndex - prevRidgeStartIndex); const interp = (a, b, t) => { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index eb1466d9..9b6b2074 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -19,6 +19,17 @@ const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; let maxPolyphony = DEFAULT_MAX_POLYPHONY; +/** + * Set the max polyphony. If notes are ringing out via `release` then they will + * start to die out in first-in-first-out order once the max polyphony has been hit + * + * @name setMaxPolyphony + * @param {number} Max polyphony. Defaults to 128 + * @example + * setMaxPolyphony(4) + * n(irand(24).seg(8)).scale("C#3:minor").room(1).release(4).gain(0.5) + * + */ export function setMaxPolyphony(polyphony) { maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY; } @@ -48,6 +59,17 @@ export function applyGainCurve(val) { return gainCurveFunc(val); } +/** + * Apply a function to all gains provided in patterns. Can be used to rescale gain to be + * quadratic, exponential, etc. rather than linear + * + * @name setGainCurve + * @param {Function} function to apply to all gain values + * @example + * setGainCurve((x) => x * x) // quadratic gain + * s("bd*4").gain(0.5) // equivalent to 0.25 gain normally + * + */ export function setGainCurve(newGainCurveFunc) { gainCurveFunc = newGainCurveFunc; } diff --git a/vitest.config.mjs b/vitest.config.mjs index dcdde113..392b1731 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -7,7 +7,7 @@ export default defineConfig({ test: { reporters: 'verbose', isolate: false, - silent: true, + silent: false, exclude: [ '**/node_modules/**', '**/dist/**', @@ -16,5 +16,6 @@ export default defineConfig({ '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress}.config.*', '**/shared.test.mjs', ], + setupFiles: './vitest.setup.mjs', }, }); diff --git a/vitest.setup.mjs b/vitest.setup.mjs new file mode 100644 index 00000000..bb9d24b0 --- /dev/null +++ b/vitest.setup.mjs @@ -0,0 +1,7 @@ +import { afterEach } from 'vitest'; +import { useOldRandom } from './packages/core/signal.mjs'; + +afterEach(() => { + // Avoid bleed between tests + useOldRandom(false); +}); From 969c9663ef4f3e661c1df8a01541c4ad258e4756 Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 24 Aug 2025 21:15:11 -0500 Subject: [PATCH 011/194] Update tests for new randomness --- test/__snapshots__/examples.test.mjs.snap | 1421 +++++++++++---------- 1 file changed, 771 insertions(+), 650 deletions(-) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 39874935..77154de9 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -686,20 +686,20 @@ exports[`runs examples > example "almostAlways" example index 0 1`] = ` "[ 13/8 → 7/4 | s:hh speed:0.5 ]", "[ 7/4 → 15/8 | s:hh speed:0.5 ]", "[ 15/8 → 2/1 | s:hh speed:0.5 ]", - "[ 2/1 → 17/8 | s:hh ]", - "[ 17/8 → 9/4 | s:hh ]", + "[ 2/1 → 17/8 | s:hh speed:0.5 ]", + "[ 17/8 → 9/4 | s:hh speed:0.5 ]", "[ 9/4 → 19/8 | s:hh speed:0.5 ]", "[ 19/8 → 5/2 | s:hh speed:0.5 ]", "[ 5/2 → 21/8 | s:hh speed:0.5 ]", "[ 21/8 → 11/4 | s:hh speed:0.5 ]", "[ 11/4 → 23/8 | s:hh speed:0.5 ]", - "[ 23/8 → 3/1 | s:hh speed:0.5 ]", + "[ 23/8 → 3/1 | s:hh ]", "[ 3/1 → 25/8 | s:hh speed:0.5 ]", "[ 25/8 → 13/4 | s:hh speed:0.5 ]", - "[ 13/4 → 27/8 | s:hh ]", + "[ 13/4 → 27/8 | s:hh speed:0.5 ]", "[ 27/8 → 7/2 | s:hh speed:0.5 ]", "[ 7/2 → 29/8 | s:hh speed:0.5 ]", - "[ 29/8 → 15/4 | s:hh ]", + "[ 29/8 → 15/4 | s:hh speed:0.5 ]", "[ 15/4 → 31/8 | s:hh speed:0.5 ]", "[ 31/8 → 4/1 | s:hh speed:0.5 ]", ] @@ -707,7 +707,7 @@ exports[`runs examples > example "almostAlways" example index 0 1`] = ` exports[`runs examples > example "almostNever" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:hh speed:0.5 ]", + "[ 0/1 → 1/8 | s:hh ]", "[ 1/8 → 1/4 | s:hh ]", "[ 1/4 → 3/8 | s:hh ]", "[ 3/8 → 1/2 | s:hh ]", @@ -715,7 +715,7 @@ exports[`runs examples > example "almostNever" example index 0 1`] = ` "[ 5/8 → 3/4 | s:hh ]", "[ 3/4 → 7/8 | s:hh ]", "[ 7/8 → 1/1 | s:hh ]", - "[ 1/1 → 9/8 | s:hh ]", + "[ 1/1 → 9/8 | s:hh speed:0.5 ]", "[ 9/8 → 5/4 | s:hh ]", "[ 5/4 → 11/8 | s:hh ]", "[ 11/8 → 3/2 | s:hh ]", @@ -728,17 +728,17 @@ exports[`runs examples > example "almostNever" example index 0 1`] = ` "[ 9/4 → 19/8 | s:hh ]", "[ 19/8 → 5/2 | s:hh ]", "[ 5/2 → 21/8 | s:hh ]", - "[ 21/8 → 11/4 | s:hh ]", + "[ 21/8 → 11/4 | s:hh speed:0.5 ]", "[ 11/4 → 23/8 | s:hh ]", - "[ 23/8 → 3/1 | s:hh speed:0.5 ]", + "[ 23/8 → 3/1 | s:hh ]", "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", "[ 13/4 → 27/8 | s:hh ]", "[ 27/8 → 7/2 | s:hh ]", "[ 7/2 → 29/8 | s:hh ]", "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:hh ]", - "[ 31/8 → 4/1 | s:hh ]", + "[ 15/4 → 31/8 | s:hh speed:0.5 ]", + "[ 31/8 → 4/1 | s:hh speed:0.5 ]", ] `; @@ -1053,70 +1053,70 @@ exports[`runs examples > example "begin" example index 0 1`] = ` exports[`runs examples > example "berlin" example index 0 1`] = ` [ - "[ 0/1 → 1/16 | note:D3 ]", - "[ 1/16 → 1/8 | note:E3 ]", - "[ 1/8 → 3/16 | note:F3 ]", - "[ 3/16 → 1/4 | note:G3 ]", - "[ 1/4 → 5/16 | note:A3 ]", - "[ 5/16 → 3/8 | note:C4 ]", - "[ 3/8 → 7/16 | note:D4 ]", - "[ 7/16 → 1/2 | note:F4 ]", - "[ 1/2 → 9/16 | note:D4 ]", - "[ 9/16 → 5/8 | note:E4 ]", - "[ 5/8 → 11/16 | note:E4 ]", - "[ 11/16 → 3/4 | note:E4 ]", - "[ 3/4 → 13/16 | note:F3 ]", - "[ 13/16 → 7/8 | note:F3 ]", - "[ 7/8 → 15/16 | note:F3 ]", - "[ 15/16 → 1/1 | note:F3 ]", - "[ 1/1 → 17/16 | note:E3 ]", - "[ 17/16 → 9/8 | note:E3 ]", - "[ 9/8 → 19/16 | note:E3 ]", - "[ 19/16 → 5/4 | note:F3 ]", - "[ 5/4 → 21/16 | note:E3 ]", - "[ 21/16 → 11/8 | note:F3 ]", - "[ 11/8 → 23/16 | note:G3 ]", - "[ 23/16 → 3/2 | note:A3 ]", - "[ 3/2 → 25/16 | note:A3 ]", - "[ 25/16 → 13/8 | note:Bb3 ]", - "[ 13/8 → 27/16 | note:Bb3 ]", - "[ 27/16 → 7/4 | note:Bb3 ]", + "[ 0/1 → 1/16 | note:A3 ]", + "[ 1/16 → 1/8 | note:A3 ]", + "[ 1/8 → 3/16 | note:A3 ]", + "[ 3/16 → 1/4 | note:A3 ]", + "[ 1/4 → 5/16 | note:E3 ]", + "[ 5/16 → 3/8 | note:F3 ]", + "[ 3/8 → 7/16 | note:F3 ]", + "[ 7/16 → 1/2 | note:G3 ]", + "[ 1/2 → 9/16 | note:A3 ]", + "[ 9/16 → 5/8 | note:Bb3 ]", + "[ 5/8 → 11/16 | note:Bb3 ]", + "[ 11/16 → 3/4 | note:C4 ]", + "[ 3/4 → 13/16 | note:G3 ]", + "[ 13/16 → 7/8 | note:A3 ]", + "[ 7/8 → 15/16 | note:A3 ]", + "[ 15/16 → 1/1 | note:Bb3 ]", + "[ 1/1 → 17/16 | note:F3 ]", + "[ 17/16 → 9/8 | note:G3 ]", + "[ 9/8 → 19/16 | note:G3 ]", + "[ 19/16 → 5/4 | note:A3 ]", + "[ 5/4 → 21/16 | note:G3 ]", + "[ 21/16 → 11/8 | note:Bb3 ]", + "[ 11/8 → 23/16 | note:D4 ]", + "[ 23/16 → 3/2 | note:E4 ]", + "[ 3/2 → 25/16 | note:D4 ]", + "[ 25/16 → 13/8 | note:E4 ]", + "[ 13/8 → 27/16 | note:E4 ]", + "[ 27/16 → 7/4 | note:E4 ]", "[ 7/4 → 29/16 | note:F3 ]", - "[ 29/16 → 15/8 | note:G3 ]", + "[ 29/16 → 15/8 | note:A3 ]", "[ 15/8 → 31/16 | note:Bb3 ]", "[ 31/16 → 2/1 | note:C4 ]", "[ 2/1 → 33/16 | note:C4 ]", - "[ 33/16 → 17/8 | note:D4 ]", - "[ 17/8 → 35/16 | note:F4 ]", - "[ 35/16 → 9/4 | note:G4 ]", - "[ 9/4 → 37/16 | note:Bb3 ]", - "[ 37/16 → 19/8 | note:Bb3 ]", - "[ 19/8 → 39/16 | note:Bb3 ]", - "[ 39/16 → 5/2 | note:C4 ]", - "[ 5/2 → 41/16 | note:F3 ]", - "[ 41/16 → 21/8 | note:F3 ]", - "[ 21/8 → 43/16 | note:G3 ]", - "[ 43/16 → 11/4 | note:A3 ]", - "[ 11/4 → 45/16 | note:A3 ]", - "[ 45/16 → 23/8 | note:A3 ]", - "[ 23/8 → 47/16 | note:A3 ]", + "[ 33/16 → 17/8 | note:E4 ]", + "[ 17/8 → 35/16 | note:G4 ]", + "[ 35/16 → 9/4 | note:A4 ]", + "[ 9/4 → 37/16 | note:D4 ]", + "[ 37/16 → 19/8 | note:F4 ]", + "[ 19/8 → 39/16 | note:G4 ]", + "[ 39/16 → 5/2 | note:A4 ]", + "[ 5/2 → 41/16 | note:Bb3 ]", + "[ 41/16 → 21/8 | note:C4 ]", + "[ 21/8 → 43/16 | note:C4 ]", + "[ 43/16 → 11/4 | note:C4 ]", + "[ 11/4 → 45/16 | note:F3 ]", + "[ 45/16 → 23/8 | note:G3 ]", + "[ 23/8 → 47/16 | note:G3 ]", "[ 47/16 → 3/1 | note:A3 ]", - "[ 3/1 → 49/16 | note:E3 ]", + "[ 3/1 → 49/16 | note:F3 ]", "[ 49/16 → 25/8 | note:G3 ]", - "[ 25/8 → 51/16 | note:Bb3 ]", - "[ 51/16 → 13/4 | note:C4 ]", - "[ 13/4 → 53/16 | note:D4 ]", - "[ 53/16 → 27/8 | note:E4 ]", - "[ 27/8 → 55/16 | note:G4 ]", - "[ 55/16 → 7/2 | note:A4 ]", - "[ 7/2 → 57/16 | note:Bb3 ]", - "[ 57/16 → 29/8 | note:Bb3 ]", - "[ 29/8 → 59/16 | note:C4 ]", - "[ 59/16 → 15/4 | note:C4 ]", - "[ 15/4 → 61/16 | note:F3 ]", - "[ 61/16 → 31/8 | note:G3 ]", - "[ 31/8 → 63/16 | note:G3 ]", - "[ 63/16 → 4/1 | note:A3 ]", + "[ 25/8 → 51/16 | note:G3 ]", + "[ 51/16 → 13/4 | note:A3 ]", + "[ 13/4 → 53/16 | note:G3 ]", + "[ 53/16 → 27/8 | note:A3 ]", + "[ 27/8 → 55/16 | note:C4 ]", + "[ 55/16 → 7/2 | note:E4 ]", + "[ 7/2 → 57/16 | note:D4 ]", + "[ 57/16 → 29/8 | note:E4 ]", + "[ 29/8 → 59/16 | note:F4 ]", + "[ 59/16 → 15/4 | note:G4 ]", + "[ 15/4 → 61/16 | note:Bb3 ]", + "[ 61/16 → 31/8 | note:C4 ]", + "[ 31/8 → 63/16 | note:D4 ]", + "[ 63/16 → 4/1 | note:E4 ]", ] `; @@ -1499,51 +1499,51 @@ exports[`runs examples > example "brand" example index 0 1`] = ` [ "[ 0/1 → 1/10 | s:hh pan:true ]", "[ 1/10 → 1/5 | s:hh pan:true ]", - "[ 1/5 → 3/10 | s:hh pan:false ]", + "[ 1/5 → 3/10 | s:hh pan:true ]", "[ 3/10 → 2/5 | s:hh pan:false ]", - "[ 2/5 → 1/2 | s:hh pan:false ]", + "[ 2/5 → 1/2 | s:hh pan:true ]", "[ 1/2 → 3/5 | s:hh pan:true ]", "[ 3/5 → 7/10 | s:hh pan:false ]", "[ 7/10 → 4/5 | s:hh pan:false ]", - "[ 4/5 → 9/10 | s:hh pan:true ]", - "[ 9/10 → 1/1 | s:hh pan:false ]", - "[ 1/1 → 11/10 | s:hh pan:false ]", + "[ 4/5 → 9/10 | s:hh pan:false ]", + "[ 9/10 → 1/1 | s:hh pan:true ]", + "[ 1/1 → 11/10 | s:hh pan:true ]", "[ 11/10 → 6/5 | s:hh pan:false ]", - "[ 6/5 → 13/10 | s:hh pan:true ]", + "[ 6/5 → 13/10 | s:hh pan:false ]", "[ 13/10 → 7/5 | s:hh pan:true ]", "[ 7/5 → 3/2 | s:hh pan:true ]", - "[ 3/2 → 8/5 | s:hh pan:false ]", - "[ 8/5 → 17/10 | s:hh pan:true ]", - "[ 17/10 → 9/5 | s:hh pan:true ]", + "[ 3/2 → 8/5 | s:hh pan:true ]", + "[ 8/5 → 17/10 | s:hh pan:false ]", + "[ 17/10 → 9/5 | s:hh pan:false ]", "[ 9/5 → 19/10 | s:hh pan:true ]", "[ 19/10 → 2/1 | s:hh pan:true ]", "[ 2/1 → 21/10 | s:hh pan:false ]", "[ 21/10 → 11/5 | s:hh pan:true ]", "[ 11/5 → 23/10 | s:hh pan:false ]", - "[ 23/10 → 12/5 | s:hh pan:false ]", - "[ 12/5 → 5/2 | s:hh pan:false ]", + "[ 23/10 → 12/5 | s:hh pan:true ]", + "[ 12/5 → 5/2 | s:hh pan:true ]", "[ 5/2 → 13/5 | s:hh pan:true ]", - "[ 13/5 → 27/10 | s:hh pan:true ]", + "[ 13/5 → 27/10 | s:hh pan:false ]", "[ 27/10 → 14/5 | s:hh pan:true ]", - "[ 14/5 → 29/10 | s:hh pan:false ]", + "[ 14/5 → 29/10 | s:hh pan:true ]", "[ 29/10 → 3/1 | s:hh pan:true ]", "[ 3/1 → 31/10 | s:hh pan:true ]", - "[ 31/10 → 16/5 | s:hh pan:true ]", + "[ 31/10 → 16/5 | s:hh pan:false ]", "[ 16/5 → 33/10 | s:hh pan:true ]", "[ 33/10 → 17/5 | s:hh pan:false ]", "[ 17/5 → 7/2 | s:hh pan:false ]", "[ 7/2 → 18/5 | s:hh pan:true ]", "[ 18/5 → 37/10 | s:hh pan:false ]", - "[ 37/10 → 19/5 | s:hh pan:false ]", - "[ 19/5 → 39/10 | s:hh pan:true ]", - "[ 39/10 → 4/1 | s:hh pan:true ]", + "[ 37/10 → 19/5 | s:hh pan:true ]", + "[ 19/5 → 39/10 | s:hh pan:false ]", + "[ 39/10 → 4/1 | s:hh pan:false ]", ] `; exports[`runs examples > example "brandBy" example index 0 1`] = ` [ - "[ 0/1 → 1/10 | s:hh pan:true ]", - "[ 1/10 → 1/5 | s:hh pan:true ]", + "[ 0/1 → 1/10 | s:hh pan:false ]", + "[ 1/10 → 1/5 | s:hh pan:false ]", "[ 1/5 → 3/10 | s:hh pan:false ]", "[ 3/10 → 2/5 | s:hh pan:false ]", "[ 2/5 → 1/2 | s:hh pan:false ]", @@ -1552,35 +1552,35 @@ exports[`runs examples > example "brandBy" example index 0 1`] = ` "[ 7/10 → 4/5 | s:hh pan:false ]", "[ 4/5 → 9/10 | s:hh pan:false ]", "[ 9/10 → 1/1 | s:hh pan:false ]", - "[ 1/1 → 11/10 | s:hh pan:false ]", + "[ 1/1 → 11/10 | s:hh pan:true ]", "[ 11/10 → 6/5 | s:hh pan:false ]", "[ 6/5 → 13/10 | s:hh pan:false ]", - "[ 13/10 → 7/5 | s:hh pan:false ]", - "[ 7/5 → 3/2 | s:hh pan:false ]", + "[ 13/10 → 7/5 | s:hh pan:true ]", + "[ 7/5 → 3/2 | s:hh pan:true ]", "[ 3/2 → 8/5 | s:hh pan:false ]", "[ 8/5 → 17/10 | s:hh pan:false ]", "[ 17/10 → 9/5 | s:hh pan:false ]", "[ 9/5 → 19/10 | s:hh pan:false ]", "[ 19/10 → 2/1 | s:hh pan:true ]", "[ 2/1 → 21/10 | s:hh pan:false ]", - "[ 21/10 → 11/5 | s:hh pan:false ]", + "[ 21/10 → 11/5 | s:hh pan:true ]", "[ 11/5 → 23/10 | s:hh pan:false ]", - "[ 23/10 → 12/5 | s:hh pan:false ]", + "[ 23/10 → 12/5 | s:hh pan:true ]", "[ 12/5 → 5/2 | s:hh pan:false ]", "[ 5/2 → 13/5 | s:hh pan:false ]", "[ 13/5 → 27/10 | s:hh pan:false ]", - "[ 27/10 → 14/5 | s:hh pan:false ]", - "[ 14/5 → 29/10 | s:hh pan:false ]", + "[ 27/10 → 14/5 | s:hh pan:true ]", + "[ 14/5 → 29/10 | s:hh pan:true ]", "[ 29/10 → 3/1 | s:hh pan:false ]", "[ 3/1 → 31/10 | s:hh pan:false ]", - "[ 31/10 → 16/5 | s:hh pan:true ]", + "[ 31/10 → 16/5 | s:hh pan:false ]", "[ 16/5 → 33/10 | s:hh pan:true ]", "[ 33/10 → 17/5 | s:hh pan:false ]", "[ 17/5 → 7/2 | s:hh pan:false ]", "[ 7/2 → 18/5 | s:hh pan:false ]", "[ 18/5 → 37/10 | s:hh pan:false ]", - "[ 37/10 → 19/5 | s:hh pan:false ]", - "[ 19/5 → 39/10 | s:hh pan:true ]", + "[ 37/10 → 19/5 | s:hh pan:true ]", + "[ 19/5 → 39/10 | s:hh pan:false ]", "[ 39/10 → 4/1 | s:hh pan:false ]", ] `; @@ -1712,99 +1712,99 @@ exports[`runs examples > example "channels" example index 0 1`] = ` exports[`runs examples > example "choose" example index 0 1`] = ` [ - "[ 0/1 → 1/5 | note:c2 s:sine ]", - "[ 1/5 → 2/5 | note:g2 s:bd n:6 ]", + "[ 0/1 → 1/5 | note:c2 s:triangle ]", + "[ 1/5 → 2/5 | note:g2 s:sine ]", "[ 2/5 → 3/5 | note:g2 s:triangle ]", "[ 3/5 → 4/5 | note:d2 s:bd n:6 ]", - "[ 4/5 → 1/1 | note:f1 s:sine ]", - "[ 1/1 → 6/5 | note:c2 s:triangle ]", - "[ 6/5 → 7/5 | note:g2 s:triangle ]", + "[ 4/5 → 1/1 | note:f1 s:bd n:6 ]", + "[ 1/1 → 6/5 | note:c2 s:sine ]", + "[ 6/5 → 7/5 | note:g2 s:bd n:6 ]", "[ 7/5 → 8/5 | note:g2 s:sine ]", - "[ 8/5 → 9/5 | note:d2 s:triangle ]", + "[ 8/5 → 9/5 | note:d2 s:bd n:6 ]", "[ 9/5 → 2/1 | note:f1 s:sine ]", - "[ 2/1 → 11/5 | note:c2 s:bd n:6 ]", + "[ 2/1 → 11/5 | note:c2 s:triangle ]", "[ 11/5 → 12/5 | note:g2 s:bd n:6 ]", - "[ 12/5 → 13/5 | note:g2 s:bd n:6 ]", - "[ 13/5 → 14/5 | note:d2 s:sine ]", - "[ 14/5 → 3/1 | note:f1 s:triangle ]", - "[ 3/1 → 16/5 | note:c2 s:sine ]", + "[ 12/5 → 13/5 | note:g2 s:sine ]", + "[ 13/5 → 14/5 | note:d2 s:bd n:6 ]", + "[ 14/5 → 3/1 | note:f1 s:sine ]", + "[ 3/1 → 16/5 | note:c2 s:triangle ]", "[ 16/5 → 17/5 | note:g2 s:sine ]", - "[ 17/5 → 18/5 | note:g2 s:triangle ]", + "[ 17/5 → 18/5 | note:g2 s:bd n:6 ]", "[ 18/5 → 19/5 | note:d2 s:triangle ]", - "[ 19/5 → 4/1 | note:f1 s:sine ]", + "[ 19/5 → 4/1 | note:f1 s:bd n:6 ]", ] `; exports[`runs examples > example "chooseCycles" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:bd ]", - "[ 1/8 → 1/4 | s:hh ]", - "[ 1/4 → 3/8 | s:sd ]", - "[ 3/8 → 1/2 | s:bd ]", + "[ 0/1 → 1/8 | s:hh ]", + "[ 1/8 → 1/4 | s:bd ]", + "[ 1/4 → 3/8 | s:hh ]", + "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:bd ]", - "[ 5/8 → 3/4 | s:bd ]", - "[ 3/4 → 7/8 | s:hh ]", + "[ 5/8 → 3/4 | s:hh ]", + "[ 3/4 → 7/8 | s:sd ]", "[ 7/8 → 1/1 | s:bd ]", "[ 1/1 → 9/8 | s:sd ]", - "[ 9/8 → 5/4 | s:hh ]", - "[ 5/4 → 11/8 | s:bd ]", - "[ 11/8 → 3/2 | s:hh ]", + "[ 9/8 → 5/4 | s:sd ]", + "[ 5/4 → 11/8 | s:hh ]", + "[ 11/8 → 3/2 | s:bd ]", "[ 3/2 → 13/8 | s:bd ]", - "[ 13/8 → 7/4 | s:sd ]", - "[ 7/4 → 15/8 | s:hh ]", - "[ 15/8 → 2/1 | s:bd ]", - "[ 2/1 → 17/8 | s:bd ]", + "[ 13/8 → 7/4 | s:bd ]", + "[ 7/4 → 15/8 | s:sd ]", + "[ 15/8 → 2/1 | s:hh ]", + "[ 2/1 → 17/8 | s:sd ]", "[ 17/8 → 9/4 | s:sd ]", - "[ 9/4 → 19/8 | s:sd ]", - "[ 19/8 → 5/2 | s:bd ]", - "[ 5/2 → 21/8 | s:hh ]", - "[ 21/8 → 11/4 | s:bd ]", - "[ 11/4 → 23/8 | s:sd ]", + "[ 9/4 → 19/8 | s:bd ]", + "[ 19/8 → 5/2 | s:hh ]", + "[ 5/2 → 21/8 | s:bd ]", + "[ 21/8 → 11/4 | s:sd ]", + "[ 11/4 → 23/8 | s:hh ]", "[ 23/8 → 3/1 | s:bd ]", "[ 3/1 → 25/8 | s:sd ]", "[ 25/8 → 13/4 | s:sd ]", - "[ 13/4 → 27/8 | s:bd ]", + "[ 13/4 → 27/8 | s:sd ]", "[ 27/8 → 7/2 | s:bd ]", - "[ 7/2 → 29/8 | s:bd ]", - "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:hh ]", + "[ 7/2 → 29/8 | s:sd ]", + "[ 29/8 → 15/4 | s:bd ]", + "[ 15/4 → 31/8 | s:bd ]", "[ 31/8 → 4/1 | s:bd ]", ] `; exports[`runs examples > example "chooseCycles" example index 1 1`] = ` [ - "[ 0/1 → 1/8 | s:bd ]", - "[ 1/8 → 1/4 | s:hh ]", - "[ 1/4 → 3/8 | s:sd ]", - "[ 3/8 → 1/2 | s:bd ]", + "[ 0/1 → 1/8 | s:hh ]", + "[ 1/8 → 1/4 | s:bd ]", + "[ 1/4 → 3/8 | s:hh ]", + "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:bd ]", - "[ 5/8 → 3/4 | s:bd ]", - "[ 3/4 → 7/8 | s:hh ]", + "[ 5/8 → 3/4 | s:hh ]", + "[ 3/4 → 7/8 | s:sd ]", "[ 7/8 → 1/1 | s:bd ]", "[ 1/1 → 9/8 | s:sd ]", - "[ 9/8 → 5/4 | s:hh ]", - "[ 5/4 → 11/8 | s:bd ]", - "[ 11/8 → 3/2 | s:hh ]", + "[ 9/8 → 5/4 | s:sd ]", + "[ 5/4 → 11/8 | s:hh ]", + "[ 11/8 → 3/2 | s:bd ]", "[ 3/2 → 13/8 | s:bd ]", - "[ 13/8 → 7/4 | s:sd ]", - "[ 7/4 → 15/8 | s:hh ]", - "[ 15/8 → 2/1 | s:bd ]", - "[ 2/1 → 17/8 | s:bd ]", + "[ 13/8 → 7/4 | s:bd ]", + "[ 7/4 → 15/8 | s:sd ]", + "[ 15/8 → 2/1 | s:hh ]", + "[ 2/1 → 17/8 | s:sd ]", "[ 17/8 → 9/4 | s:sd ]", - "[ 9/4 → 19/8 | s:sd ]", - "[ 19/8 → 5/2 | s:bd ]", - "[ 5/2 → 21/8 | s:hh ]", - "[ 21/8 → 11/4 | s:bd ]", - "[ 11/4 → 23/8 | s:sd ]", + "[ 9/4 → 19/8 | s:bd ]", + "[ 19/8 → 5/2 | s:hh ]", + "[ 5/2 → 21/8 | s:bd ]", + "[ 21/8 → 11/4 | s:sd ]", + "[ 11/4 → 23/8 | s:hh ]", "[ 23/8 → 3/1 | s:bd ]", "[ 3/1 → 25/8 | s:sd ]", "[ 25/8 → 13/4 | s:sd ]", - "[ 13/4 → 27/8 | s:bd ]", + "[ 13/4 → 27/8 | s:sd ]", "[ 27/8 → 7/2 | s:bd ]", - "[ 7/2 → 29/8 | s:bd ]", - "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:hh ]", + "[ 7/2 → 29/8 | s:sd ]", + "[ 29/8 → 15/4 | s:bd ]", + "[ 15/4 → 31/8 | s:bd ]", "[ 31/8 → 4/1 | s:bd ]", ] `; @@ -2423,51 +2423,61 @@ exports[`runs examples > example "decay" example index 0 1`] = ` exports[`runs examples > example "degrade" example index 0 1`] = ` [ "[ 1/8 → 1/4 | s:hh ]", - "[ 1/1 → 9/8 | s:hh ]", + "[ 1/4 → 3/8 | s:hh ]", + "[ 3/8 → 1/2 | s:hh ]", + "[ 5/8 → 3/4 | s:hh ]", + "[ 3/4 → 7/8 | s:hh ]", + "[ 7/8 → 1/1 | s:hh ]", "[ 9/8 → 5/4 | s:hh ]", "[ 5/4 → 11/8 | s:hh ]", - "[ 3/2 → 13/8 | s:hh ]", + "[ 11/8 → 3/2 | s:hh ]", + "[ 7/4 → 15/8 | s:hh ]", "[ 15/8 → 2/1 | s:hh ]", "[ 2/1 → 17/8 | s:hh ]", "[ 17/8 → 9/4 | s:hh ]", + "[ 9/4 → 19/8 | s:hh ]", "[ 19/8 → 5/2 | s:hh ]", "[ 11/4 → 23/8 | s:hh ]", - "[ 13/4 → 27/8 | s:hh ]", + "[ 23/8 → 3/1 | s:hh ]", + "[ 25/8 → 13/4 | s:hh ]", + "[ 27/8 → 7/2 | s:hh ]", "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:hh ]", - "[ 31/8 → 4/1 | s:hh ]", ] `; exports[`runs examples > example "degrade" example index 1 1`] = ` [ - "[ 1/8 → 1/4 | s:hh ]", "[ 1/4 → 3/8 | s:hh ]", "[ 3/4 → 7/8 | s:hh ]", "[ 1/1 → 9/8 | s:hh ]", "[ 9/8 → 5/4 | s:hh ]", - "[ 13/8 → 7/4 | s:hh ]", + "[ 5/4 → 11/8 | s:hh ]", "[ 7/4 → 15/8 | s:hh ]", + "[ 15/8 → 2/1 | s:hh ]", + "[ 2/1 → 17/8 | s:hh ]", "[ 17/8 → 9/4 | s:hh ]", - "[ 9/4 → 19/8 | s:hh ]", - "[ 5/2 → 21/8 | s:hh ]", - "[ 11/4 → 23/8 | s:hh ]", + "[ 19/8 → 5/2 | s:hh ]", + "[ 21/8 → 11/4 | s:hh ]", "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", - "[ 15/4 → 31/8 | s:hh ]", + "[ 13/4 → 27/8 | s:hh ]", + "[ 7/2 → 29/8 | s:hh ]", ] `; exports[`runs examples > example "degradeBy" example index 0 1`] = ` [ + "[ 0/1 → 1/8 | s:hh ]", "[ 1/8 → 1/4 | s:hh ]", "[ 1/4 → 3/8 | s:hh ]", "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:hh ]", + "[ 5/8 → 3/4 | s:hh ]", + "[ 3/4 → 7/8 | s:hh ]", "[ 7/8 → 1/1 | s:hh ]", - "[ 1/1 → 9/8 | s:hh ]", "[ 9/8 → 5/4 | s:hh ]", "[ 5/4 → 11/8 | s:hh ]", + "[ 11/8 → 3/2 | s:hh ]", "[ 3/2 → 13/8 | s:hh ]", "[ 13/8 → 7/4 | s:hh ]", "[ 7/4 → 15/8 | s:hh ]", @@ -2478,74 +2488,89 @@ exports[`runs examples > example "degradeBy" example index 0 1`] = ` "[ 19/8 → 5/2 | s:hh ]", "[ 5/2 → 21/8 | s:hh ]", "[ 11/4 → 23/8 | s:hh ]", + "[ 23/8 → 3/1 | s:hh ]", "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", "[ 13/4 → 27/8 | s:hh ]", "[ 27/8 → 7/2 | s:hh ]", "[ 7/2 → 29/8 | s:hh ]", "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:hh ]", - "[ 31/8 → 4/1 | s:hh ]", ] `; exports[`runs examples > example "degradeBy" example index 1 1`] = ` [ - "[ 1/8 → 1/4 | s:hh ]", + "[ 0/1 → 1/8 | s:hh ]", "[ 1/4 → 3/8 | s:hh ]", "[ 3/8 → 1/2 | s:hh ]", + "[ 1/2 → 5/8 | s:hh ]", + "[ 5/8 → 3/4 | s:hh ]", "[ 3/4 → 7/8 | s:hh ]", + "[ 7/8 → 1/1 | s:hh ]", "[ 1/1 → 9/8 | s:hh ]", "[ 9/8 → 5/4 | s:hh ]", + "[ 5/4 → 11/8 | s:hh ]", "[ 11/8 → 3/2 | s:hh ]", + "[ 3/2 → 13/8 | s:hh ]", "[ 13/8 → 7/4 | s:hh ]", "[ 7/4 → 15/8 | s:hh ]", "[ 15/8 → 2/1 | s:hh ]", "[ 2/1 → 17/8 | s:hh ]", "[ 17/8 → 9/4 | s:hh ]", - "[ 9/4 → 19/8 | s:hh ]", "[ 19/8 → 5/2 | s:hh ]", "[ 5/2 → 21/8 | s:hh ]", "[ 21/8 → 11/4 | s:hh ]", "[ 11/4 → 23/8 | s:hh ]", + "[ 23/8 → 3/1 | s:hh ]", "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", + "[ 13/4 → 27/8 | s:hh ]", "[ 27/8 → 7/2 | s:hh ]", "[ 7/2 → 29/8 | s:hh ]", - "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:hh ]", ] `; exports[`runs examples > example "degradeBy" example index 2 1`] = ` [ + "[ 0/1 → 1/16 | s:bd ]", + "[ 1/16 → 1/8 | s:bd ]", "[ 1/8 → 3/16 | s:bd ]", - "[ 1/4 → 5/16 | s:bd ]", - "[ 5/16 → 3/8 | s:bd ]", + "[ 3/8 → 7/16 | s:bd ]", "[ 1/2 → 9/16 | s:bd ]", "[ 9/16 → 5/8 | s:bd ]", + "[ 5/8 → 11/16 | s:bd ]", "[ 11/16 → 3/4 | s:bd ]", + "[ 3/4 → 13/16 | s:bd ]", "[ 15/16 → 1/1 | s:bd ]", + "[ 1/1 → 17/16 | s:bd ]", + "[ 17/16 → 9/8 | s:bd ]", "[ 9/8 → 19/16 | s:bd ]", - "[ 5/4 → 21/16 | s:bd ]", - "[ 21/16 → 11/8 | s:bd ]", + "[ 11/8 → 23/16 | s:bd ]", "[ 3/2 → 25/16 | s:bd ]", "[ 25/16 → 13/8 | s:bd ]", + "[ 13/8 → 27/16 | s:bd ]", "[ 27/16 → 7/4 | s:bd ]", + "[ 7/4 → 29/16 | s:bd ]", "[ 31/16 → 2/1 | s:bd ]", + "[ 2/1 → 33/16 | s:bd ]", + "[ 33/16 → 17/8 | s:bd ]", "[ 17/8 → 35/16 | s:bd ]", - "[ 9/4 → 37/16 | s:bd ]", - "[ 37/16 → 19/8 | s:bd ]", + "[ 19/8 → 39/16 | s:bd ]", "[ 5/2 → 41/16 | s:bd ]", "[ 41/16 → 21/8 | s:bd ]", + "[ 21/8 → 43/16 | s:bd ]", "[ 43/16 → 11/4 | s:bd ]", + "[ 11/4 → 45/16 | s:bd ]", "[ 47/16 → 3/1 | s:bd ]", + "[ 3/1 → 49/16 | s:bd ]", + "[ 49/16 → 25/8 | s:bd ]", "[ 25/8 → 51/16 | s:bd ]", - "[ 13/4 → 53/16 | s:bd ]", - "[ 53/16 → 27/8 | s:bd ]", + "[ 27/8 → 55/16 | s:bd ]", "[ 7/2 → 57/16 | s:bd ]", "[ 57/16 → 29/8 | s:bd ]", + "[ 29/8 → 59/16 | s:bd ]", "[ 59/16 → 15/4 | s:bd ]", + "[ 15/4 → 61/16 | s:bd ]", "[ 63/16 → 4/1 | s:bd ]", ] `; @@ -4844,34 +4869,34 @@ exports[`runs examples > example "invert" example index 0 1`] = ` exports[`runs examples > example "irand" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:C3 ]", - "[ 1/4 → 3/8 | note:Eb3 ]", - "[ 3/8 → 1/2 | note:F3 ]", - "[ 1/2 → 3/4 | note:Eb3 ]", - "[ 3/4 → 5/6 | note:D3 ]", - "[ 5/6 → 11/12 | note:C3 ]", + "[ 0/1 → 1/4 | note:F3 ]", + "[ 1/4 → 3/8 | note:G3 ]", + "[ 3/8 → 1/2 | note:G3 ]", + "[ 1/2 → 3/4 | note:F3 ]", + "[ 3/4 → 5/6 | note:Ab3 ]", + "[ 5/6 → 11/12 | note:Bb3 ]", "[ 11/12 → 1/1 | note:C4 ]", - "[ 1/1 → 5/4 | note:G3 ]", + "[ 1/1 → 5/4 | note:C3 ]", "[ 5/4 → 11/8 | note:Ab3 ]", - "[ 11/8 → 3/2 | note:D3 ]", - "[ 3/2 → 7/4 | note:G3 ]", - "[ 7/4 → 11/6 | note:D3 ]", + "[ 11/8 → 3/2 | note:Bb3 ]", + "[ 3/2 → 7/4 | note:D3 ]", + "[ 7/4 → 11/6 | note:Ab3 ]", "[ 11/6 → 23/12 | note:Bb3 ]", - "[ 23/12 → 2/1 | note:Eb3 ]", - "[ 2/1 → 9/4 | note:C4 ]", - "[ 9/4 → 19/8 | note:Eb3 ]", - "[ 19/8 → 5/2 | note:Bb3 ]", - "[ 5/2 → 11/4 | note:F3 ]", + "[ 23/12 → 2/1 | note:C3 ]", + "[ 2/1 → 9/4 | note:G3 ]", + "[ 9/4 → 19/8 | note:Ab3 ]", + "[ 19/8 → 5/2 | note:Ab3 ]", + "[ 5/2 → 11/4 | note:Eb3 ]", "[ 11/4 → 17/6 | note:Ab3 ]", - "[ 17/6 → 35/12 | note:D3 ]", - "[ 35/12 → 3/1 | note:F3 ]", - "[ 3/1 → 13/4 | note:D3 ]", - "[ 13/4 → 27/8 | note:C4 ]", - "[ 27/8 → 7/2 | note:F3 ]", - "[ 7/2 → 15/4 | note:F3 ]", - "[ 15/4 → 23/6 | note:Bb3 ]", + "[ 17/6 → 35/12 | note:Bb3 ]", + "[ 35/12 → 3/1 | note:Ab3 ]", + "[ 3/1 → 13/4 | note:F3 ]", + "[ 13/4 → 27/8 | note:Eb3 ]", + "[ 27/8 → 7/2 | note:Ab3 ]", + "[ 7/2 → 15/4 | note:Eb3 ]", + "[ 15/4 → 23/6 | note:C3 ]", "[ 23/6 → 47/12 | note:Ab3 ]", - "[ 47/12 → 4/1 | note:C3 ]", + "[ 47/12 → 4/1 | note:Ab3 ]", ] `; @@ -6503,36 +6528,36 @@ exports[`runs examples > example "off" example index 0 1`] = ` exports[`runs examples > example "often" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh speed:0.5 ]", - "[ 1/8 → 1/4 | s:hh speed:0.5 ]", + "[ 1/8 → 1/4 | s:hh ]", "[ 1/4 → 3/8 | s:hh speed:0.5 ]", "[ 3/8 → 1/2 | s:hh speed:0.5 ]", "[ 1/2 → 5/8 | s:hh speed:0.5 ]", - "[ 5/8 → 3/4 | s:hh speed:0.5 ]", + "[ 5/8 → 3/4 | s:hh ]", "[ 3/4 → 7/8 | s:hh speed:0.5 ]", - "[ 7/8 → 1/1 | s:hh speed:0.5 ]", + "[ 7/8 → 1/1 | s:hh ]", "[ 1/1 → 9/8 | s:hh speed:0.5 ]", - "[ 9/8 → 5/4 | s:hh speed:0.5 ]", + "[ 9/8 → 5/4 | s:hh ]", "[ 5/4 → 11/8 | s:hh speed:0.5 ]", - "[ 11/8 → 3/2 | s:hh speed:0.5 ]", + "[ 11/8 → 3/2 | s:hh ]", "[ 3/2 → 13/8 | s:hh speed:0.5 ]", "[ 13/8 → 7/4 | s:hh speed:0.5 ]", "[ 7/4 → 15/8 | s:hh speed:0.5 ]", - "[ 15/8 → 2/1 | s:hh speed:0.5 ]", - "[ 2/1 → 17/8 | s:hh ]", + "[ 15/8 → 2/1 | s:hh ]", + "[ 2/1 → 17/8 | s:hh speed:0.5 ]", "[ 17/8 → 9/4 | s:hh ]", "[ 9/4 → 19/8 | s:hh speed:0.5 ]", - "[ 19/8 → 5/2 | s:hh ]", + "[ 19/8 → 5/2 | s:hh speed:0.5 ]", "[ 5/2 → 21/8 | s:hh speed:0.5 ]", "[ 21/8 → 11/4 | s:hh speed:0.5 ]", "[ 11/4 → 23/8 | s:hh speed:0.5 ]", - "[ 23/8 → 3/1 | s:hh speed:0.5 ]", + "[ 23/8 → 3/1 | s:hh ]", "[ 3/1 → 25/8 | s:hh speed:0.5 ]", "[ 25/8 → 13/4 | s:hh speed:0.5 ]", - "[ 13/4 → 27/8 | s:hh ]", + "[ 13/4 → 27/8 | s:hh speed:0.5 ]", "[ 27/8 → 7/2 | s:hh speed:0.5 ]", "[ 7/2 → 29/8 | s:hh speed:0.5 ]", "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:hh ]", + "[ 15/4 → 31/8 | s:hh speed:0.5 ]", "[ 31/8 → 4/1 | s:hh speed:0.5 ]", ] `; @@ -6785,54 +6810,54 @@ exports[`runs examples > example "penv" example index 0 1`] = ` exports[`runs examples > example "perlin" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:hh cutoff:500 ]", - "[ 0/1 → 1/4 | s:bd cutoff:500 ]", - "[ 1/8 → 1/4 | s:hh cutoff:562.5486401770559 ]", - "[ 1/4 → 3/8 | s:hh cutoff:903.3554895067937 ]", - "[ 1/4 → 1/2 | s:bd cutoff:903.3554895067937 ]", - "[ 3/8 → 1/2 | s:hh cutoff:1572.364329119182 ]", - "[ 1/2 → 5/8 | s:hh cutoff:2448.2831191271544 ]", - "[ 1/2 → 3/4 | s:bd cutoff:2448.2831191271544 ]", - "[ 5/8 → 3/4 | s:hh cutoff:3324.2019091351267 ]", - "[ 3/4 → 7/8 | s:hh cutoff:3993.210748747515 ]", - "[ 3/4 → 1/1 | s:bd cutoff:3993.210748747515 ]", - "[ 7/8 → 1/1 | s:hh cutoff:4334.017598077253 ]", - "[ 1/1 → 9/8 | s:hh cutoff:4396.566238254309 ]", - "[ 1/1 → 5/4 | s:bd cutoff:4396.566238254309 ]", - "[ 9/8 → 5/4 | s:hh cutoff:4449.536839055554 ]", - "[ 5/4 → 11/8 | s:hh cutoff:4738.156120227359 ]", - "[ 5/4 → 3/2 | s:bd cutoff:4738.156120227359 ]", - "[ 11/8 → 3/2 | s:hh cutoff:5304.71999875931 ]", - "[ 3/2 → 13/8 | s:hh cutoff:6046.5098191052675 ]", - "[ 3/2 → 7/4 | s:bd cutoff:6046.5098191052675 ]", - "[ 13/8 → 7/4 | s:hh cutoff:6788.299639451225 ]", - "[ 7/4 → 15/8 | s:hh cutoff:7354.863517983176 ]", - "[ 7/4 → 2/1 | s:bd cutoff:7354.863517983176 ]", - "[ 15/8 → 2/1 | s:hh cutoff:7643.482799154981 ]", - "[ 2/1 → 17/8 | s:hh cutoff:7696.453399956226 ]", - "[ 2/1 → 9/4 | s:bd cutoff:7696.453399956226 ]", - "[ 17/8 → 9/4 | s:hh cutoff:7607.093996059746 ]", - "[ 9/4 → 19/8 | s:hh cutoff:7120.204164182724 ]", - "[ 9/4 → 5/2 | s:bd cutoff:7120.204164182724 ]", - "[ 19/8 → 5/2 | s:hh cutoff:6164.432289046601 ]", - "[ 5/2 → 21/8 | s:hh cutoff:4913.0608648993075 ]", - "[ 5/2 → 11/4 | s:bd cutoff:4913.0608648993075 ]", - "[ 21/8 → 11/4 | s:hh cutoff:3661.6894407520135 ]", - "[ 11/4 → 23/8 | s:hh cutoff:2705.9175656158914 ]", - "[ 11/4 → 3/1 | s:bd cutoff:2705.9175656158914 ]", - "[ 23/8 → 3/1 | s:hh cutoff:2219.0277337388693 ]", - "[ 3/1 → 25/8 | s:hh cutoff:2129.6683298423886 ]", - "[ 3/1 → 13/4 | s:bd cutoff:2129.6683298423886 ]", - "[ 25/8 → 13/4 | s:hh cutoff:2113.2537022102156 ]", - "[ 13/4 → 27/8 | s:hh cutoff:2023.8158261763601 ]", - "[ 13/4 → 7/2 | s:bd cutoff:2023.8158261763601 ]", - "[ 27/8 → 7/2 | s:hh cutoff:1848.2479648482126 ]", - "[ 7/2 → 29/8 | s:hh cutoff:1618.380764964968 ]", - "[ 7/2 → 15/4 | s:bd cutoff:1618.380764964968 ]", - "[ 29/8 → 15/4 | s:hh cutoff:1388.5135650817233 ]", - "[ 15/4 → 31/8 | s:hh cutoff:1212.9457037535758 ]", - "[ 15/4 → 4/1 | s:bd cutoff:1212.9457037535758 ]", - "[ 31/8 → 4/1 | s:hh cutoff:1123.5078277197204 ]", + "[ 0/1 → 1/8 | s:hh cutoff:3801.944299018942 ]", + "[ 0/1 → 1/4 | s:bd cutoff:3801.944299018942 ]", + "[ 1/8 → 1/4 | s:hh cutoff:3752.180364182138 ]", + "[ 1/4 → 3/8 | s:hh cutoff:3481.0331450903504 ]", + "[ 1/4 → 1/2 | s:bd cutoff:3481.0331450903504 ]", + "[ 3/8 → 1/2 | s:hh cutoff:2948.767180467044 ]", + "[ 1/2 → 5/8 | s:hh cutoff:2251.8828762695193 ]", + "[ 1/2 → 3/4 | s:bd cutoff:2251.8828762695193 ]", + "[ 5/8 → 3/4 | s:hh cutoff:1554.9985720719947 ]", + "[ 3/4 → 7/8 | s:hh cutoff:1022.7326074486882 ]", + "[ 3/4 → 1/1 | s:bd cutoff:1022.7326074486882 ]", + "[ 7/8 → 1/1 | s:hh cutoff:751.5853883569008 ]", + "[ 1/1 → 9/8 | s:hh cutoff:701.8214535200968 ]", + "[ 1/1 → 5/4 | s:bd cutoff:701.8214535200968 ]", + "[ 9/8 → 5/4 | s:hh cutoff:758.9508705680288 ]", + "[ 5/4 → 11/8 | s:hh cutoff:1070.2301657379394 ]", + "[ 5/4 → 3/2 | s:bd cutoff:1070.2301657379394 ]", + "[ 11/8 → 3/2 | s:hh cutoff:1681.2759838209531 ]", + "[ 3/2 → 13/8 | s:hh cutoff:2481.3050446100533 ]", + "[ 3/2 → 7/4 | s:bd cutoff:2481.3050446100533 ]", + "[ 13/8 → 7/4 | s:hh cutoff:3281.3341053991535 ]", + "[ 7/4 → 15/8 | s:hh cutoff:3892.379923482167 ]", + "[ 7/4 → 2/1 | s:bd cutoff:3892.379923482167 ]", + "[ 15/8 → 2/1 | s:hh cutoff:4203.6592186520775 ]", + "[ 2/1 → 17/8 | s:hh cutoff:4260.78863570001 ]", + "[ 2/1 → 9/4 | s:bd cutoff:4260.78863570001 ]", + "[ 17/8 → 9/4 | s:hh cutoff:4250.943561981614 ]", + "[ 9/4 → 19/8 | s:hh cutoff:4197.301012025491 ]", + "[ 9/4 → 5/2 | s:bd cutoff:4197.301012025491 ]", + "[ 19/8 → 5/2 | s:hh cutoff:4091.9999003530734 ]", + "[ 5/2 → 21/8 | s:hh cutoff:3954.1314345551655 ]", + "[ 5/2 → 11/4 | s:bd cutoff:3954.1314345551655 ]", + "[ 21/8 → 11/4 | s:hh cutoff:3816.2629687572576 ]", + "[ 11/4 → 23/8 | s:hh cutoff:3710.9618570848397 ]", + "[ 11/4 → 3/1 | s:bd cutoff:3710.9618570848397 ]", + "[ 23/8 → 3/1 | s:hh cutoff:3657.3193071287164 ]", + "[ 3/1 → 25/8 | s:hh cutoff:3647.474233410321 ]", + "[ 3/1 → 13/4 | s:bd cutoff:3647.474233410321 ]", + "[ 25/8 → 13/4 | s:hh cutoff:3623.927675406968 ]", + "[ 13/4 → 27/8 | s:hh cutoff:3495.6302700122706 ]", + "[ 13/4 → 7/2 | s:bd cutoff:3495.6302700122706 ]", + "[ 27/8 → 7/2 | s:hh cutoff:3243.7805830790653 ]", + "[ 7/2 → 29/8 | s:hh cutoff:2914.039240393322 ]", + "[ 7/2 → 15/4 | s:bd cutoff:2914.039240393322 ]", + "[ 29/8 → 15/4 | s:hh cutoff:2584.2978977075786 ]", + "[ 15/4 → 31/8 | s:hh cutoff:2332.4482107743734 ]", + "[ 15/4 → 4/1 | s:bd cutoff:2332.4482107743734 ]", + "[ 31/8 → 4/1 | s:hh cutoff:2204.150805379676 ]", ] `; @@ -7596,54 +7621,54 @@ exports[`runs examples > example "queryArc" example index 0 1`] = `[]`; exports[`runs examples > example "rand" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:hh cutoff:500 ]", - "[ 0/1 → 1/4 | s:bd cutoff:500 ]", - "[ 1/8 → 1/4 | s:hh cutoff:5639.116775244474 ]", - "[ 1/4 → 3/8 | s:hh cutoff:3273.1976890936494 ]", - "[ 1/4 → 1/2 | s:bd cutoff:3273.1976890936494 ]", - "[ 3/8 → 1/2 | s:hh cutoff:3510.4438681155443 ]", - "[ 1/2 → 5/8 | s:hh cutoff:2453.604621812701 ]", - "[ 1/2 → 3/4 | s:bd cutoff:2453.604621812701 ]", - "[ 5/8 → 3/4 | s:hh cutoff:1517.2690078616142 ]", - "[ 3/4 → 7/8 | s:hh cutoff:1968.6986012384295 ]", - "[ 3/4 → 1/1 | s:bd cutoff:1968.6986012384295 ]", - "[ 7/8 → 1/1 | s:hh cutoff:3482.2331061586738 ]", - "[ 1/1 → 9/8 | s:hh cutoff:4396.566238254309 ]", - "[ 1/1 → 5/4 | s:bd cutoff:4396.566238254309 ]", - "[ 9/8 → 5/4 | s:hh cutoff:5543.671359308064 ]", - "[ 5/4 → 11/8 | s:hh cutoff:5965.461523272097 ]", - "[ 5/4 → 3/2 | s:bd cutoff:5965.461523272097 ]", - "[ 11/8 → 3/2 | s:hh cutoff:1871.028139255941 ]", - "[ 3/2 → 13/8 | s:hh cutoff:5063.060561195016 ]", - "[ 3/2 → 7/4 | s:bd cutoff:5063.060561195016 ]", - "[ 13/8 → 7/4 | s:hh cutoff:4225.660336203873 ]", - "[ 7/4 → 15/8 | s:hh cutoff:2035.5342207476497 ]", - "[ 7/4 → 2/1 | s:bd cutoff:2035.5342207476497 ]", - "[ 15/8 → 2/1 | s:hh cutoff:4959.178843535483 ]", - "[ 2/1 → 17/8 | s:hh cutoff:7696.453399956226 ]", - "[ 2/1 → 9/4 | s:bd cutoff:7696.453399956226 ]", - "[ 17/8 → 9/4 | s:hh cutoff:7275.427204556763 ]", - "[ 9/4 → 19/8 | s:hh cutoff:3091.1188358440995 ]", - "[ 9/4 → 5/2 | s:bd cutoff:3091.1188358440995 ]", - "[ 19/8 → 5/2 | s:hh cutoff:6773.7998040392995 ]", - "[ 5/2 → 21/8 | s:hh cutoff:3939.620556309819 ]", - "[ 5/2 → 11/4 | s:bd cutoff:3939.620556309819 ]", - "[ 21/8 → 11/4 | s:hh cutoff:1701.4511320739985 ]", - "[ 11/4 → 23/8 | s:hh cutoff:5249.245778657496 ]", - "[ 11/4 → 3/1 | s:bd cutoff:5249.245778657496 ]", - "[ 23/8 → 3/1 | s:hh cutoff:621.9062879681587 ]", - "[ 3/1 → 25/8 | s:hh cutoff:2129.6683298423886 ]", - "[ 3/1 → 13/4 | s:bd cutoff:2129.6683298423886 ]", - "[ 25/8 → 13/4 | s:hh cutoff:3074.329087510705 ]", - "[ 13/4 → 27/8 | s:hh cutoff:7942.738036625087 ]", - "[ 13/4 → 7/2 | s:bd cutoff:7942.738036625087 ]", - "[ 27/8 → 7/2 | s:hh cutoff:3565.1885084807873 ]", - "[ 7/2 → 29/8 | s:hh cutoff:3574.6161099523306 ]", - "[ 7/2 → 15/4 | s:bd cutoff:3574.6161099523306 ]", - "[ 29/8 → 15/4 | s:hh cutoff:7543.614340946078 ]", - "[ 15/4 → 31/8 | s:hh cutoff:6591.254916973412 ]", - "[ 15/4 → 4/1 | s:bd cutoff:6591.254916973412 ]", - "[ 31/8 → 4/1 | s:hh cutoff:6021.249040029943 ]", + "[ 0/1 → 1/8 | s:hh cutoff:3801.944299018942 ]", + "[ 0/1 → 1/4 | s:bd cutoff:3801.944299018942 ]", + "[ 1/8 → 1/4 | s:hh cutoff:6647.100417292677 ]", + "[ 1/4 → 3/8 | s:hh cutoff:4935.184325557202 ]", + "[ 1/4 → 1/2 | s:bd cutoff:4935.184325557202 ]", + "[ 3/8 → 1/2 | s:hh cutoff:4274.936107802205 ]", + "[ 1/2 → 5/8 | s:hh cutoff:3599.519268143922 ]", + "[ 1/2 → 3/4 | s:bd cutoff:3599.519268143922 ]", + "[ 5/8 → 3/4 | s:hh cutoff:7203.329019364901 ]", + "[ 3/4 → 7/8 | s:hh cutoff:5311.3214410841465 ]", + "[ 3/4 → 1/1 | s:bd cutoff:5311.3214410841465 ]", + "[ 7/8 → 1/1 | s:hh cutoff:7230.677842278965 ]", + "[ 1/1 → 9/8 | s:hh cutoff:701.8214535200968 ]", + "[ 1/1 → 5/4 | s:bd cutoff:701.8214535200968 ]", + "[ 9/8 → 5/4 | s:hh cutoff:6626.493136398494 ]", + "[ 5/4 → 11/8 | s:hh cutoff:5536.982340388931 ]", + "[ 5/4 → 3/2 | s:bd cutoff:5536.982340388931 ]", + "[ 11/8 → 3/2 | s:hh cutoff:6926.114265923388 ]", + "[ 3/2 → 13/8 | s:hh cutoff:2300.4843831295148 ]", + "[ 3/2 → 7/4 | s:bd cutoff:2300.4843831295148 ]", + "[ 13/8 → 7/4 | s:hh cutoff:2510.1786771556363 ]", + "[ 7/4 → 15/8 | s:hh cutoff:5990.686780540273 ]", + "[ 7/4 → 2/1 | s:bd cutoff:5990.686780540273 ]", + "[ 15/8 → 2/1 | s:hh cutoff:6187.015319708735 ]", + "[ 2/1 → 17/8 | s:hh cutoff:4260.78863570001 ]", + "[ 2/1 → 9/4 | s:bd cutoff:4260.78863570001 ]", + "[ 17/8 → 9/4 | s:hh cutoff:6301.981445983984 ]", + "[ 9/4 → 19/8 | s:hh cutoff:5212.422806769609 ]", + "[ 9/4 → 5/2 | s:bd cutoff:5212.422806769609 ]", + "[ 19/8 → 5/2 | s:hh cutoff:5561.523957410827 ]", + "[ 5/2 → 21/8 | s:hh cutoff:3020.9835229907185 ]", + "[ 5/2 → 11/4 | s:bd cutoff:3020.9835229907185 ]", + "[ 21/8 → 11/4 | s:hh cutoff:576.5502714784816 ]", + "[ 11/4 → 23/8 | s:hh cutoff:5743.939216597937 ]", + "[ 11/4 → 3/1 | s:bd cutoff:5743.939216597937 ]", + "[ 23/8 → 3/1 | s:hh cutoff:7271.8438868178055 ]", + "[ 3/1 → 25/8 | s:hh cutoff:3647.474233410321 ]", + "[ 3/1 → 13/4 | s:bd cutoff:3647.474233410321 ]", + "[ 25/8 → 13/4 | s:hh cutoff:5633.619675179943 ]", + "[ 13/4 → 27/8 | s:hh cutoff:2873.2354695675895 ]", + "[ 13/4 → 7/2 | s:bd cutoff:2873.2354695675895 ]", + "[ 27/8 → 7/2 | s:hh cutoff:5596.839588950388 ]", + "[ 7/2 → 29/8 | s:hh cutoff:2628.3173956908286 ]", + "[ 7/2 → 15/4 | s:bd cutoff:2628.3173956908286 ]", + "[ 29/8 → 15/4 | s:hh cutoff:7124.103914946318 ]", + "[ 15/4 → 31/8 | s:hh cutoff:814.1072567086667 ]", + "[ 15/4 → 4/1 | s:bd cutoff:814.1072567086667 ]", + "[ 31/8 → 4/1 | s:hh cutoff:518.1868247454986 ]", ] `; @@ -7808,21 +7833,21 @@ exports[`runs examples > example "rangex" example index 0 1`] = ` exports[`runs examples > example "rarely" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:hh speed:0.5 ]", + "[ 0/1 → 1/8 | s:hh ]", "[ 1/8 → 1/4 | s:hh ]", "[ 1/4 → 3/8 | s:hh ]", "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:hh ]", - "[ 5/8 → 3/4 | s:hh speed:0.5 ]", - "[ 3/4 → 7/8 | s:hh speed:0.5 ]", + "[ 5/8 → 3/4 | s:hh ]", + "[ 3/4 → 7/8 | s:hh ]", "[ 7/8 → 1/1 | s:hh ]", - "[ 1/1 → 9/8 | s:hh ]", + "[ 1/1 → 9/8 | s:hh speed:0.5 ]", "[ 9/8 → 5/4 | s:hh ]", "[ 5/4 → 11/8 | s:hh ]", - "[ 11/8 → 3/2 | s:hh speed:0.5 ]", - "[ 3/2 → 13/8 | s:hh ]", + "[ 11/8 → 3/2 | s:hh ]", + "[ 3/2 → 13/8 | s:hh speed:0.5 ]", "[ 13/8 → 7/4 | s:hh ]", - "[ 7/4 → 15/8 | s:hh speed:0.5 ]", + "[ 7/4 → 15/8 | s:hh ]", "[ 15/8 → 2/1 | s:hh ]", "[ 2/1 → 17/8 | s:hh ]", "[ 17/8 → 9/4 | s:hh ]", @@ -7831,15 +7856,15 @@ exports[`runs examples > example "rarely" example index 0 1`] = ` "[ 5/2 → 21/8 | s:hh ]", "[ 21/8 → 11/4 | s:hh speed:0.5 ]", "[ 11/4 → 23/8 | s:hh ]", - "[ 23/8 → 3/1 | s:hh speed:0.5 ]", - "[ 3/1 → 25/8 | s:hh speed:0.5 ]", + "[ 23/8 → 3/1 | s:hh ]", + "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", "[ 13/4 → 27/8 | s:hh ]", "[ 27/8 → 7/2 | s:hh ]", "[ 7/2 → 29/8 | s:hh ]", "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:hh ]", - "[ 31/8 → 4/1 | s:hh ]", + "[ 15/4 → 31/8 | s:hh speed:0.5 ]", + "[ 31/8 → 4/1 | s:hh speed:0.5 ]", ] `; @@ -7883,22 +7908,22 @@ exports[`runs examples > example "release" example index 0 1`] = ` exports[`runs examples > example "repeatCycles" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:34 s:gm_acoustic_guitar_nylon ]", - "[ 1/4 → 1/2 | note:38 s:gm_acoustic_guitar_nylon ]", - "[ 1/2 → 3/4 | note:37 s:gm_acoustic_guitar_nylon ]", - "[ 3/4 → 1/1 | note:36 s:gm_acoustic_guitar_nylon ]", - "[ 1/1 → 5/4 | note:34 s:gm_acoustic_guitar_nylon ]", - "[ 5/4 → 3/2 | note:38 s:gm_acoustic_guitar_nylon ]", - "[ 3/2 → 7/4 | note:37 s:gm_acoustic_guitar_nylon ]", - "[ 7/4 → 2/1 | note:36 s:gm_acoustic_guitar_nylon ]", - "[ 2/1 → 9/4 | note:40 s:gm_acoustic_guitar_nylon ]", + "[ 0/1 → 1/4 | note:39 s:gm_acoustic_guitar_nylon ]", + "[ 1/4 → 1/2 | note:41 s:gm_acoustic_guitar_nylon ]", + "[ 1/2 → 3/4 | note:38 s:gm_acoustic_guitar_nylon ]", + "[ 3/4 → 1/1 | note:41 s:gm_acoustic_guitar_nylon ]", + "[ 1/1 → 5/4 | note:39 s:gm_acoustic_guitar_nylon ]", + "[ 5/4 → 3/2 | note:41 s:gm_acoustic_guitar_nylon ]", + "[ 3/2 → 7/4 | note:38 s:gm_acoustic_guitar_nylon ]", + "[ 7/4 → 2/1 | note:41 s:gm_acoustic_guitar_nylon ]", + "[ 2/1 → 9/4 | note:34 s:gm_acoustic_guitar_nylon ]", "[ 9/4 → 5/2 | note:42 s:gm_acoustic_guitar_nylon ]", - "[ 5/2 → 11/4 | note:41 s:gm_acoustic_guitar_nylon ]", - "[ 11/4 → 3/1 | note:36 s:gm_acoustic_guitar_nylon ]", - "[ 3/1 → 13/4 | note:40 s:gm_acoustic_guitar_nylon ]", + "[ 5/2 → 11/4 | note:36 s:gm_acoustic_guitar_nylon ]", + "[ 11/4 → 3/1 | note:42 s:gm_acoustic_guitar_nylon ]", + "[ 3/1 → 13/4 | note:34 s:gm_acoustic_guitar_nylon ]", "[ 13/4 → 7/2 | note:42 s:gm_acoustic_guitar_nylon ]", - "[ 7/2 → 15/4 | note:41 s:gm_acoustic_guitar_nylon ]", - "[ 15/4 → 4/1 | note:36 s:gm_acoustic_guitar_nylon ]", + "[ 7/2 → 15/4 | note:36 s:gm_acoustic_guitar_nylon ]", + "[ 15/4 → 4/1 | note:42 s:gm_acoustic_guitar_nylon ]", ] `; @@ -8036,67 +8061,43 @@ exports[`runs examples > example "ribbon" example index 0 1`] = ` exports[`runs examples > example "ribbon" example index 1 1`] = ` [ - "[ 0/1 → 1/4 | note:A3 ]", - "[ 1/4 → 1/2 | note:C3 ]", - "[ 1/2 → 3/4 | note:D3 ]", - "[ 3/4 → 1/1 | note:G3 ]", - "[ 1/1 → 5/4 | note:E3 ]", - "[ 5/4 → 3/2 | note:C4 ]", + "[ 0/1 → 1/4 | note:E4 ]", + "[ 1/4 → 1/2 | note:G3 ]", + "[ 1/2 → 3/4 | note:C3 ]", + "[ 3/4 → 1/1 | note:E4 ]", + "[ 1/1 → 5/4 | note:A3 ]", + "[ 5/4 → 3/2 | note:D3 ]", "[ 3/2 → 7/4 | note:A3 ]", - "[ 7/4 → 2/1 | note:E4 ]", - "[ 2/1 → 9/4 | note:A3 ]", - "[ 9/4 → 5/2 | note:C3 ]", - "[ 5/2 → 11/4 | note:D3 ]", - "[ 11/4 → 3/1 | note:G3 ]", - "[ 3/1 → 13/4 | note:E3 ]", - "[ 13/4 → 7/2 | note:C4 ]", + "[ 7/4 → 2/1 | note:G3 ]", + "[ 2/1 → 9/4 | note:E4 ]", + "[ 9/4 → 5/2 | note:G3 ]", + "[ 5/2 → 11/4 | note:C3 ]", + "[ 11/4 → 3/1 | note:E4 ]", + "[ 3/1 → 13/4 | note:A3 ]", + "[ 13/4 → 7/2 | note:D3 ]", "[ 7/2 → 15/4 | note:A3 ]", - "[ 15/4 → 4/1 | note:E4 ]", + "[ 15/4 → 4/1 | note:G3 ]", ] `; exports[`runs examples > example "ribbon" example index 2 1`] = ` [ - "[ 1/16 → 1/8 | s:bd ]", "[ 1/8 → 3/16 | s:bd ]", - "[ 3/16 → 1/4 | s:bd ]", - "[ 1/4 → 5/16 | s:bd ]", - "[ 3/8 → 7/16 | s:bd ]", - "[ 9/16 → 5/8 | s:bd ]", + "[ 7/16 → 1/2 | s:bd ]", "[ 5/8 → 11/16 | s:bd ]", - "[ 11/16 → 3/4 | s:bd ]", - "[ 3/4 → 13/16 | s:bd ]", - "[ 7/8 → 15/16 | s:bd ]", - "[ 17/16 → 9/8 | s:bd ]", + "[ 15/16 → 1/1 | s:bd ]", "[ 9/8 → 19/16 | s:bd ]", - "[ 19/16 → 5/4 | s:bd ]", - "[ 5/4 → 21/16 | s:bd ]", - "[ 11/8 → 23/16 | s:bd ]", - "[ 25/16 → 13/8 | s:bd ]", + "[ 23/16 → 3/2 | s:bd ]", "[ 13/8 → 27/16 | s:bd ]", - "[ 27/16 → 7/4 | s:bd ]", - "[ 7/4 → 29/16 | s:bd ]", - "[ 15/8 → 31/16 | s:bd ]", - "[ 33/16 → 17/8 | s:bd ]", + "[ 31/16 → 2/1 | s:bd ]", "[ 17/8 → 35/16 | s:bd ]", - "[ 35/16 → 9/4 | s:bd ]", - "[ 9/4 → 37/16 | s:bd ]", - "[ 19/8 → 39/16 | s:bd ]", - "[ 41/16 → 21/8 | s:bd ]", + "[ 39/16 → 5/2 | s:bd ]", "[ 21/8 → 43/16 | s:bd ]", - "[ 43/16 → 11/4 | s:bd ]", - "[ 11/4 → 45/16 | s:bd ]", - "[ 23/8 → 47/16 | s:bd ]", - "[ 49/16 → 25/8 | s:bd ]", + "[ 47/16 → 3/1 | s:bd ]", "[ 25/8 → 51/16 | s:bd ]", - "[ 51/16 → 13/4 | s:bd ]", - "[ 13/4 → 53/16 | s:bd ]", - "[ 27/8 → 55/16 | s:bd ]", - "[ 57/16 → 29/8 | s:bd ]", + "[ 55/16 → 7/2 | s:bd ]", "[ 29/8 → 59/16 | s:bd ]", - "[ 59/16 → 15/4 | s:bd ]", - "[ 15/4 → 61/16 | s:bd ]", - "[ 31/8 → 63/16 | s:bd ]", + "[ 63/16 → 4/1 | s:bd ]", ] `; @@ -8627,38 +8628,38 @@ exports[`runs examples > example "scale" example index 1 1`] = ` exports[`runs examples > example "scale" example index 2 1`] = ` [ - "[ 0/1 → 1/8 | note:C3 s:piano ]", - "[ 1/8 → 1/4 | note:A4 s:piano ]", - "[ 1/4 → 3/8 | note:C4 s:piano ]", - "[ 3/8 → 1/2 | note:C4 s:piano ]", - "[ 1/2 → 5/8 | note:A3 s:piano ]", - "[ 5/8 → 3/4 | note:F3 s:piano ]", - "[ 3/4 → 7/8 | note:G3 s:piano ]", - "[ 7/8 → 1/1 | note:C4 s:piano ]", - "[ 1/1 → 9/8 | note:F4 s:piano ]", - "[ 9/8 → 5/4 | note:A4 s:piano ]", + "[ 0/1 → 1/8 | note:D4 s:piano ]", + "[ 1/8 → 1/4 | note:C5 s:piano ]", + "[ 1/4 → 3/8 | note:G4 s:piano ]", + "[ 3/8 → 1/2 | note:F4 s:piano ]", + "[ 1/2 → 5/8 | note:C4 s:piano ]", + "[ 5/8 → 3/4 | note:D5 s:piano ]", + "[ 3/4 → 7/8 | note:G4 s:piano ]", + "[ 7/8 → 1/1 | note:D5 s:piano ]", + "[ 1/1 → 9/8 | note:D3 s:piano ]", + "[ 9/8 → 5/4 | note:C5 s:piano ]", "[ 5/4 → 11/8 | note:A4 s:piano ]", - "[ 11/8 → 3/2 | note:G3 s:piano ]", - "[ 3/2 → 13/8 | note:G4 s:piano ]", - "[ 13/8 → 7/4 | note:D4 s:piano ]", - "[ 7/4 → 15/8 | note:G3 s:piano ]", - "[ 15/8 → 2/1 | note:G4 s:piano ]", - "[ 2/1 → 17/8 | note:F5 s:piano ]", - "[ 17/8 → 9/4 | note:D5 s:piano ]", - "[ 9/4 → 19/8 | note:C4 s:piano ]", - "[ 19/8 → 5/2 | note:D5 s:piano ]", - "[ 5/2 → 21/8 | note:D4 s:piano ]", - "[ 21/8 → 11/4 | note:F3 s:piano ]", - "[ 11/4 → 23/8 | note:G4 s:piano ]", - "[ 23/8 → 3/1 | note:D3 s:piano ]", - "[ 3/1 → 25/8 | note:G3 s:piano ]", - "[ 25/8 → 13/4 | note:C4 s:piano ]", - "[ 13/4 → 27/8 | note:F5 s:piano ]", - "[ 27/8 → 7/2 | note:C4 s:piano ]", - "[ 7/2 → 29/8 | note:C4 s:piano ]", - "[ 29/8 → 15/4 | note:F5 s:piano ]", - "[ 15/4 → 31/8 | note:C5 s:piano ]", - "[ 31/8 → 4/1 | note:A4 s:piano ]", + "[ 11/8 → 3/2 | note:D5 s:piano ]", + "[ 3/2 → 13/8 | note:G3 s:piano ]", + "[ 13/8 → 7/4 | note:A3 s:piano ]", + "[ 7/4 → 15/8 | note:A4 s:piano ]", + "[ 15/8 → 2/1 | note:C5 s:piano ]", + "[ 2/1 → 17/8 | note:F4 s:piano ]", + "[ 17/8 → 9/4 | note:C5 s:piano ]", + "[ 9/4 → 19/8 | note:G4 s:piano ]", + "[ 19/8 → 5/2 | note:A4 s:piano ]", + "[ 5/2 → 21/8 | note:C4 s:piano ]", + "[ 21/8 → 11/4 | note:D3 s:piano ]", + "[ 11/4 → 23/8 | note:A4 s:piano ]", + "[ 23/8 → 3/1 | note:D5 s:piano ]", + "[ 3/1 → 25/8 | note:D4 s:piano ]", + "[ 25/8 → 13/4 | note:A4 s:piano ]", + "[ 13/4 → 27/8 | note:A3 s:piano ]", + "[ 27/8 → 7/2 | note:A4 s:piano ]", + "[ 7/2 → 29/8 | note:A3 s:piano ]", + "[ 29/8 → 15/4 | note:D5 s:piano ]", + "[ 15/4 → 31/8 | note:D3 s:piano ]", + "[ 31/8 → 4/1 | note:D3 s:piano ]", ] `; @@ -8694,46 +8695,46 @@ exports[`runs examples > example "scope" example index 0 1`] = ` exports[`runs examples > example "scramble" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:c s:piano ]", - "[ 1/4 → 1/2 | note:d s:piano ]", + "[ 0/1 → 1/4 | note:d s:piano ]", + "[ 1/4 → 1/2 | note:e s:piano ]", "[ 1/2 → 3/4 | note:d s:piano ]", - "[ 3/4 → 1/1 | note:c s:piano ]", - "[ 1/1 → 5/4 | note:e s:piano ]", + "[ 3/4 → 1/1 | note:e s:piano ]", + "[ 1/1 → 5/4 | note:c s:piano ]", "[ 5/4 → 3/2 | note:e s:piano ]", - "[ 3/2 → 7/4 | note:e s:piano ]", - "[ 7/4 → 2/1 | note:c s:piano ]", - "[ 2/1 → 9/4 | note:f s:piano ]", - "[ 9/4 → 5/2 | note:d s:piano ]", + "[ 3/2 → 7/4 | note:c s:piano ]", + "[ 7/4 → 2/1 | note:e s:piano ]", + "[ 2/1 → 9/4 | note:e s:piano ]", + "[ 9/4 → 5/2 | note:e s:piano ]", "[ 5/2 → 11/4 | note:d s:piano ]", "[ 11/4 → 3/1 | note:e s:piano ]", - "[ 3/1 → 13/4 | note:c s:piano ]", - "[ 13/4 → 7/2 | note:f s:piano ]", + "[ 3/1 → 13/4 | note:d s:piano ]", + "[ 13/4 → 7/2 | note:d s:piano ]", "[ 7/2 → 15/4 | note:d s:piano ]", - "[ 15/4 → 4/1 | note:f s:piano ]", + "[ 15/4 → 4/1 | note:c s:piano ]", ] `; exports[`runs examples > example "scramble" example index 1 1`] = ` [ - "[ 0/1 → 1/8 | note:c s:piano ]", - "[ 1/8 → 1/4 | note:d s:piano ]", + "[ 0/1 → 1/8 | note:d s:piano ]", + "[ 1/8 → 1/4 | note:e s:piano ]", "[ 1/4 → 3/8 | note:d s:piano ]", - "[ 3/8 → 1/2 | note:c s:piano ]", + "[ 3/8 → 1/2 | note:e s:piano ]", "[ 1/2 → 1/1 | note:g s:piano ]", - "[ 1/1 → 9/8 | note:e s:piano ]", + "[ 1/1 → 9/8 | note:c s:piano ]", "[ 9/8 → 5/4 | note:e s:piano ]", - "[ 5/4 → 11/8 | note:e s:piano ]", - "[ 11/8 → 3/2 | note:c s:piano ]", + "[ 5/4 → 11/8 | note:c s:piano ]", + "[ 11/8 → 3/2 | note:e s:piano ]", "[ 3/2 → 2/1 | note:g s:piano ]", - "[ 2/1 → 17/8 | note:f s:piano ]", - "[ 17/8 → 9/4 | note:d s:piano ]", + "[ 2/1 → 17/8 | note:e s:piano ]", + "[ 17/8 → 9/4 | note:e s:piano ]", "[ 9/4 → 19/8 | note:d s:piano ]", "[ 19/8 → 5/2 | note:e s:piano ]", "[ 5/2 → 3/1 | note:g s:piano ]", - "[ 3/1 → 25/8 | note:c s:piano ]", - "[ 25/8 → 13/4 | note:f s:piano ]", + "[ 3/1 → 25/8 | note:d s:piano ]", + "[ 25/8 → 13/4 | note:d s:piano ]", "[ 13/4 → 27/8 | note:d s:piano ]", - "[ 27/8 → 7/2 | note:f s:piano ]", + "[ 27/8 → 7/2 | note:c s:piano ]", "[ 7/2 → 4/1 | note:g s:piano ]", ] `; @@ -8967,6 +8968,64 @@ exports[`runs examples > example "seqPLoop" example index 0 1`] = ` ] `; +exports[`runs examples > example "setGainCurve" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd gain:0.5 ]", + "[ 1/4 → 1/2 | s:bd gain:0.5 ]", + "[ 1/2 → 3/4 | s:bd gain:0.5 ]", + "[ 3/4 → 1/1 | s:bd gain:0.5 ]", + "[ 1/1 → 5/4 | s:bd gain:0.5 ]", + "[ 5/4 → 3/2 | s:bd gain:0.5 ]", + "[ 3/2 → 7/4 | s:bd gain:0.5 ]", + "[ 7/4 → 2/1 | s:bd gain:0.5 ]", + "[ 2/1 → 9/4 | s:bd gain:0.5 ]", + "[ 9/4 → 5/2 | s:bd gain:0.5 ]", + "[ 5/2 → 11/4 | s:bd gain:0.5 ]", + "[ 11/4 → 3/1 | s:bd gain:0.5 ]", + "[ 3/1 → 13/4 | s:bd gain:0.5 ]", + "[ 13/4 → 7/2 | s:bd gain:0.5 ]", + "[ 7/2 → 15/4 | s:bd gain:0.5 ]", + "[ 15/4 → 4/1 | s:bd gain:0.5 ]", +] +`; + +exports[`runs examples > example "setMaxPolyphony" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | note:F#4 room:1 release:4 gain:0.5 ]", + "[ 1/8 → 1/4 | note:A5 room:1 release:4 gain:0.5 ]", + "[ 1/4 → 3/8 | note:C#5 room:1 release:4 gain:0.5 ]", + "[ 3/8 → 1/2 | note:A4 room:1 release:4 gain:0.5 ]", + "[ 1/2 → 5/8 | note:E4 room:1 release:4 gain:0.5 ]", + "[ 5/8 → 3/4 | note:C#6 room:1 release:4 gain:0.5 ]", + "[ 3/4 → 7/8 | note:D#5 room:1 release:4 gain:0.5 ]", + "[ 7/8 → 1/1 | note:C#6 room:1 release:4 gain:0.5 ]", + "[ 1/1 → 9/8 | note:C#3 room:1 release:4 gain:0.5 ]", + "[ 9/8 → 5/4 | note:A5 room:1 release:4 gain:0.5 ]", + "[ 5/4 → 11/8 | note:E5 room:1 release:4 gain:0.5 ]", + "[ 11/8 → 3/2 | note:B5 room:1 release:4 gain:0.5 ]", + "[ 3/2 → 13/8 | note:A3 room:1 release:4 gain:0.5 ]", + "[ 13/8 → 7/4 | note:B3 room:1 release:4 gain:0.5 ]", + "[ 7/4 → 15/8 | note:F#5 room:1 release:4 gain:0.5 ]", + "[ 15/8 → 2/1 | note:G#5 room:1 release:4 gain:0.5 ]", + "[ 2/1 → 17/8 | note:A4 room:1 release:4 gain:0.5 ]", + "[ 17/8 → 9/4 | note:G#5 room:1 release:4 gain:0.5 ]", + "[ 9/4 → 19/8 | note:D#5 room:1 release:4 gain:0.5 ]", + "[ 19/8 → 5/2 | note:E5 room:1 release:4 gain:0.5 ]", + "[ 5/2 → 21/8 | note:D#4 room:1 release:4 gain:0.5 ]", + "[ 21/8 → 11/4 | note:C#3 room:1 release:4 gain:0.5 ]", + "[ 11/4 → 23/8 | note:E5 room:1 release:4 gain:0.5 ]", + "[ 23/8 → 3/1 | note:C#6 room:1 release:4 gain:0.5 ]", + "[ 3/1 → 25/8 | note:F#4 room:1 release:4 gain:0.5 ]", + "[ 25/8 → 13/4 | note:E5 room:1 release:4 gain:0.5 ]", + "[ 13/4 → 27/8 | note:C#4 room:1 release:4 gain:0.5 ]", + "[ 27/8 → 7/2 | note:E5 room:1 release:4 gain:0.5 ]", + "[ 7/2 → 29/8 | note:B3 room:1 release:4 gain:0.5 ]", + "[ 29/8 → 15/4 | note:C#6 room:1 release:4 gain:0.5 ]", + "[ 15/4 → 31/8 | note:D#3 room:1 release:4 gain:0.5 ]", + "[ 31/8 → 4/1 | note:C#3 room:1 release:4 gain:0.5 ]", +] +`; + exports[`runs examples > example "setcpm" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:bd bank:tr707 ]", @@ -9200,45 +9259,45 @@ exports[`runs examples > example "shrink" example index 3 1`] = ` exports[`runs examples > example "shuffle" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:e s:piano ]", - "[ 1/4 → 1/2 | note:d s:piano ]", + "[ 1/4 → 1/2 | note:c s:piano ]", "[ 1/2 → 3/4 | note:f s:piano ]", - "[ 3/4 → 1/1 | note:c s:piano ]", - "[ 1/1 → 5/4 | note:e s:piano ]", + "[ 3/4 → 1/1 | note:d s:piano ]", + "[ 1/1 → 5/4 | note:f s:piano ]", "[ 5/4 → 3/2 | note:c s:piano ]", - "[ 3/2 → 7/4 | note:f s:piano ]", + "[ 3/2 → 7/4 | note:e s:piano ]", "[ 7/4 → 2/1 | note:d s:piano ]", - "[ 2/1 → 9/4 | note:d s:piano ]", - "[ 9/4 → 5/2 | note:c s:piano ]", - "[ 5/2 → 11/4 | note:e s:piano ]", - "[ 11/4 → 3/1 | note:f s:piano ]", + "[ 2/1 → 9/4 | note:c s:piano ]", + "[ 9/4 → 5/2 | note:f s:piano ]", + "[ 5/2 → 11/4 | note:d s:piano ]", + "[ 11/4 → 3/1 | note:e s:piano ]", "[ 3/1 → 13/4 | note:c s:piano ]", - "[ 13/4 → 7/2 | note:e s:piano ]", + "[ 13/4 → 7/2 | note:d s:piano ]", "[ 7/2 → 15/4 | note:f s:piano ]", - "[ 15/4 → 4/1 | note:d s:piano ]", + "[ 15/4 → 4/1 | note:e s:piano ]", ] `; exports[`runs examples > example "shuffle" example index 1 1`] = ` [ "[ 0/1 → 1/8 | note:e s:piano ]", - "[ 1/8 → 1/4 | note:d s:piano ]", + "[ 1/8 → 1/4 | note:c s:piano ]", "[ 1/4 → 3/8 | note:f s:piano ]", - "[ 3/8 → 1/2 | note:c s:piano ]", + "[ 3/8 → 1/2 | note:d s:piano ]", "[ 1/2 → 1/1 | note:g s:piano ]", - "[ 1/1 → 9/8 | note:e s:piano ]", + "[ 1/1 → 9/8 | note:f s:piano ]", "[ 9/8 → 5/4 | note:c s:piano ]", - "[ 5/4 → 11/8 | note:f s:piano ]", + "[ 5/4 → 11/8 | note:e s:piano ]", "[ 11/8 → 3/2 | note:d s:piano ]", "[ 3/2 → 2/1 | note:g s:piano ]", - "[ 2/1 → 17/8 | note:d s:piano ]", - "[ 17/8 → 9/4 | note:c s:piano ]", - "[ 9/4 → 19/8 | note:e s:piano ]", - "[ 19/8 → 5/2 | note:f s:piano ]", + "[ 2/1 → 17/8 | note:c s:piano ]", + "[ 17/8 → 9/4 | note:f s:piano ]", + "[ 9/4 → 19/8 | note:d s:piano ]", + "[ 19/8 → 5/2 | note:e s:piano ]", "[ 5/2 → 3/1 | note:g s:piano ]", "[ 3/1 → 25/8 | note:c s:piano ]", - "[ 25/8 → 13/4 | note:e s:piano ]", + "[ 25/8 → 13/4 | note:d s:piano ]", "[ 13/4 → 27/8 | note:f s:piano ]", - "[ 27/8 → 7/2 | note:d s:piano ]", + "[ 27/8 → 7/2 | note:e s:piano ]", "[ 7/2 → 4/1 | note:g s:piano ]", ] `; @@ -9425,15 +9484,15 @@ exports[`runs examples > example "someCycles" example index 0 1`] = ` "[ 5/8 → 3/4 | s:hh speed:0.5 ]", "[ 3/4 → 7/8 | s:hh speed:0.5 ]", "[ 7/8 → 1/1 | s:hh speed:0.5 ]", - "[ 1/1 → 9/8 | s:hh ]", - "[ 1/1 → 2/1 | s:bd ]", - "[ 9/8 → 5/4 | s:hh ]", - "[ 5/4 → 11/8 | s:hh ]", - "[ 11/8 → 3/2 | s:hh ]", - "[ 3/2 → 13/8 | s:hh ]", - "[ 13/8 → 7/4 | s:hh ]", - "[ 7/4 → 15/8 | s:hh ]", - "[ 15/8 → 2/1 | s:hh ]", + "[ 1/1 → 9/8 | s:hh speed:0.5 ]", + "[ 1/1 → 2/1 | s:bd speed:0.5 ]", + "[ 9/8 → 5/4 | s:hh speed:0.5 ]", + "[ 5/4 → 11/8 | s:hh speed:0.5 ]", + "[ 11/8 → 3/2 | s:hh speed:0.5 ]", + "[ 3/2 → 13/8 | s:hh speed:0.5 ]", + "[ 13/8 → 7/4 | s:hh speed:0.5 ]", + "[ 7/4 → 15/8 | s:hh speed:0.5 ]", + "[ 15/8 → 2/1 | s:hh speed:0.5 ]", "[ 2/1 → 17/8 | s:hh ]", "[ 2/1 → 3/1 | s:bd ]", "[ 17/8 → 9/4 | s:hh ]", @@ -9457,24 +9516,24 @@ exports[`runs examples > example "someCycles" example index 0 1`] = ` exports[`runs examples > example "someCyclesBy" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:hh speed:0.5 ]", - "[ 0/1 → 1/1 | s:bd speed:0.5 ]", - "[ 1/8 → 1/4 | s:hh speed:0.5 ]", - "[ 1/4 → 3/8 | s:hh speed:0.5 ]", - "[ 3/8 → 1/2 | s:hh speed:0.5 ]", - "[ 1/2 → 5/8 | s:hh speed:0.5 ]", - "[ 5/8 → 3/4 | s:hh speed:0.5 ]", - "[ 3/4 → 7/8 | s:hh speed:0.5 ]", - "[ 7/8 → 1/1 | s:hh speed:0.5 ]", - "[ 1/1 → 9/8 | s:hh ]", - "[ 1/1 → 2/1 | s:bd ]", - "[ 9/8 → 5/4 | s:hh ]", - "[ 5/4 → 11/8 | s:hh ]", - "[ 11/8 → 3/2 | s:hh ]", - "[ 3/2 → 13/8 | s:hh ]", - "[ 13/8 → 7/4 | s:hh ]", - "[ 7/4 → 15/8 | s:hh ]", - "[ 15/8 → 2/1 | s:hh ]", + "[ 0/1 → 1/8 | s:hh ]", + "[ 0/1 → 1/1 | s:bd ]", + "[ 1/8 → 1/4 | s:hh ]", + "[ 1/4 → 3/8 | s:hh ]", + "[ 3/8 → 1/2 | s:hh ]", + "[ 1/2 → 5/8 | s:hh ]", + "[ 5/8 → 3/4 | s:hh ]", + "[ 3/4 → 7/8 | s:hh ]", + "[ 7/8 → 1/1 | s:hh ]", + "[ 1/1 → 9/8 | s:hh speed:0.5 ]", + "[ 1/1 → 2/1 | s:bd speed:0.5 ]", + "[ 9/8 → 5/4 | s:hh speed:0.5 ]", + "[ 5/4 → 11/8 | s:hh speed:0.5 ]", + "[ 11/8 → 3/2 | s:hh speed:0.5 ]", + "[ 3/2 → 13/8 | s:hh speed:0.5 ]", + "[ 13/8 → 7/4 | s:hh speed:0.5 ]", + "[ 7/4 → 15/8 | s:hh speed:0.5 ]", + "[ 15/8 → 2/1 | s:hh speed:0.5 ]", "[ 2/1 → 17/8 | s:hh ]", "[ 2/1 → 3/1 | s:bd ]", "[ 17/8 → 9/4 | s:hh ]", @@ -9484,15 +9543,15 @@ exports[`runs examples > example "someCyclesBy" example index 0 1`] = ` "[ 21/8 → 11/4 | s:hh ]", "[ 11/4 → 23/8 | s:hh ]", "[ 23/8 → 3/1 | s:hh ]", - "[ 3/1 → 25/8 | s:hh speed:0.5 ]", - "[ 3/1 → 4/1 | s:bd speed:0.5 ]", - "[ 25/8 → 13/4 | s:hh speed:0.5 ]", - "[ 13/4 → 27/8 | s:hh speed:0.5 ]", - "[ 27/8 → 7/2 | s:hh speed:0.5 ]", - "[ 7/2 → 29/8 | s:hh speed:0.5 ]", - "[ 29/8 → 15/4 | s:hh speed:0.5 ]", - "[ 15/4 → 31/8 | s:hh speed:0.5 ]", - "[ 31/8 → 4/1 | s:hh speed:0.5 ]", + "[ 3/1 → 25/8 | s:hh ]", + "[ 3/1 → 4/1 | s:bd ]", + "[ 25/8 → 13/4 | s:hh ]", + "[ 13/4 → 27/8 | s:hh ]", + "[ 27/8 → 7/2 | s:hh ]", + "[ 7/2 → 29/8 | s:hh ]", + "[ 29/8 → 15/4 | s:hh ]", + "[ 15/4 → 31/8 | s:hh ]", + "[ 31/8 → 4/1 | s:hh ]", ] `; @@ -9500,73 +9559,73 @@ exports[`runs examples > example "sometimes" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh speed:0.5 ]", "[ 1/8 → 1/4 | s:hh ]", - "[ 1/4 → 3/8 | s:hh speed:0.5 ]", - "[ 3/8 → 1/2 | s:hh speed:0.5 ]", + "[ 1/4 → 3/8 | s:hh ]", + "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:hh speed:0.5 ]", - "[ 5/8 → 3/4 | s:hh speed:0.5 ]", - "[ 3/4 → 7/8 | s:hh speed:0.5 ]", - "[ 7/8 → 1/1 | s:hh speed:0.5 ]", - "[ 1/1 → 9/8 | s:hh ]", + "[ 5/8 → 3/4 | s:hh ]", + "[ 3/4 → 7/8 | s:hh ]", + "[ 7/8 → 1/1 | s:hh ]", + "[ 1/1 → 9/8 | s:hh speed:0.5 ]", "[ 9/8 → 5/4 | s:hh ]", "[ 5/4 → 11/8 | s:hh ]", - "[ 11/8 → 3/2 | s:hh speed:0.5 ]", - "[ 3/2 → 13/8 | s:hh ]", + "[ 11/8 → 3/2 | s:hh ]", + "[ 3/2 → 13/8 | s:hh speed:0.5 ]", "[ 13/8 → 7/4 | s:hh speed:0.5 ]", - "[ 7/4 → 15/8 | s:hh speed:0.5 ]", + "[ 7/4 → 15/8 | s:hh ]", "[ 15/8 → 2/1 | s:hh ]", "[ 2/1 → 17/8 | s:hh ]", "[ 17/8 → 9/4 | s:hh ]", - "[ 9/4 → 19/8 | s:hh speed:0.5 ]", + "[ 9/4 → 19/8 | s:hh ]", "[ 19/8 → 5/2 | s:hh ]", "[ 5/2 → 21/8 | s:hh speed:0.5 ]", "[ 21/8 → 11/4 | s:hh speed:0.5 ]", "[ 11/4 → 23/8 | s:hh ]", - "[ 23/8 → 3/1 | s:hh speed:0.5 ]", + "[ 23/8 → 3/1 | s:hh ]", "[ 3/1 → 25/8 | s:hh speed:0.5 ]", - "[ 25/8 → 13/4 | s:hh speed:0.5 ]", - "[ 13/4 → 27/8 | s:hh ]", - "[ 27/8 → 7/2 | s:hh speed:0.5 ]", + "[ 25/8 → 13/4 | s:hh ]", + "[ 13/4 → 27/8 | s:hh speed:0.5 ]", + "[ 27/8 → 7/2 | s:hh ]", "[ 7/2 → 29/8 | s:hh speed:0.5 ]", "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:hh ]", - "[ 31/8 → 4/1 | s:hh ]", + "[ 15/4 → 31/8 | s:hh speed:0.5 ]", + "[ 31/8 → 4/1 | s:hh speed:0.5 ]", ] `; exports[`runs examples > example "sometimesBy" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:hh speed:0.5 ]", + "[ 0/1 → 1/8 | s:hh ]", "[ 1/8 → 1/4 | s:hh ]", - "[ 1/4 → 3/8 | s:hh speed:0.5 ]", + "[ 1/4 → 3/8 | s:hh ]", "[ 3/8 → 1/2 | s:hh ]", - "[ 1/2 → 5/8 | s:hh speed:0.5 ]", - "[ 5/8 → 3/4 | s:hh speed:0.5 ]", - "[ 3/4 → 7/8 | s:hh speed:0.5 ]", - "[ 7/8 → 1/1 | s:hh speed:0.5 ]", - "[ 1/1 → 9/8 | s:hh ]", + "[ 1/2 → 5/8 | s:hh ]", + "[ 5/8 → 3/4 | s:hh ]", + "[ 3/4 → 7/8 | s:hh ]", + "[ 7/8 → 1/1 | s:hh ]", + "[ 1/1 → 9/8 | s:hh speed:0.5 ]", "[ 9/8 → 5/4 | s:hh ]", "[ 5/4 → 11/8 | s:hh ]", - "[ 11/8 → 3/2 | s:hh speed:0.5 ]", - "[ 3/2 → 13/8 | s:hh ]", - "[ 13/8 → 7/4 | s:hh ]", - "[ 7/4 → 15/8 | s:hh speed:0.5 ]", + "[ 11/8 → 3/2 | s:hh ]", + "[ 3/2 → 13/8 | s:hh speed:0.5 ]", + "[ 13/8 → 7/4 | s:hh speed:0.5 ]", + "[ 7/4 → 15/8 | s:hh ]", "[ 15/8 → 2/1 | s:hh ]", "[ 2/1 → 17/8 | s:hh ]", "[ 17/8 → 9/4 | s:hh ]", - "[ 9/4 → 19/8 | s:hh speed:0.5 ]", + "[ 9/4 → 19/8 | s:hh ]", "[ 19/8 → 5/2 | s:hh ]", - "[ 5/2 → 21/8 | s:hh ]", + "[ 5/2 → 21/8 | s:hh speed:0.5 ]", "[ 21/8 → 11/4 | s:hh speed:0.5 ]", "[ 11/4 → 23/8 | s:hh ]", - "[ 23/8 → 3/1 | s:hh speed:0.5 ]", - "[ 3/1 → 25/8 | s:hh speed:0.5 ]", - "[ 25/8 → 13/4 | s:hh speed:0.5 ]", - "[ 13/4 → 27/8 | s:hh ]", + "[ 23/8 → 3/1 | s:hh ]", + "[ 3/1 → 25/8 | s:hh ]", + "[ 25/8 → 13/4 | s:hh ]", + "[ 13/4 → 27/8 | s:hh speed:0.5 ]", "[ 27/8 → 7/2 | s:hh ]", - "[ 7/2 → 29/8 | s:hh ]", + "[ 7/2 → 29/8 | s:hh speed:0.5 ]", "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:hh ]", - "[ 31/8 → 4/1 | s:hh ]", + "[ 15/4 → 31/8 | s:hh speed:0.5 ]", + "[ 31/8 → 4/1 | s:hh speed:0.5 ]", ] `; @@ -10950,23 +11009,17 @@ exports[`runs examples > example "tri" example index 0 1`] = ` exports[`runs examples > example "undegrade" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh ]", - "[ 1/4 → 3/8 | s:hh ]", - "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:hh ]", - "[ 5/8 → 3/4 | s:hh ]", - "[ 3/4 → 7/8 | s:hh ]", - "[ 7/8 → 1/1 | s:hh ]", - "[ 11/8 → 3/2 | s:hh ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 3/2 → 13/8 | s:hh ]", "[ 13/8 → 7/4 | s:hh ]", - "[ 7/4 → 15/8 | s:hh ]", - "[ 9/4 → 19/8 | s:hh ]", "[ 5/2 → 21/8 | s:hh ]", "[ 21/8 → 11/4 | s:hh ]", - "[ 23/8 → 3/1 | s:hh ]", "[ 3/1 → 25/8 | s:hh ]", - "[ 25/8 → 13/4 | s:hh ]", - "[ 27/8 → 7/2 | s:hh ]", + "[ 13/4 → 27/8 | s:hh ]", "[ 7/2 → 29/8 | s:hh ]", + "[ 15/4 → 31/8 | s:hh ]", + "[ 31/8 → 4/1 | s:hh ]", ] `; @@ -10974,82 +11027,81 @@ exports[`runs examples > example "undegrade" example index 1 1`] = ` [ "[ 0/1 → 1/10 | s:hh pan:1 ]", "[ 1/10 → 1/5 | s:hh pan:1 ]", - "[ 1/5 → 3/10 | s:hh pan:0 ]", + "[ 1/5 → 3/10 | s:hh pan:1 ]", "[ 3/10 → 2/5 | s:hh pan:0 ]", - "[ 2/5 → 1/2 | s:hh pan:0 ]", + "[ 2/5 → 1/2 | s:hh pan:1 ]", "[ 1/2 → 3/5 | s:hh pan:1 ]", "[ 3/5 → 7/10 | s:hh pan:0 ]", "[ 7/10 → 4/5 | s:hh pan:0 ]", - "[ 4/5 → 9/10 | s:hh pan:1 ]", - "[ 9/10 → 1/1 | s:hh pan:0 ]", - "[ 1/1 → 11/10 | s:hh pan:0 ]", + "[ 4/5 → 9/10 | s:hh pan:0 ]", + "[ 9/10 → 1/1 | s:hh pan:1 ]", + "[ 1/1 → 11/10 | s:hh pan:1 ]", "[ 11/10 → 6/5 | s:hh pan:0 ]", - "[ 6/5 → 13/10 | s:hh pan:1 ]", + "[ 6/5 → 13/10 | s:hh pan:0 ]", "[ 13/10 → 7/5 | s:hh pan:1 ]", "[ 7/5 → 3/2 | s:hh pan:1 ]", - "[ 3/2 → 8/5 | s:hh pan:0 ]", - "[ 8/5 → 17/10 | s:hh pan:1 ]", - "[ 17/10 → 9/5 | s:hh pan:1 ]", + "[ 3/2 → 8/5 | s:hh pan:1 ]", + "[ 8/5 → 17/10 | s:hh pan:0 ]", + "[ 17/10 → 9/5 | s:hh pan:0 ]", "[ 9/5 → 19/10 | s:hh pan:1 ]", "[ 19/10 → 2/1 | s:hh pan:1 ]", "[ 2/1 → 21/10 | s:hh pan:0 ]", "[ 21/10 → 11/5 | s:hh pan:1 ]", "[ 11/5 → 23/10 | s:hh pan:0 ]", - "[ 23/10 → 12/5 | s:hh pan:0 ]", - "[ 12/5 → 5/2 | s:hh pan:0 ]", + "[ 23/10 → 12/5 | s:hh pan:1 ]", + "[ 12/5 → 5/2 | s:hh pan:1 ]", "[ 5/2 → 13/5 | s:hh pan:1 ]", - "[ 13/5 → 27/10 | s:hh pan:1 ]", + "[ 13/5 → 27/10 | s:hh pan:0 ]", "[ 27/10 → 14/5 | s:hh pan:1 ]", - "[ 14/5 → 29/10 | s:hh pan:0 ]", + "[ 14/5 → 29/10 | s:hh pan:1 ]", "[ 29/10 → 3/1 | s:hh pan:1 ]", "[ 3/1 → 31/10 | s:hh pan:1 ]", - "[ 31/10 → 16/5 | s:hh pan:1 ]", + "[ 31/10 → 16/5 | s:hh pan:0 ]", "[ 16/5 → 33/10 | s:hh pan:1 ]", "[ 33/10 → 17/5 | s:hh pan:0 ]", "[ 17/5 → 7/2 | s:hh pan:0 ]", "[ 7/2 → 18/5 | s:hh pan:1 ]", "[ 18/5 → 37/10 | s:hh pan:0 ]", - "[ 37/10 → 19/5 | s:hh pan:0 ]", - "[ 19/5 → 39/10 | s:hh pan:1 ]", - "[ 39/10 → 4/1 | s:hh pan:1 ]", + "[ 37/10 → 19/5 | s:hh pan:1 ]", + "[ 19/5 → 39/10 | s:hh pan:0 ]", + "[ 39/10 → 4/1 | s:hh pan:0 ]", ] `; exports[`runs examples > example "undegradeBy" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh ]", - "[ 1/8 → 1/4 | s:hh ]", "[ 1/4 → 3/8 | s:hh ]", "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:hh ]", - "[ 5/8 → 3/4 | s:hh ]", "[ 3/4 → 7/8 | s:hh ]", - "[ 7/8 → 1/1 | s:hh ]", "[ 1/1 → 9/8 | s:hh ]", - "[ 9/8 → 5/4 | s:hh ]", "[ 5/4 → 11/8 | s:hh ]", - "[ 11/8 → 3/2 | s:hh ]", "[ 3/2 → 13/8 | s:hh ]", "[ 13/8 → 7/4 | s:hh ]", "[ 7/4 → 15/8 | s:hh ]", "[ 15/8 → 2/1 | s:hh ]", + "[ 2/1 → 17/8 | s:hh ]", + "[ 17/8 → 9/4 | s:hh ]", "[ 9/4 → 19/8 | s:hh ]", + "[ 19/8 → 5/2 | s:hh ]", "[ 5/2 → 21/8 | s:hh ]", "[ 21/8 → 11/4 | s:hh ]", "[ 11/4 → 23/8 | s:hh ]", - "[ 23/8 → 3/1 | s:hh ]", "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", + "[ 13/4 → 27/8 | s:hh ]", "[ 27/8 → 7/2 | s:hh ]", "[ 7/2 → 29/8 | s:hh ]", + "[ 15/4 → 31/8 | s:hh ]", "[ 31/8 → 4/1 | s:hh ]", ] `; exports[`runs examples > example "undegradeBy" example index 1 1`] = ` [ - "[ 0/1 → 1/10 | s:hh pan:1 ]", - "[ 1/10 → 1/5 | s:hh pan:1 ]", + "[ 0/1 → 1/10 | s:hh pan:0 ]", + "[ 1/10 → 1/5 | s:hh pan:0 ]", "[ 1/5 → 3/10 | s:hh pan:0 ]", "[ 3/10 → 2/5 | s:hh pan:0 ]", "[ 2/5 → 1/2 | s:hh pan:0 ]", @@ -11058,35 +11110,35 @@ exports[`runs examples > example "undegradeBy" example index 1 1`] = ` "[ 7/10 → 4/5 | s:hh pan:0 ]", "[ 4/5 → 9/10 | s:hh pan:0 ]", "[ 9/10 → 1/1 | s:hh pan:0 ]", - "[ 1/1 → 11/10 | s:hh pan:0 ]", + "[ 1/1 → 11/10 | s:hh pan:1 ]", "[ 11/10 → 6/5 | s:hh pan:0 ]", "[ 6/5 → 13/10 | s:hh pan:0 ]", - "[ 13/10 → 7/5 | s:hh pan:0 ]", - "[ 7/5 → 3/2 | s:hh pan:0 ]", + "[ 13/10 → 7/5 | s:hh pan:1 ]", + "[ 7/5 → 3/2 | s:hh pan:1 ]", "[ 3/2 → 8/5 | s:hh pan:0 ]", "[ 8/5 → 17/10 | s:hh pan:0 ]", "[ 17/10 → 9/5 | s:hh pan:0 ]", "[ 9/5 → 19/10 | s:hh pan:0 ]", "[ 19/10 → 2/1 | s:hh pan:1 ]", "[ 2/1 → 21/10 | s:hh pan:0 ]", - "[ 21/10 → 11/5 | s:hh pan:0 ]", + "[ 21/10 → 11/5 | s:hh pan:1 ]", "[ 11/5 → 23/10 | s:hh pan:0 ]", - "[ 23/10 → 12/5 | s:hh pan:0 ]", + "[ 23/10 → 12/5 | s:hh pan:1 ]", "[ 12/5 → 5/2 | s:hh pan:0 ]", "[ 5/2 → 13/5 | s:hh pan:0 ]", "[ 13/5 → 27/10 | s:hh pan:0 ]", - "[ 27/10 → 14/5 | s:hh pan:0 ]", - "[ 14/5 → 29/10 | s:hh pan:0 ]", + "[ 27/10 → 14/5 | s:hh pan:1 ]", + "[ 14/5 → 29/10 | s:hh pan:1 ]", "[ 29/10 → 3/1 | s:hh pan:0 ]", "[ 3/1 → 31/10 | s:hh pan:0 ]", - "[ 31/10 → 16/5 | s:hh pan:1 ]", + "[ 31/10 → 16/5 | s:hh pan:0 ]", "[ 16/5 → 33/10 | s:hh pan:1 ]", "[ 33/10 → 17/5 | s:hh pan:0 ]", "[ 17/5 → 7/2 | s:hh pan:0 ]", "[ 7/2 → 18/5 | s:hh pan:0 ]", "[ 18/5 → 37/10 | s:hh pan:0 ]", - "[ 37/10 → 19/5 | s:hh pan:0 ]", - "[ 19/5 → 39/10 | s:hh pan:1 ]", + "[ 37/10 → 19/5 | s:hh pan:1 ]", + "[ 19/5 → 39/10 | s:hh pan:0 ]", "[ 39/10 → 4/1 | s:hh pan:0 ]", ] `; @@ -11165,6 +11217,75 @@ exports[`runs examples > example "unit" example index 0 1`] = ` ] `; +exports[`runs examples > example "useOldRandom" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:D8 ]", + "[ 1/16 → 1/8 | note:Bb7 ]", + "[ 1/8 → 3/16 | note:Ab6 ]", + "[ 3/16 → 1/4 | note:D5 ]", + "[ 1/4 → 5/16 | note:Ab9 ]", + "[ 5/16 → 3/8 | note:D3 ]", + "[ 3/8 → 7/16 | note:G5 ]", + "[ 7/16 → 1/2 | note:G3 ]", + "[ 1/2 → 9/16 | note:Ab3 ]", + "[ 9/16 → 5/8 | note:Eb6 ]", + "[ 5/8 → 11/16 | note:Eb6 ]", + "[ 11/16 → 3/4 | note:Eb5 ]", + "[ 3/4 → 13/16 | note:G7 ]", + "[ 13/16 → 7/8 | note:Bb5 ]", + "[ 7/8 → 15/16 | note:D3 ]", + "[ 15/16 → 1/1 | note:Eb7 ]", + "[ 1/1 → 17/16 | note:Eb9 ]", + "[ 17/16 → 9/8 | note:G8 ]", + "[ 9/8 → 19/16 | note:Ab3 ]", + "[ 19/16 → 5/4 | note:Ab5 ]", + "[ 5/4 → 21/16 | note:C10 ]", + "[ 21/16 → 11/8 | note:C7 ]", + "[ 11/8 → 23/16 | note:G5 ]", + "[ 23/16 → 3/2 | note:Ab7 ]", + "[ 3/2 → 25/16 | note:G6 ]", + "[ 25/16 → 13/8 | note:Bb6 ]", + "[ 13/8 → 27/16 | note:Eb9 ]", + "[ 27/16 → 7/4 | note:G9 ]", + "[ 7/4 → 29/16 | note:G7 ]", + "[ 29/16 → 15/8 | note:C10 ]", + "[ 15/8 → 31/16 | note:Eb3 ]", + "[ 31/16 → 2/1 | note:Ab7 ]", + "[ 2/1 → 33/16 | note:F6 ]", + "[ 33/16 → 17/8 | note:C5 ]", + "[ 17/8 → 35/16 | note:Ab4 ]", + "[ 35/16 → 9/4 | note:G5 ]", + "[ 9/4 → 37/16 | note:C9 ]", + "[ 37/16 → 19/8 | note:Eb6 ]", + "[ 19/8 → 39/16 | note:C9 ]", + "[ 39/16 → 5/2 | note:Eb9 ]", + "[ 5/2 → 41/16 | note:D5 ]", + "[ 41/16 → 21/8 | note:F5 ]", + "[ 21/8 → 43/16 | note:F9 ]", + "[ 43/16 → 11/4 | note:Bb7 ]", + "[ 11/4 → 45/16 | note:Ab6 ]", + "[ 45/16 → 23/8 | note:Bb9 ]", + "[ 23/8 → 47/16 | note:C8 ]", + "[ 47/16 → 3/1 | note:Eb5 ]", + "[ 3/1 → 49/16 | note:F3 ]", + "[ 49/16 → 25/8 | note:G4 ]", + "[ 25/8 → 51/16 | note:D7 ]", + "[ 51/16 → 13/4 | note:D4 ]", + "[ 13/4 → 53/16 | note:F8 ]", + "[ 53/16 → 27/8 | note:C7 ]", + "[ 27/8 → 55/16 | note:Ab5 ]", + "[ 55/16 → 7/2 | note:Ab3 ]", + "[ 7/2 → 57/16 | note:F9 ]", + "[ 57/16 → 29/8 | note:D8 ]", + "[ 29/8 → 59/16 | note:F5 ]", + "[ 59/16 → 15/4 | note:Eb9 ]", + "[ 15/4 → 61/16 | note:Bb4 ]", + "[ 61/16 → 31/8 | note:C6 ]", + "[ 31/8 → 63/16 | note:C4 ]", + "[ 63/16 → 4/1 | note:G8 ]", +] +`; + exports[`runs examples > example "velocity" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh gain:0.4 velocity:0.4 ]", @@ -11323,38 +11444,38 @@ exports[`runs examples > example "vowel" example index 0 1`] = ` exports[`runs examples > example "vowel" example index 1 1`] = ` [ - "[ 0/1 → 1/8 | s:bd vowel:a ]", - "[ 1/8 → 1/4 | s:sd vowel:a ]", - "[ 1/4 → 3/8 | s:mt vowel:a ]", - "[ 3/8 → 1/2 | s:ht vowel:a ]", - "[ 1/2 → 5/8 | s:bd vowel:a ]", - "[ 11/16 → 3/4 | s:cp vowel:a ]", - "[ 3/4 → 7/8 | s:ht vowel:a ]", - "[ 7/8 → 1/1 | s:lt vowel:a ]", - "[ 1/1 → 9/8 | s:bd vowel:i ]", - "[ 9/8 → 5/4 | s:sd vowel:i ]", - "[ 5/4 → 11/8 | s:mt vowel:i ]", - "[ 11/8 → 3/2 | s:ht vowel:i ]", - "[ 3/2 → 13/8 | s:bd vowel:i ]", - "[ 27/16 → 7/4 | s:cp vowel:i ]", - "[ 7/4 → 15/8 | s:ht vowel:i ]", - "[ 15/8 → 2/1 | s:lt vowel:i ]", - "[ 2/1 → 17/8 | s:bd vowel:u ]", - "[ 17/8 → 9/4 | s:sd vowel:u ]", - "[ 9/4 → 19/8 | s:mt vowel:u ]", - "[ 19/8 → 5/2 | s:ht vowel:u ]", - "[ 5/2 → 21/8 | s:bd vowel:u ]", - "[ 43/16 → 11/4 | s:cp vowel:u ]", - "[ 11/4 → 23/8 | s:ht vowel:u ]", - "[ 23/8 → 3/1 | s:lt vowel:u ]", - "[ 3/1 → 25/8 | s:bd vowel:e ]", - "[ 25/8 → 13/4 | s:sd vowel:e ]", - "[ 13/4 → 27/8 | s:mt vowel:e ]", - "[ 27/8 → 7/2 | s:ht vowel:e ]", - "[ 7/2 → 29/8 | s:bd vowel:e ]", - "[ 59/16 → 15/4 | s:cp vowel:e ]", - "[ 15/4 → 31/8 | s:ht vowel:e ]", - "[ 31/8 → 4/1 | s:lt vowel:e ]", + "[ 0/1 → 1/8 | s:bd vowel:i ]", + "[ 1/8 → 1/4 | s:sd vowel:i ]", + "[ 1/4 → 3/8 | s:mt vowel:i ]", + "[ 3/8 → 1/2 | s:ht vowel:i ]", + "[ 1/2 → 5/8 | s:bd vowel:i ]", + "[ 11/16 → 3/4 | s:cp vowel:i ]", + "[ 3/4 → 7/8 | s:ht vowel:i ]", + "[ 7/8 → 1/1 | s:lt vowel:i ]", + "[ 1/1 → 9/8 | s:bd vowel:a ]", + "[ 9/8 → 5/4 | s:sd vowel:a ]", + "[ 5/4 → 11/8 | s:mt vowel:a ]", + "[ 11/8 → 3/2 | s:ht vowel:a ]", + "[ 3/2 → 13/8 | s:bd vowel:a ]", + "[ 27/16 → 7/4 | s:cp vowel:a ]", + "[ 7/4 → 15/8 | s:ht vowel:a ]", + "[ 15/8 → 2/1 | s:lt vowel:a ]", + "[ 2/1 → 17/8 | s:bd vowel:i ]", + "[ 17/8 → 9/4 | s:sd vowel:i ]", + "[ 9/4 → 19/8 | s:mt vowel:i ]", + "[ 19/8 → 5/2 | s:ht vowel:i ]", + "[ 5/2 → 21/8 | s:bd vowel:i ]", + "[ 43/16 → 11/4 | s:cp vowel:i ]", + "[ 11/4 → 23/8 | s:ht vowel:i ]", + "[ 23/8 → 3/1 | s:lt vowel:i ]", + "[ 3/1 → 25/8 | s:bd vowel:i ]", + "[ 25/8 → 13/4 | s:sd vowel:i ]", + "[ 13/4 → 27/8 | s:mt vowel:i ]", + "[ 27/8 → 7/2 | s:ht vowel:i ]", + "[ 7/2 → 29/8 | s:bd vowel:i ]", + "[ 59/16 → 15/4 | s:cp vowel:i ]", + "[ 15/4 → 31/8 | s:ht vowel:i ]", + "[ 31/8 → 4/1 | s:lt vowel:i ]", ] `; @@ -11363,21 +11484,21 @@ exports[`runs examples > example "wchoose" example index 0 1`] = ` "[ 0/1 → 1/5 | note:c2 s:sine ]", "[ 1/5 → 2/5 | note:g2 s:sine ]", "[ 2/5 → 3/5 | note:g2 s:sine ]", - "[ 3/5 → 4/5 | note:d2 s:sine ]", - "[ 4/5 → 1/1 | note:f1 s:sine ]", + "[ 3/5 → 4/5 | note:d2 s:bd n:6 ]", + "[ 4/5 → 1/1 | note:f1 s:bd n:6 ]", "[ 1/1 → 6/5 | note:c2 s:sine ]", "[ 6/5 → 7/5 | note:g2 s:sine ]", "[ 7/5 → 8/5 | note:g2 s:sine ]", "[ 8/5 → 9/5 | note:d2 s:sine ]", "[ 9/5 → 2/1 | note:f1 s:sine ]", - "[ 2/1 → 11/5 | note:c2 s:bd n:6 ]", - "[ 11/5 → 12/5 | note:g2 s:sine ]", - "[ 12/5 → 13/5 | note:g2 s:bd n:6 ]", + "[ 2/1 → 11/5 | note:c2 s:sine ]", + "[ 11/5 → 12/5 | note:g2 s:triangle ]", + "[ 12/5 → 13/5 | note:g2 s:sine ]", "[ 13/5 → 14/5 | note:d2 s:sine ]", "[ 14/5 → 3/1 | note:f1 s:sine ]", "[ 3/1 → 16/5 | note:c2 s:sine ]", "[ 16/5 → 17/5 | note:g2 s:sine ]", - "[ 17/5 → 18/5 | note:g2 s:sine ]", + "[ 17/5 → 18/5 | note:g2 s:triangle ]", "[ 18/5 → 19/5 | note:d2 s:sine ]", "[ 19/5 → 4/1 | note:f1 s:sine ]", ] @@ -11387,29 +11508,29 @@ exports[`runs examples > example "wchooseCycles" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:bd ]", "[ 1/8 → 1/4 | s:bd ]", - "[ 1/4 → 3/8 | s:sd ]", + "[ 1/4 → 3/8 | s:bd ]", "[ 3/8 → 1/2 | s:bd ]", "[ 1/2 → 5/8 | s:bd ]", "[ 5/8 → 3/4 | s:bd ]", - "[ 3/4 → 7/8 | s:bd ]", + "[ 3/4 → 7/8 | s:sd ]", "[ 7/8 → 1/1 | s:bd ]", - "[ 1/1 → 9/8 | s:hh ]", - "[ 9/8 → 5/4 | s:bd ]", + "[ 1/1 → 9/8 | s:bd ]", + "[ 9/8 → 5/4 | s:sd ]", "[ 5/4 → 11/8 | s:bd ]", "[ 11/8 → 3/2 | s:bd ]", "[ 3/2 → 13/8 | s:bd ]", - "[ 13/8 → 7/4 | s:sd ]", - "[ 7/4 → 15/8 | s:bd ]", + "[ 13/8 → 7/4 | s:bd ]", + "[ 7/4 → 15/8 | s:sd ]", "[ 15/8 → 2/1 | s:bd ]", "[ 2/1 → 17/8 | s:bd ]", - "[ 17/8 → 9/4 | s:bd ]", + "[ 17/8 → 9/4 | s:sd ]", "[ 9/4 → 19/8 | s:bd ]", "[ 19/8 → 5/2 | s:bd ]", "[ 5/2 → 21/8 | s:bd ]", "[ 21/8 → 11/4 | s:bd ]", - "[ 11/4 → 23/8 | s:sd ]", + "[ 11/4 → 23/8 | s:bd ]", "[ 23/8 → 3/1 | s:bd ]", - "[ 3/1 → 25/8 | s:bd ]", + "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:bd ]", "[ 13/4 → 27/8 | s:bd ]", "[ 27/8 → 7/2 | s:bd ]", @@ -11428,9 +11549,9 @@ exports[`runs examples > example "wchooseCycles" example index 1 1`] = ` "[ 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 ]", + "[ 1/2 → 7/12 | s:bd ]", + "[ 7/12 → 2/3 | s:bd ]", + "[ 2/3 → 3/4 | s:bd ]", "[ 3/4 → 5/6 | s:bd ]", "[ 5/6 → 11/12 | s:bd ]", "[ 11/12 → 1/1 | s:bd ]", @@ -11440,36 +11561,36 @@ exports[`runs examples > example "wchooseCycles" example index 1 1`] = ` "[ 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 ]", + "[ 3/2 → 19/12 | s:sd ]", + "[ 19/12 → 5/3 | s:sd ]", + "[ 5/3 → 7/4 | s:sd ]", "[ 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 ]", + "[ 9/4 → 7/3 | s:sd ]", + "[ 7/3 → 29/12 | s:sd ]", + "[ 29/12 → 5/2 | s:sd ]", + "[ 5/2 → 31/12 | s:hh ]", + "[ 31/12 → 8/3 | s:hh ]", + "[ 8/3 → 11/4 | s:hh ]", "[ 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 ]", + "[ 13/4 → 10/3 | s:bd ]", + "[ 10/3 → 41/12 | s:bd ]", + "[ 41/12 → 7/2 | s:bd ]", + "[ 7/2 → 43/12 | s:sd ]", + "[ 43/12 → 11/3 | s:sd ]", + "[ 11/3 → 15/4 | s:sd ]", + "[ 15/4 → 23/6 | s:hh ]", + "[ 23/6 → 47/12 | s:hh ]", + "[ 47/12 → 4/1 | s:hh ]", ] `; @@ -11481,9 +11602,9 @@ exports[`runs examples > example "wchooseCycles" example index 2 1`] = ` "[ 1/4 → 1/3 | s:hh ]", "[ 1/3 → 5/12 | s:hh ]", "[ 5/12 → 1/2 | s:hh ]", - "[ 1/2 → 7/12 | s:hh ]", - "[ 7/12 → 2/3 | s:hh ]", - "[ 2/3 → 3/4 | s:hh ]", + "[ 1/2 → 17/32 | s:bd ]", + "[ 19/32 → 5/8 | s:bd ]", + "[ 11/16 → 23/32 | s:bd ]", "[ 3/4 → 5/6 | s:hh ]", "[ 5/6 → 11/12 | s:hh ]", "[ 11/12 → 1/1 | s:hh ]", @@ -11493,9 +11614,9 @@ exports[`runs examples > example "wchooseCycles" example index 2 1`] = ` "[ 5/4 → 4/3 | s:hh ]", "[ 4/3 → 17/12 | s:hh ]", "[ 17/12 → 3/2 | s:hh ]", - "[ 3/2 → 49/32 | s:bd ]", - "[ 51/32 → 13/8 | s:bd ]", - "[ 27/16 → 55/32 | 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:hh ]", "[ 11/6 → 23/12 | s:hh ]", "[ 23/12 → 2/1 | s:hh ]", @@ -11505,9 +11626,9 @@ exports[`runs examples > example "wchooseCycles" example index 2 1`] = ` "[ 9/4 → 7/3 | s:hh ]", "[ 7/3 → 29/12 | s:hh ]", "[ 29/12 → 5/2 | s:hh ]", - "[ 5/2 → 81/32 | s:bd ]", - "[ 83/32 → 21/8 | s:bd ]", - "[ 43/16 → 87/32 | s:bd ]", + "[ 5/2 → 31/12 | s:hh ]", + "[ 31/12 → 8/3 | s:hh ]", + "[ 8/3 → 11/4 | s:hh ]", "[ 11/4 → 17/6 | s:hh ]", "[ 17/6 → 35/12 | s:hh ]", "[ 35/12 → 3/1 | s:hh ]", From b3e433291cb911542f5e3e3b48fb4d42222d5b7d Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 24 Aug 2025 21:15:47 -0500 Subject: [PATCH 012/194] Turn back on silent --- vitest.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vitest.config.mjs b/vitest.config.mjs index 392b1731..47f62477 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -7,7 +7,7 @@ export default defineConfig({ test: { reporters: 'verbose', isolate: false, - silent: false, + silent: true, exclude: [ '**/node_modules/**', '**/dist/**', From 9c8e39d83d4415cbdaab6960986fa2fe1ec2d569 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 27 Aug 2025 16:27:36 -0500 Subject: [PATCH 013/194] Add.. a lot of doc strings --- packages/core/controls.mjs | 269 +++++++++++++++++++++++++++++ packages/superdough/superdough.mjs | 2 +- 2 files changed, 270 insertions(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index ddbfe98e..a078c2b6 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1428,24 +1428,293 @@ export const { panorient } = registerControl('panorient'); // ['pitch2'], // ['pitch3'], // ['portamento'], +/** + * Selects which LFO number to use for modulation. Multiple LFOs + * can be applied using the ':' mininotation. There are an arbitrary number + * of LFOs available -- the number is only used to share LFOs across targets + * if desired (and to conserve processing power) + * + * @name lfoNum + * @param {number | Pattern} lfoNum Index of the LFO. + * setup: note("F2").sound("supersaw") + * .lpf(100) + * .lfoDepth(1000) + * .lfoRate(0.25) + * .lfoSynced(1) + * .lfoTarget("lpf") + * .lfoParam("frequency") + * .lfoNum(2) + * + * reuse: note("F3").sound("square").lpf(50) + * .lfoTarget("lpf") + * .lfoParam("frequency") + * .lfoNum(2) // uses the same LFO + */ export const { lfoNum } = registerControl('lfoNum'); + +/** + * Sets the target destination for the LFO modulation. Names are typically related + * to existing controls ("source", "lpf", "vibrato", etc.). You can try a value + * and if it fails, the console will print the available options. + * + * @name lfoTarget + * @param {string | Pattern} lfoTarget Target identifier for modulation. + * note("F2").sound("supersaw") + * .lpf(100) + * .lfoDepth(3000) + * .lfoRate(0.25) + * .lfoSynced(1) + * .lfoTarget("lpf") + * .lfoParam("frequency") + */ export const { lfoTarget } = registerControl('lfoTarget'); + +/** + * Chooses which parameter the LFO will modulate on the target. Parameter values + * are things like "frequency", "Q", "detune", etc. You can try a value + * and if it fails, the console will print the available options. + * + * @name lfoParam + * @param {string | Pattern} lfoParam Parameter name + * note("F2").sound("supersaw") + * .lpf(100) + * .lfoDepth(3000) + * .lfoRate(0.25) + * .lfoSynced(1) + * .lfoTarget("lpf") + * .lfoParam("frequency") + */ export const { lfoParam } = registerControl('lfoParam'); + +/** + * Controls the speed of the LFO. + * + * @name lfoRate + * @param {number | Pattern} lfoRate Frequency or tempo-relative value. + * note("F2").sound("supersaw") + * .lpf(100) + * .lfoTarget("lpf") + * .lfoParam("frequency") + * .lfoDepth(3000) + * .lfoRate(0.25) + */ export const { lfoRate } = registerControl('lfoRate'); + +/** + * Sets the modulation depth of the LFO. + * + * @name lfoDepth + * @param {number | Pattern} lfoDepth Modulation depth amount. + * note("F2").sound("supersaw") + * .lpf(100) + * .lfoTarget("lpf") + * .lfoParam("frequency") + * .lfoSynced(1) + * .lfoDepth(5000) + */ export const { lfoDepth } = registerControl('lfoDepth'); + +/** + * Applies a DC offset to the LFO signal. Normally the LFO varies from + * 0 to 1 (i.e. unipolar). By using an offset of -0.5, one can achieve + * a bipolar LFO. + * + * @name lfoDCOffset + * @param {number | Pattern} lfoDCOffset Offset amount. + * note("F2").sound("supersaw") + * .lpf(2000) + * .lfoTarget("lpf") + * .lfoParam("frequency") + * .lfoDepth(1000) + * .lfoRate(4) + * .lfoSynced(1) + * .lfoDCOffset(-0.5) + */ export const { lfoDCOffset } = registerControl('lfoDCOffset'); + +/** + * Selects the waveform shape of the LFO. Current options are + * triangle, square, sine, saw, ramp (corresponding to 0 through 4, + * respectively). + * + * @name lfoShape + * @param {number | Pattern} lfoShape Waveform type identifier. + * note("F2").sound("supersaw") + * .lpf(100) + * .lfoTarget("lpf") + * .lfoParam("frequency") + * .lfoDepth(1000) + * .lfoRate(4) + * .lfoSynced(1) + * .lfoShape(3) + */ export const { lfoShape } = registerControl('lfoShape'); + +/** + * Skews the LFO waveform. + * + * @name lfoSkew + * @param {number | Pattern} lfoSkew Skew amount (between 0 and 1). + * note("F2").sound("supersaw") + * .lpf(100) + * .lfoTarget("lpf") + * .lfoParam("frequency") + * .lfoDepth(1000) + * .lfoRate(4) + * .lfoSynced(1) + * .lfoSkew(0.75) + */ export const { lfoSkew } = registerControl('lfoSkew'); + +/** + * Adjusts the (exponential) curvature of the LFO waveform. + * + * @name lfoCurve + * @param {number | Pattern} lfoCurve Curve shaping amount. + * note("F2").sound("supersaw") + * .lpf(100) + * .lfoTarget("lpf") + * .lfoParam("frequency") + * .lfoDepth(1000) + * .lfoShape(3).lfoRate(4) + * .lfoSynced(1) + * .lfoCurve(0.95) + */ export const { lfoCurve } = registerControl('lfoCurve'); + +/** + * Determines whether the LFO is tempo-synced. + * + * @name lfoSynced + * @param {number | Pattern} lfoSynced Boolean flag (0 or 1). + * note("F2").sound("supersaw") + * .lpf(100) + * .lfoTarget("lpf") + * .lfoParam("frequency") + * .lfoDepth(1000) + * .lfoShape(3).lfoRate(2) + * .lfoSynced(1) + */ export const { lfoSynced } = registerControl('lfoSynced'); +/** + * Sets the target destination for the envelope modulation. Names are typically related + * to existing controls ("source", "lpf", "vibrato", etc.). You can try a value + * and if it fails, the console will print the available options. + * + * @name envTarget + * @param {number | Pattern} envTarget Target identifier for modulation. + * n(irand(12).seg(8)).scale("F#3:minor").room(1) + * .lpf(100) + * .envDepth("4800:400") + * .envTarget("source:lpf") + * .envParam("detune:frequency") + */ export const { envTarget } = registerControl('envTarget'); + +/** + * Chooses which parameter the LFO will modulate on the target. Parameter values + * are things like "frequency", "Q", "detune", etc. You can try a value + * and if it fails, the console will print the available options. + * + * @name envParam + * @param {number | Pattern} envParam Parameter index or identifier. + * n(irand(12).seg(8)).scale("F#3:minor").room(1) + * .lpf(100) + * .envDepth("4800:400") + * .envTarget("source:lpf") + * .envParam("detune:frequency") + */ export const { envParam } = registerControl('envParam'); + +/** + * Controls the attack time of the envelope. + * + * @name envAttack + * @param {number | Pattern} envAttack Duration of attack phase. + * n(irand(12).seg(8)).scale("F#3:minor").room(1) + * .lpf(500) + * .envDepth("4800:400") + * .envTarget("source:lpf") + * .envParam("detune:frequency") + * .envAttack(0.5) + */ export const { envAttack } = registerControl('envAttack'); + +/** + * Controls the decay time of the envelope. + * + * @name envDecay + * @param {number | Pattern} envDecay Duration of decay phase. + * n(irand(12).seg(8)).scale("F#3:minor").room(1) + * .lpf(100) + * .envDepth("4800:400") + * .envTarget("source:lpf") + * .envParam("detune:frequency") + * .envDecay("0.03:0.15").envCurve("exp:exp") + */ export const { envDecay } = registerControl('envDecay'); + +/** + * Sets the sustain level of the envelope. + * + * @name envSustain + * @param {number | Pattern} envSustain Sustain amplitude level. + * n(irand(12).seg(8)).scale("F#3:minor").room(1) + * .lpf(100) + * .envDepth("4800:400") + * .envTarget("source:lpf") + * .envParam("detune:frequency") + * .envDecay("0.03:0.15").envCurve("exp:exp") + * .envSustain(0.2) + */ export const { envSustain } = registerControl('envSustain'); + +/** + * Controls the release time of the envelope. + * + * @name envRelease + * @param {number | Pattern} envRelease Duration of release phase. + * @example + * n(irand(12).seg(8)).scale("F#3:minor").room(1) + * .lpf(100).release(3) + * .envDepth("4800:400") + * .envTarget("source:lpf") + * .envParam("detune:frequency") + * .envDecay("0.03:0.15").envCurve("exp:exp") + * .envSustain(0.5) + * .envRelease(3) + */ export const { envRelease } = registerControl('envRelease'); + +/** + * Selects the style of envelope: `exp` or `lin` (exponential or linear). + * + * @name envCurve + * @param {string | Pattern} envCurve Envelope curve style. + * @example + * n(irand(12).seg(8)).scale("F#3:minor").room(1) + * .lpf(100).release(2) + * .envTarget("source:lpf") + * .envParam("detune:frequency") + * .envDepth("500:4000") + * .envDecay("0.3:0.15") + * .envCurve("lin:exp") + */ export const { envCurve } = registerControl('envCurve'); + +/** + * Sets the modulation depth of the envelope. + * + * @name envDepth + * @param {number | Pattern} envDepth Modulation depth amount. + * @example + * n(irand(12).seg(8)).scale("F#3:minor").room(1) + * .lpf(100) + * .envTarget("source:lpf") + * .envParam("detune:frequency") + * .envDepth("4800:400") + */ export const { envDepth } = registerControl('envDepth'); // TODO: slide param for certain synths diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 97f3900d..91a97c17 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -548,7 +548,7 @@ function _getNodeParams(node) { * @param {Object} params - Dictionary of modulation parameters. * @returns {Object[]} - Array of parameter objects, one per parameter modulation */ -function _splitParams(params, countKeys) { +function _splitParams(params) { const num = ['num', 'target', 'parameter'] // names used to indicate individual parameter modulations .map((k) => [params[k] ?? 0].flat().length) .reduce((a, v) => Math.max(a, v), 1); From 862b88b6c405d95a39500b3915a70bb198179b17 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 4 Sep 2025 20:27:05 -0500 Subject: [PATCH 014/194] Some typos and cleanup --- packages/superdough/superdough.mjs | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 91a97c17..f38382e8 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -549,7 +549,7 @@ function _getNodeParams(node) { * @returns {Object[]} - Array of parameter objects, one per parameter modulation */ function _splitParams(params) { - const num = ['num', 'target', 'parameter'] // names used to indicate individual parameter modulations + const num = ['num', 'target', 'param'] // names used to indicate individual parameter modulations .map((k) => [params[k] ?? 0].flat().length) .reduce((a, v) => Math.max(a, v), 1); @@ -646,11 +646,6 @@ function _connectLFO(params) { lfoNode = getLfo(ac, filteredParams); lfos[num] = lfoNode; } - try { - lfoNode.disconnect(); - } catch { - // pass - } const targets = _getTargetParams(target, param); targets.forEach((target) => lfoNode.connect(target)); _setWorkletParamsAtTime(lfoNode.parameters, Object.entries(filteredParams), begin); @@ -975,29 +970,29 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) if (vowel !== undefined) { const vowelFilter = ac.createVowelFilter(vowel); - nodes['vowel'] = vowelFilter; + nodes['vowel'] = [vowelFilter]; chain.push(vowelFilter); } // effects if (coarse !== undefined) { const coarseNode = getWorklet(ac, 'coarse-processor', { coarse }); - nodes['coarse'] = coarseNode; + nodes['coarse'] = [coarseNode]; chain.push(coarseNode); } if (crush !== undefined) { const crushNode = getWorklet(ac, 'crush-processor', { crush }); - nodes['crush'] = crushNode; + nodes['crush'] = [crushNode]; chain.push(crushNode); } if (shape !== undefined) { const shapeNode = getWorklet(ac, 'shape-processor', { shape }); - nodes['shape'] = shapeNode; + nodes['shape'] = [shapeNode]; chain.push(shapeNode); } if (distort !== undefined) { const distortNode = getWorklet(ac, 'distort-processor', { distort }); - nodes['distort'] = distortNode; + nodes['distort'] = [distortNode]; chain.push(distortNode); } @@ -1039,7 +1034,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) compressorAttack, compressorRelease, ); - nodes['compressor'] = compressorNode; + nodes['compressor'] = [compressorNode]; chain.push(compressorNode); } @@ -1052,20 +1047,20 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // phaser if (phaser !== undefined && phaserdepth > 0) { const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep); - nodes['phaser'] = phaserFX; + nodes['phaser'] = [phaserFX]; chain.push(phaserFX); } // last gain const post = new GainNode(ac, { gain: postgain }); - nodes['post'] = post; + nodes['post'] = [post]; chain.push(post); // delay let delaySend; if (delay > 0 && delaytime > 0 && delayfeedback > 0) { const delayNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels); - nodes['delay'] = delayNode; + nodes['delay'] = [delayNode]; delaySend = effectSend(post, delayNode, delay); audioNodes.push(delaySend); } @@ -1084,7 +1079,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) roomIR = await loadBuffer(url, ac, ir, 0); } const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, orbitChannels); - nodes['room'] = reverbNode; + nodes['room'] = [reverbNode]; reverbSend = effectSend(post, reverbNode, room); audioNodes.push(reverbSend); } From 2ccbb0596d8c0ab0b226e276fc0c58dee8a3e606 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 4 Sep 2025 20:27:32 -0500 Subject: [PATCH 015/194] Codeformat --- 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 a078c2b6..d969d792 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1444,7 +1444,7 @@ export const { panorient } = registerControl('panorient'); * .lfoTarget("lpf") * .lfoParam("frequency") * .lfoNum(2) - * + * * reuse: note("F3").sound("square").lpf(50) * .lfoTarget("lpf") * .lfoParam("frequency") From 945b32533654e06591af4d4425586690e3759829 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Sep 2025 14:33:32 -0500 Subject: [PATCH 016/194] Codeformat, example tests --- test/__snapshots__/examples.test.mjs.snap | 111 ++++++++++++++++++++++ website/src/pages/learn/xen.mdx | 11 +-- 2 files changed, 114 insertions(+), 8 deletions(-) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index ecbc33ea..b602749d 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3395,6 +3395,117 @@ exports[`runs examples > example "end" example index 0 1`] = ` ] `; +exports[`runs examples > example "envCurve" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | note:F#3 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 1/8 → 1/4 | note:G#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 1/4 → 3/8 | note:C#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 3/8 → 1/2 | note:C#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 1/2 → 5/8 | note:B3 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 5/8 → 3/4 | note:G#3 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 3/4 → 7/8 | note:A3 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 7/8 → 1/1 | note:C#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 1/1 → 9/8 | note:E4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 9/8 → 5/4 | note:G#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 5/4 → 11/8 | note:G#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 11/8 → 3/2 | note:A3 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 3/2 → 13/8 | note:F#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 13/8 → 7/4 | note:D4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 7/4 → 15/8 | note:A3 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 15/8 → 2/1 | note:F#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 2/1 → 17/8 | note:C#5 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 17/8 → 9/4 | note:B4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 9/4 → 19/8 | note:C#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 19/8 → 5/2 | note:B4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 5/2 → 21/8 | note:D4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 21/8 → 11/4 | note:G#3 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 11/4 → 23/8 | note:F#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 23/8 → 3/1 | note:F#3 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 3/1 → 25/8 | note:A3 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 25/8 → 13/4 | note:C#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 13/4 → 27/8 | note:C#5 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 27/8 → 7/2 | note:C#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 7/2 → 29/8 | note:C#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 29/8 → 15/4 | note:C#5 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 15/4 → 31/8 | note:A4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", + "[ 31/8 → 4/1 | note:G#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", +] +`; + +exports[`runs examples > example "envDepth" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | note:F#3 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 1/8 → 1/4 | note:G#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 1/4 → 3/8 | note:C#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 3/8 → 1/2 | note:C#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 1/2 → 5/8 | note:B3 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 5/8 → 3/4 | note:G#3 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 3/4 → 7/8 | note:A3 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 7/8 → 1/1 | note:C#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 1/1 → 9/8 | note:E4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 9/8 → 5/4 | note:G#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 5/4 → 11/8 | note:G#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 11/8 → 3/2 | note:A3 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 3/2 → 13/8 | note:F#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 13/8 → 7/4 | note:D4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 7/4 → 15/8 | note:A3 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 15/8 → 2/1 | note:F#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 2/1 → 17/8 | note:C#5 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 17/8 → 9/4 | note:B4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 9/4 → 19/8 | note:C#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 19/8 → 5/2 | note:B4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 5/2 → 21/8 | note:D4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 21/8 → 11/4 | note:G#3 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 11/4 → 23/8 | note:F#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 23/8 → 3/1 | note:F#3 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 3/1 → 25/8 | note:A3 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 25/8 → 13/4 | note:C#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 13/4 → 27/8 | note:C#5 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 27/8 → 7/2 | note:C#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 7/2 → 29/8 | note:C#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 29/8 → 15/4 | note:C#5 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 15/4 → 31/8 | note:A4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", + "[ 31/8 → 4/1 | note:G#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", +] +`; + +exports[`runs examples > example "envRelease" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | note:F#3 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 1/8 → 1/4 | note:G#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 1/4 → 3/8 | note:C#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 3/8 → 1/2 | note:C#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 1/2 → 5/8 | note:B3 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 5/8 → 3/4 | note:G#3 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 3/4 → 7/8 | note:A3 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 7/8 → 1/1 | note:C#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 1/1 → 9/8 | note:E4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 9/8 → 5/4 | note:G#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 5/4 → 11/8 | note:G#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 11/8 → 3/2 | note:A3 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 3/2 → 13/8 | note:F#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 13/8 → 7/4 | note:D4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 7/4 → 15/8 | note:A3 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 15/8 → 2/1 | note:F#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 2/1 → 17/8 | note:C#5 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 17/8 → 9/4 | note:B4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 9/4 → 19/8 | note:C#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 19/8 → 5/2 | note:B4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 5/2 → 21/8 | note:D4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 21/8 → 11/4 | note:G#3 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 11/4 → 23/8 | note:F#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 23/8 → 3/1 | note:F#3 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 3/1 → 25/8 | note:A3 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 25/8 → 13/4 | note:C#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 13/4 → 27/8 | note:C#5 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 27/8 → 7/2 | note:C#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 7/2 → 29/8 | note:C#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 29/8 → 15/4 | note:C#5 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 15/4 → 31/8 | note:A4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", + "[ 31/8 → 4/1 | note:G#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", +] +`; + exports[`runs examples > example "euclid" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:c3 ]", diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx index 32e2c0b4..b8b27ac7 100644 --- a/website/src/pages/learn/xen.mdx +++ b/website/src/pages/learn/xen.mdx @@ -16,10 +16,7 @@ These functions allow the use of scales other than your typical chromatic 12 bas Here's an example of how to configure a basic hexany scale: - + Try other scales like `hexany1`, `iraq`, `gumbeng`, `gunkali`, or `tranh3` @@ -67,6 +64,7 @@ Another helpful trick when exploring new tunings is to strum them. Many have a much more enchanting sound that was chosen over many generations of musicians for being strummed. Take the `sanza` tuning: + + This quality is often due to how the tunings were formed with instruments that were played differently than a piano. As such, some tunings are much better strummed, with the subtle clash of the detuned notes actually making the sound much more magical: From 23aa8ef5090df4b86089333d89df612dc58c4d0f Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Sep 2025 14:49:36 -0500 Subject: [PATCH 017/194] Add ability for LFOs to target.. other lfos :devil emoji: --- packages/superdough/superdough.mjs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 364681c2..7ef3ee94 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -636,7 +636,7 @@ function _setWorkletParamsAtTime(audioParams, params, time) { } let lfos = {}; -function _connectLFO(params) { +function _connectLFO(params, nodeTracker) { const { frequency = 1, synced = 0, @@ -653,10 +653,12 @@ function _connectLFO(params) { const ac = getAudioContext(); lfoNode = getLfo(ac, filteredParams); lfos[num] = lfoNode; + nodeTracker[`lfo${num}`] = [lfoNode]; } const targets = _getTargetParams(target, param); targets.forEach((target) => lfoNode.connect(target)); _setWorkletParamsAtTime(lfoNode.parameters, Object.entries(filteredParams), begin); + return lfoNode; } function _connectEnvelope(params) { @@ -671,15 +673,16 @@ function _connectEnvelope(params) { }); } -function connectModulators(params, modulatorType) { +function connectModulators(params, modulatorType, nodeTracker) { // We break down params specifying multiple modulators into a set of parameters for // a single one const individualParams = _splitParams(params); if (modulatorType === 'lfo') { - individualParams.forEach(_connectLFO); + individualParams.map((p) => _connectLFO(p, nodeTracker)); } else if (modulatorType === 'envelope') { individualParams.forEach(_connectEnvelope); } + return []; } let activeSoundSources = new Map(); @@ -1118,7 +1121,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) if (lfoTarget !== undefined && lfoParam !== undefined) { connectModulators( { - num: lfoNum, + num: lfoNum ?? 1, target: lfoTarget, param: lfoParam, frequency: lfoRate, @@ -1133,6 +1136,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) cps: cps, }, 'lfo', + nodes, ); } if (envTarget !== undefined && envParam !== undefined) { From 1be9f40574aa213d3ef501024e5a0e18cb04fef9 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 18 Oct 2025 10:42:35 +0100 Subject: [PATCH 018/194] 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 00000000..66fc1dd5 --- /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 019/194] 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 e4daf445..9260e367 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 020/194] 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 66fc1dd5..e16cfbe4 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 0fbde61b2dd4bb2f2c3bdf2b451d05bfa73264e4 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 19 Oct 2025 14:35:23 +0300 Subject: [PATCH 021/194] WIP non-realtime exporting --- packages/codemirror/codemirror.mjs | 10 +- packages/core/cyclist.mjs | 13 ++ packages/core/repl.mjs | 31 ++-- packages/superdough/audioContext.mjs | 5 + packages/superdough/feedbackdelay.mjs | 2 +- packages/superdough/reverb.mjs | 4 +- packages/superdough/superdough.mjs | 12 +- packages/superdough/vowel.mjs | 2 +- packages/webaudio/webaudio.mjs | 151 +++++++++++++++++- website/src/repl/components/Header.jsx | 12 +- .../src/repl/components/panel/SettingsTab.jsx | 2 +- website/src/repl/useReplContext.jsx | 4 + 12 files changed, 217 insertions(+), 31 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 4dc23996..645a8a19 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -42,9 +42,9 @@ const extensions = { isMultiCursorEnabled: (on) => on ? [ - EditorState.allowMultipleSelections.of(true), - EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), - ] + EditorState.allowMultipleSelections.of(true), + EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), + ] : [], }; const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); @@ -268,6 +268,10 @@ export class StrudelMirror { this.flash(); await this.repl.evaluate(this.code); } + async exportAudio(begin, end) { + // await this.repl.evaluate(this.code, false) + this.repl.exportAudio(begin, end); + } async stop() { this.repl.scheduler.stop(); } diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 59e410c5..b7ab7dde 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -6,6 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import createClock from './zyklus.mjs'; import { errorLogger, logger } from './logger.mjs'; +import { loadBuffer, renderPatternAudio, setAudioContext, superdough } from '@strudel/webaudio'; export class Cyclist { constructor({ @@ -98,6 +99,7 @@ export class Cyclist { this.started = v; this.onToggle?.(v); } + async start() { await this.beforeStart?.(); this.num_ticks_since_cps_change = 0; @@ -109,6 +111,17 @@ export class Cyclist { this.clock.start(); this.setStarted(true); } + + async exportAudio(begin, end) { + if (!this.pattern) { + throw new Error('Scheduler: no pattern set! call .setPattern first.'); + } + logger('[cyclist] exporting'); + // this.clock.start(); + // this.setStarted(true); + await renderPatternAudio(this.pattern, this.cps, begin, end) + } + pause() { logger('[cyclist] pause'); this.clock.pause(); diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 171697eb..d0cb1e19 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -91,6 +91,7 @@ export function repl({ const stop = () => scheduler.stop(); const start = () => scheduler.start(); + const exportAudio = (begin, end) => scheduler.exportAudio(begin, end); const pause = () => scheduler.pause(); const toggle = () => scheduler.toggle(); const setCps = (cps) => { @@ -257,23 +258,23 @@ export function repl({ } }; const setCode = (code) => updateState({ code }); - return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state }; + return { scheduler, evaluate, start, exportAudio, stop, pause, setCps, setPattern, setCode, toggle, state }; } export const getTrigger = ({ getTime, defaultOutput }) => - async (hap, deadline, duration, cps, t) => { - // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) - // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 - try { - if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t); + async (hap, deadline, duration, cps, t) => { + // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) + // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 + try { + if (!hap.context.onTrigger || !hap.context.dominantTrigger) { + await defaultOutput(hap, deadline, duration, cps, t); + } + if (hap.context.onTrigger) { + // call signature of output / onTrigger is different... + await hap.context.onTrigger(hap, getTime(), cps, t); + } + } catch (err) { + errorLogger(err, 'getTrigger'); } - if (hap.context.onTrigger) { - // call signature of output / onTrigger is different... - await hap.context.onTrigger(hap, getTime(), cps, t); - } - } catch (err) { - errorLogger(err, 'getTrigger'); - } - }; + }; diff --git a/packages/superdough/audioContext.mjs b/packages/superdough/audioContext.mjs index 71e01d57..30fbd9fd 100644 --- a/packages/superdough/audioContext.mjs +++ b/packages/superdough/audioContext.mjs @@ -5,6 +5,11 @@ export const setDefaultAudioContext = () => { return audioContext; }; +export const setAudioContext = (context) => { + audioContext = context + return audioContext; +}; + export const getAudioContext = () => { if (!audioContext) { return setDefaultAudioContext(); diff --git a/packages/superdough/feedbackdelay.mjs b/packages/superdough/feedbackdelay.mjs index c182d655..b8269bc0 100644 --- a/packages/superdough/feedbackdelay.mjs +++ b/packages/superdough/feedbackdelay.mjs @@ -25,7 +25,7 @@ if (typeof DelayNode !== 'undefined') { } } - AudioContext.prototype.createFeedbackDelay = function (wet, time, feedback) { + BaseAudioContext.prototype.createFeedbackDelay = function (wet, time, feedback) { return new FeedbackDelayNode(this, wet, time, feedback); }; } diff --git a/packages/superdough/reverb.mjs b/packages/superdough/reverb.mjs index 2960b597..c8b6eb3e 100644 --- a/packages/superdough/reverb.mjs +++ b/packages/superdough/reverb.mjs @@ -2,7 +2,7 @@ import reverbGen from './reverbGen.mjs'; import { clamp } from './util.mjs'; if (typeof AudioContext !== 'undefined') { - AudioContext.prototype.adjustLength = function (duration, buffer, speed = 1, offsetAmount = 0) { + BaseAudioContext.prototype.adjustLength = function (duration, buffer, speed = 1, offsetAmount = 0) { const sampleOffset = Math.floor(clamp(offsetAmount, 0, 1) * buffer.length); const newLength = buffer.sampleRate * duration; const newBuffer = this.createBuffer(buffer.numberOfChannels, buffer.length, buffer.sampleRate); @@ -23,7 +23,7 @@ if (typeof AudioContext !== 'undefined') { return newBuffer; }; - AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir, irspeed, irbegin) { + BaseAudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir, irspeed, irbegin) { const convolver = this.createConvolver(); convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir, irspeed, irbegin) => { convolver.duration = d; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index a37504b1..80205a29 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -13,7 +13,7 @@ import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorkle import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; -import { getAudioContext } from './audioContext.mjs'; +import { getAudioContext, setAudioContext } from './audioContext.mjs'; import { SuperdoughAudioController } from './superdoughoutput.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; @@ -367,6 +367,7 @@ function mapChannelNumbers(channels) { } export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { + controller = null; // new: t is always expected to be the absolute target onset time const ac = getAudioContext(); const audioController = getSuperdoughAudioController(); @@ -387,14 +388,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // duration is passed as value too.. value.duration = hapDuration; // calculate absolute time - - if (t < ac.currentTime) { + if (t < ac.currentTime && !(ac instanceof OfflineAudioContext)) { console.warn( `[superdough]: cannot schedule sounds in the past (target: ${t.toFixed(2)}, now: ${ac.currentTime.toFixed(2)})`, ); return; - } - // destructure + } // FIXME: fix + // destructure let { tremolo, tremolosync, @@ -736,4 +736,4 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) export const superdoughTrigger = (t, hap, ct, cps) => { superdough(hap, t - ct, hap.duration / cps, cps); -}; +}; \ No newline at end of file diff --git a/packages/superdough/vowel.mjs b/packages/superdough/vowel.mjs index 3f30aef1..025677fc 100644 --- a/packages/superdough/vowel.mjs +++ b/packages/superdough/vowel.mjs @@ -63,7 +63,7 @@ if (typeof GainNode !== 'undefined') { } } - AudioContext.prototype.createVowelFilter = function (letter) { + BaseAudioContext.prototype.createVowelFilter = function (letter) { return new VowelNode(this, letter); }; } diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 383e87f8..46bef88d 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import * as strudel from '@strudel/core'; -import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough'; +import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet, setAudioContext, getSampleBufferSource, loadBuffer, getSampleInfo, getSound } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; @@ -26,6 +26,60 @@ export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => { return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf()); }; +export async function renderPatternAudio(pattern, cps, begin, end) { + let audioContext = new OfflineAudioContext(2, (end - begin) / cps * 48000, 48000); + setAudioContext(audioContext) + logger('[webaudio] start rendering'); + console.log(audioContext) + + let haps = pattern.queryArc(begin, end, { _cps: cps }) + Promise.all(haps.map(async h => { + let s; + if (h.value.s) { + if (h.value.bank) { + s = `${h.value.bank}_${h.value.s}`; + } + else { + s = h.value.s + } + } + let bank = getSound(s).data.samples + if (bank) { + let { url: sampleUrl, label } = getSampleInfo(h.value, bank); + await loadBuffer(sampleUrl, audioContext, label) + } + })) + haps.forEach(async (hap) => { + if (hap.hasOnset()) { + hap.ensureObjectValue(); + await superdough(hap.value, hap.whole.begin.valueOf() / cps, hap.duration, cps, hap.whole?.begin.valueOf()); + } + }) + + let context = getAudioContext() + context.startRendering().then((renderedBuffer) => { + // console.log(renderedBuffer) + const wavBuffer = audioBufferToWav(renderedBuffer); + const blob = new Blob([wavBuffer], { type: 'audio/wav' }); + let downloadName = '' + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + // remove leading dash if they want an empty download name + if (downloadName == '') { + downloadName = `${new Date().toISOString()}.wav`; + } else { + downloadName = downloadName + `-${new Date().toISOString()}.wav`; + } + a.download = `${downloadName}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + setAudioContext(null) + }); +} + export function webaudioRepl(options = {}) { options = { getTime: () => getAudioContext().currentTime, @@ -38,3 +92,98 @@ export function webaudioRepl(options = {}) { Pattern.prototype.dough = function () { return this.onTrigger(doughTrigger, 1); }; + + +function audioBufferToWav(buffer, opt) { + opt = opt || {} + + var numChannels = buffer.numberOfChannels + var sampleRate = buffer.sampleRate + var format = opt.float32 ? 3 : 1 + var bitDepth = format === 3 ? 32 : 16 + + var result + if (numChannels === 2) { + result = interleave(buffer.getChannelData(0), buffer.getChannelData(1)) + } else { + result = buffer.getChannelData(0) + } + + return encodeWAV(result, format, sampleRate, numChannels, bitDepth) +} + +function encodeWAV(samples, format, sampleRate, numChannels, bitDepth) { + var bytesPerSample = bitDepth / 8 + var blockAlign = numChannels * bytesPerSample + + var buffer = new ArrayBuffer(44 + samples.length * bytesPerSample) + var view = new DataView(buffer) + + /* RIFF identifier */ + writeString(view, 0, 'RIFF') + /* RIFF chunk length */ + view.setUint32(4, 36 + samples.length * bytesPerSample, true) + /* RIFF type */ + writeString(view, 8, 'WAVE') + /* format chunk identifier */ + writeString(view, 12, 'fmt ') + /* format chunk length */ + view.setUint32(16, 16, true) + /* sample format (raw) */ + view.setUint16(20, format, true) + /* channel count */ + view.setUint16(22, numChannels, true) + /* sample rate */ + view.setUint32(24, sampleRate, true) + /* byte rate (sample rate * block align) */ + view.setUint32(28, sampleRate * blockAlign, true) + /* block align (channel count * bytes per sample) */ + view.setUint16(32, blockAlign, true) + /* bits per sample */ + view.setUint16(34, bitDepth, true) + /* data chunk identifier */ + writeString(view, 36, 'data') + /* data chunk length */ + view.setUint32(40, samples.length * bytesPerSample, true) + if (format === 1) { // Raw PCM + floatTo16BitPCM(view, 44, samples) + } else { + writeFloat32(view, 44, samples) + } + + return buffer +} + +function interleave(inputL, inputR) { + var length = inputL.length + inputR.length + var result = new Float32Array(length) + + var index = 0 + var inputIndex = 0 + + while (index < length) { + result[index++] = inputL[inputIndex] + result[index++] = inputR[inputIndex] + inputIndex++ + } + return result +} + +function writeFloat32(output, offset, input) { + for (var i = 0; i < input.length; i++, offset += 4) { + output.setFloat32(offset, input[i], true) + } +} + +function floatTo16BitPCM(output, offset, input) { + for (var i = 0; i < input.length; i++, offset += 2) { + var s = Math.max(-1, Math.min(1, input[i])) + output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true) + } +} + +function writeString(view, offset, string) { + for (var i = 0; i < string.length; i++) { + view.setUint8(offset + i, string.charCodeAt(i)) + } +} \ No newline at end of file diff --git a/website/src/repl/components/Header.jsx b/website/src/repl/components/Header.jsx index ca4e1ecf..656aa786 100644 --- a/website/src/repl/components/Header.jsx +++ b/website/src/repl/components/Header.jsx @@ -8,7 +8,7 @@ const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; export function Header({ context, embedded = false }) { - const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare } = + const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare, handleExport } = context; const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location); const { isZen, isButtonRowHidden, isCSSAnimationDisabled, fontFamily } = useSettings(); @@ -93,6 +93,16 @@ export function Header({ context, embedded = false }) { > {!isEmbedded && update} + {/* !isEmbedded && ( + + +
+

Start cycle

+ { + let v = parseInt(e.target.value); + console.log(v) + v = isNaN(v) ? 0 : Math.max(1, v); + setStartCycle(v); + }} + onChange={v => { + v = parseInt(v) + setStartCycle(v); + }} + type="number" + placeholder="" + value={startCycle ?? ''} + /> +
+
+

End cycle

+ { + let v = parseInt(e.target.value); + v = isNaN(v) ? Math.max(startCycle + 1, parseInt(v)) : v; + setEndCycle(v); + }} + onChange={(v) => { + v = parseInt(v) + setEndCycle(v); + }} + type="number" + placeholder="" + value={endCycle ?? ''} + /> +
+
+

Maximum polyphony

+ { + let v = parseInt(e.target.value); + v = isNaN(v) ? Math.max(1, parseInt(v)) : v; + setMaxPolyphony(v); + }} + onChange={(v) => { + v = Math.max(1, parseInt(v)); + setMaxPolyphony(v); + }} + type="number" + placeholder="" + value={maxPolyphony ?? ''} + /> +
+
+ { + const val = cbEvent.target.checked; + setMultiChannelOrbits(val) + }} + value={multiChannelOrbits} + /> +
+ +
+ + ); +} diff --git a/website/src/repl/components/Header.jsx b/website/src/repl/components/Header.jsx index 91e942d3..d5d8b5f7 100644 --- a/website/src/repl/components/Header.jsx +++ b/website/src/repl/components/Header.jsx @@ -3,6 +3,7 @@ import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon'; import cx from '@src/cx.mjs'; import { useSettings, setIsZen } from '../../settings.mjs'; import '../Repl.css'; +import ExportModal from './ExportModal'; const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; @@ -93,18 +94,7 @@ export function Header({ context, embedded = false }) { > {!isEmbedded && update} - + {/* !isEmbedded && ( - - -
-

Start cycle

- { - let v = parseInt(e.target.value); - console.log(v) - v = isNaN(v) ? 0 : Math.max(0, v); - setStartCycle(v); - }} - onChange={v => { - v = parseInt(v) - setStartCycle(v); - }} - type="number" - placeholder="" - value={startCycle ?? ''} - /> -
-
-

End cycle

- { - let v = parseInt(e.target.value); - v = isNaN(v) ? Math.max(startCycle + 1, parseInt(v)) : v; - setEndCycle(v); - }} - onChange={(v) => { - v = parseInt(v) - setEndCycle(v); - }} - type="number" - placeholder="" - value={endCycle ?? ''} - /> -
-
-

Maximum polyphony

- { - let v = parseInt(e.target.value); - v = isNaN(v) ? Math.max(1, parseInt(v)) : v; - setMaxPolyphony(v); - }} - onChange={(v) => { - v = Math.max(1, parseInt(v)); - setMaxPolyphony(v); - }} - type="number" - placeholder="" - value={maxPolyphony ?? ''} - /> -
-
- { - const val = cbEvent.target.checked; - setMultiChannelOrbits(val) - }} - value={multiChannelOrbits} - /> -
- + }} + className={cx( + 'absolute text-foreground max-h-8 min-h-8 max-w-8 min-w-8 items-center p-1.5 right-2 top-2 z-50', + exporting && "opacity-50" + )} + aria-label="Close Modal" + > + + +
+ + { + setDownloadName(e.target.value) + }} + onChange={v => { + setDownloadName(v) + }} + disabled={exporting} + placeholder="Leave empty to use current date" + className={cx("placeholder:opacity-50", exporting && "opacity-50 border-opacity-50")} + value={downloadName ?? ''} + /> + +
+ + { + let v = parseInt(e.target.value); + v = isNaN(v) ? 0 : Math.max(0, v); + setStartCycle(v); + }} + onChange={v => { + v = parseInt(v) + setStartCycle(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && "opacity-50 border-opacity-50")} + value={startCycle ?? ''} + /> + + + { + let v = parseInt(e.target.value); + v = isNaN(v) ? Math.max(startCycle + 1, parseInt(v)) : v; + setEndCycle(v); + }} + onChange={(v) => { + v = parseInt(v) + setEndCycle(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && "opacity-50 border-opacity-50")} + value={endCycle ?? ''} + /> + +
+
+ + { + let v = parseInt(e.target.value); + v = isNaN(v) ? 1 : Math.max(1, v); + setSampleRate(v); + }} + onChange={v => { + v = parseInt(v) + setSampleRate(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && "opacity-50 border-opacity-50")} + value={sampleRate ?? ''} + /> + + + { + let v = parseInt(e.target.value); + v = isNaN(v) ? Math.max(1, parseInt(v)) : v; + setMaxPolyphony(v); + }} + onChange={(v) => { + v = Math.max(1, parseInt(v)); + setMaxPolyphony(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && "opacity-50 border-opacity-50")} + value={maxPolyphony ?? ''} + /> + +
+
+ { + const val = cbEvent.target.checked; + setMultiChannelOrbits(val) + }} + disabled={exporting} + value={multiChannelOrbits} + /> +
+ +
); diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index b9f1c8c8..0e89fb68 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -203,8 +203,8 @@ export function useReplContext() { const handleEvaluate = () => { editorRef.current.evaluate(); }; - const handleExport = async (begin, end) => { - await editorRef.current.exportAudio(begin, end); + const handleExport = async (begin, end, sampleRate, downloadName = undefined) => { + await editorRef.current.exportAudio(begin, end, sampleRate, downloadName); }; const handleShuffle = async () => { const patternData = await getRandomTune(); From 428a4b3dd742087b5fa50cc0c15ebd380c5d573b Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 20:21:23 +0300 Subject: [PATCH 028/194] Revert workletsUrl to use ?audioworklet --- 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 ed5cd964..2f9eca3e 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -8,7 +8,7 @@ import './feedbackdelay.mjs'; import './reverb.mjs'; import './vowel.mjs'; import { nanFallback, _mod, cycleToSeconds } from './util.mjs'; -import workletsUrl from './worklets.mjs?url'; +import workletsUrl from './worklets.mjs?audioworklet'; import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; From 4bdaf577c08cab55afdc4440ab737054578f8aa7 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 20:22:16 +0300 Subject: [PATCH 029/194] Format --- packages/codemirror/codemirror.mjs | 9 +- packages/core/cyclist.mjs | 2 +- packages/core/repl.mjs | 31 +- packages/superdough/audioContext.mjs | 6 +- packages/superdough/helpers.mjs | 2 +- packages/superdough/superdough.mjs | 12 +- packages/webaudio/webaudio.mjs | 198 +++++----- website/src/repl/components/ExportModal.jsx | 405 ++++++++++---------- website/src/repl/components/Header.jsx | 13 +- 9 files changed, 360 insertions(+), 318 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 7cbb925d..84cbfa96 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -42,9 +42,9 @@ const extensions = { isMultiCursorEnabled: (on) => on ? [ - EditorState.allowMultipleSelections.of(true), - EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), - ] + EditorState.allowMultipleSelections.of(true), + EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), + ] : [], }; const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); @@ -269,10 +269,9 @@ export class StrudelMirror { await this.repl.evaluate(this.code); } async exportAudio(begin, end, sampleRate, downloadName = undefined) { - await this.repl.evaluate(this.code, false) + await this.repl.evaluate(this.code, false); await this.repl.exportAudio(begin, end, sampleRate, downloadName); this.repl.scheduler.stop(); - } async stop() { this.repl.scheduler.stop(); diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 167f1882..185d5588 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -117,7 +117,7 @@ export class Cyclist { throw new Error('Scheduler: no pattern set! call .setPattern first.'); } logger('[cyclist] exporting'); - await renderPatternAudio(this.pattern, this.cps, begin, end, sampleRate, downloadName) + await renderPatternAudio(this.pattern, this.cps, begin, end, sampleRate, downloadName); } pause() { diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 970c15a6..4a35b932 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -91,7 +91,8 @@ export function repl({ const stop = () => scheduler.stop(); const start = () => scheduler.start(); - const exportAudio = async (begin, end, sampleRate, downloadName = undefined) => await scheduler.exportAudio(begin, end, sampleRate, downloadName); + const exportAudio = async (begin, end, sampleRate, downloadName = undefined) => + await scheduler.exportAudio(begin, end, sampleRate, downloadName); const pause = () => scheduler.pause(); const toggle = () => scheduler.toggle(); const setCps = (cps) => { @@ -263,18 +264,18 @@ export function repl({ export const getTrigger = ({ getTime, defaultOutput }) => - async (hap, deadline, duration, cps, t) => { - // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) - // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 - try { - if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t); - } - if (hap.context.onTrigger) { - // call signature of output / onTrigger is different... - await hap.context.onTrigger(hap, getTime(), cps, t); - } - } catch (err) { - errorLogger(err, 'getTrigger'); + async (hap, deadline, duration, cps, t) => { + // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) + // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 + try { + if (!hap.context.onTrigger || !hap.context.dominantTrigger) { + await defaultOutput(hap, deadline, duration, cps, t); } - }; + if (hap.context.onTrigger) { + // call signature of output / onTrigger is different... + await hap.context.onTrigger(hap, getTime(), cps, t); + } + } catch (err) { + errorLogger(err, 'getTrigger'); + } + }; diff --git a/packages/superdough/audioContext.mjs b/packages/superdough/audioContext.mjs index ea60722b..9a5913d8 100644 --- a/packages/superdough/audioContext.mjs +++ b/packages/superdough/audioContext.mjs @@ -1,15 +1,15 @@ -import { analysers, resetGlobalEffects } from "./superdough.mjs"; +import { analysers, resetGlobalEffects } from './superdough.mjs'; let audioContext; export const setDefaultAudioContext = () => { audioContext = new AudioContext(); - resetGlobalEffects() + resetGlobalEffects(); return audioContext; }; export const setAudioContext = (context) => { - audioContext = context + audioContext = context; return audioContext; }; diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index cf65c390..a471da9d 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -348,7 +348,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 2f9eca3e..118a152b 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -212,7 +212,9 @@ export function loadWorklets() { if (!workletsLoading) { const audioCtx = getAudioContext(); const allWorkletURLs = externalWorklets.concat([workletsUrl]); - workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL))).then(() => workletsLoading = undefined); + workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL))).then( + () => (workletsLoading = undefined), + ); } return workletsLoading; @@ -250,7 +252,7 @@ export async function initAudio(options = {}) { logger('[superdough] failed to set audio interface', 'warning'); } } - if (!audioCtx instanceof OfflineAudioContext) { + if ((!audioCtx) instanceof OfflineAudioContext) { await audioCtx.resume(); } if (disableWorklets) { @@ -288,7 +290,7 @@ function getSuperdoughAudioController() { } export function setSuperdoughAudioController(newController) { - controller = newController + controller = newController; return controller; } @@ -401,7 +403,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) ); return; } // FIXME: fix - // destructure + // destructure let { tremolo, tremolosync, @@ -743,4 +745,4 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) export const superdoughTrigger = (t, hap, ct, cps) => { superdough(hap, t - ct, hap.duration / cps, cps); -}; \ No newline at end of file +}; diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 27718bd7..43c01146 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -5,7 +5,23 @@ This program is free software: you can redistribute it and/or modify it under th */ import * as strudel from '@strudel/core'; -import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet, setAudioContext, getSampleBufferSource, loadBuffer, getSampleInfo, getSound, initAudio, setSuperdoughAudioController, loadWorklets, setMaxPolyphony, setMultiChannelOrbits } from 'superdough'; +import { + superdough, + getAudioContext, + setLogger, + doughTrigger, + registerWorklet, + setAudioContext, + getSampleBufferSource, + loadBuffer, + getSampleInfo, + getSound, + initAudio, + setSuperdoughAudioController, + loadWorklets, + setMaxPolyphony, + setMultiChannelOrbits, +} from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; import { SuperdoughAudioController } from 'superdough/superdoughoutput.mjs'; @@ -28,59 +44,63 @@ export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => { }; export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, downloadName = undefined) { - const oldAudioCtx = getAudioContext() - await oldAudioCtx.close() - let audioContext = new OfflineAudioContext(2, (end - begin) / cps * sampleRate, sampleRate); - setAudioContext(audioContext) - let context = getAudioContext() - setSuperdoughAudioController(new SuperdoughAudioController(context)) - setMaxPolyphony(1024) - setMultiChannelOrbits(true) - await loadWorklets() + const oldAudioCtx = getAudioContext(); + await oldAudioCtx.close(); + let audioContext = new OfflineAudioContext(2, ((end - begin) / cps) * sampleRate, sampleRate); + setAudioContext(audioContext); + let context = getAudioContext(); + setSuperdoughAudioController(new SuperdoughAudioController(context)); + setMaxPolyphony(1024); + setMultiChannelOrbits(true); + await loadWorklets(); logger('[webaudio] start rendering'); - let haps = pattern.queryArc(begin, end, { _cps: cps }) - Promise.all(haps.map(async h => { - let s; - if (h.value.s) { - if (h.value.bank) { - s = `${h.value.bank}_${h.value.s}`; + let haps = pattern.queryArc(begin, end, { _cps: cps }); + Promise.all( + haps.map(async (h) => { + let s; + if (h.value.s) { + if (h.value.bank) { + s = `${h.value.bank}_${h.value.s}`; + } else { + s = h.value.s; + } + let bank = getSound(s).data.samples; + if (bank) { + let { url: sampleUrl, label } = getSampleInfo(h.value, bank); + await loadBuffer(sampleUrl, context, label); + } } - else { - s = h.value.s - } - let bank = getSound(s).data.samples - if (bank) { - let { url: sampleUrl, label } = getSampleInfo(h.value, bank); - await loadBuffer(sampleUrl, context, label) - } - } - })) + }), + ); haps.forEach(async (hap) => { if (hap.hasOnset()) { hap.ensureObjectValue(); await superdough(hap.value, hap.whole.begin.valueOf() / cps, hap.duration, cps, hap.whole?.begin.valueOf()); } - }) - - return context.startRendering().then((renderedBuffer) => { - console.log(renderedBuffer) - const wavBuffer = audioBufferToWav(renderedBuffer); - const blob = new Blob([wavBuffer], { type: 'audio/wav' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - downloadName = downloadName ? `${downloadName}.wav` : `${new Date().toISOString()}.wav`; - a.download = `${downloadName}`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }).finally(async () => { - setAudioContext(null) - setSuperdoughAudioController(null) - await initAudio() }); + + return context + .startRendering() + .then((renderedBuffer) => { + console.log(renderedBuffer); + const wavBuffer = audioBufferToWav(renderedBuffer); + const blob = new Blob([wavBuffer], { type: 'audio/wav' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + downloadName = downloadName ? `${downloadName}.wav` : `${new Date().toISOString()}.wav`; + a.download = `${downloadName}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }) + .finally(async () => { + setAudioContext(null); + setSuperdoughAudioController(null); + await initAudio(); + }); } export function webaudioRepl(options = {}) { @@ -96,97 +116,97 @@ Pattern.prototype.dough = function () { return this.onTrigger(doughTrigger, 1); }; - function audioBufferToWav(buffer, opt) { - opt = opt || {} + opt = opt || {}; - var numChannels = buffer.numberOfChannels - var sampleRate = buffer.sampleRate - var format = opt.float32 ? 3 : 1 - var bitDepth = format === 3 ? 32 : 16 + var numChannels = buffer.numberOfChannels; + var sampleRate = buffer.sampleRate; + var format = opt.float32 ? 3 : 1; + var bitDepth = format === 3 ? 32 : 16; - var result + var result; if (numChannels === 2) { - result = interleave(buffer.getChannelData(0), buffer.getChannelData(1)) + result = interleave(buffer.getChannelData(0), buffer.getChannelData(1)); } else { - result = buffer.getChannelData(0) + result = buffer.getChannelData(0); } - return encodeWAV(result, format, sampleRate, numChannels, bitDepth) + return encodeWAV(result, format, sampleRate, numChannels, bitDepth); } function encodeWAV(samples, format, sampleRate, numChannels, bitDepth) { - var bytesPerSample = bitDepth / 8 - var blockAlign = numChannels * bytesPerSample + var bytesPerSample = bitDepth / 8; + var blockAlign = numChannels * bytesPerSample; - var buffer = new ArrayBuffer(44 + samples.length * bytesPerSample) - var view = new DataView(buffer) + var buffer = new ArrayBuffer(44 + samples.length * bytesPerSample); + var view = new DataView(buffer); /* RIFF identifier */ - writeString(view, 0, 'RIFF') + writeString(view, 0, 'RIFF'); /* RIFF chunk length */ - view.setUint32(4, 36 + samples.length * bytesPerSample, true) + view.setUint32(4, 36 + samples.length * bytesPerSample, true); /* RIFF type */ - writeString(view, 8, 'WAVE') + writeString(view, 8, 'WAVE'); /* format chunk identifier */ - writeString(view, 12, 'fmt ') + writeString(view, 12, 'fmt '); /* format chunk length */ - view.setUint32(16, 16, true) + view.setUint32(16, 16, true); /* sample format (raw) */ - view.setUint16(20, format, true) + view.setUint16(20, format, true); /* channel count */ - view.setUint16(22, numChannels, true) + view.setUint16(22, numChannels, true); /* sample rate */ - view.setUint32(24, sampleRate, true) + view.setUint32(24, sampleRate, true); /* byte rate (sample rate * block align) */ - view.setUint32(28, sampleRate * blockAlign, true) + view.setUint32(28, sampleRate * blockAlign, true); /* block align (channel count * bytes per sample) */ - view.setUint16(32, blockAlign, true) + view.setUint16(32, blockAlign, true); /* bits per sample */ - view.setUint16(34, bitDepth, true) + view.setUint16(34, bitDepth, true); /* data chunk identifier */ - writeString(view, 36, 'data') + writeString(view, 36, 'data'); /* data chunk length */ - view.setUint32(40, samples.length * bytesPerSample, true) - if (format === 1) { // Raw PCM - floatTo16BitPCM(view, 44, samples) + view.setUint32(40, samples.length * bytesPerSample, true); + if (format === 1) { + // Raw PCM + floatTo16BitPCM(view, 44, samples); } else { - writeFloat32(view, 44, samples) + writeFloat32(view, 44, samples); } - return buffer + return buffer; } function interleave(inputL, inputR) { - var length = inputL.length + inputR.length - var result = new Float32Array(length) + var length = inputL.length + inputR.length; + var result = new Float32Array(length); - var index = 0 - var inputIndex = 0 + var index = 0; + var inputIndex = 0; while (index < length) { - result[index++] = inputL[inputIndex] - result[index++] = inputR[inputIndex] - inputIndex++ + result[index++] = inputL[inputIndex]; + result[index++] = inputR[inputIndex]; + inputIndex++; } - return result + return result; } function writeFloat32(output, offset, input) { for (var i = 0; i < input.length; i++, offset += 4) { - output.setFloat32(offset, input[i], true) + output.setFloat32(offset, input[i], true); } } function floatTo16BitPCM(output, offset, input) { for (var i = 0; i < input.length; i++, offset += 2) { - var s = Math.max(-1, Math.min(1, input[i])) - output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true) + var s = Math.max(-1, Math.min(1, input[i])); + output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true); } } function writeString(view, offset, string) { for (var i = 0; i < string.length; i++) { - view.setUint8(offset + i, string.charCodeAt(i)) + view.setUint8(offset + i, string.charCodeAt(i)); } -} \ No newline at end of file +} diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx index 4f474f38..036f6f36 100644 --- a/website/src/repl/components/ExportModal.jsx +++ b/website/src/repl/components/ExportModal.jsx @@ -3,215 +3,226 @@ import cx from '@src/cx.mjs'; import NumberInput from './NumberInput'; import { useEffect, useState } from 'react'; import { Textbox } from './textbox/Textbox'; -import { setMultiChannelOrbits as setStrudelMultiChannelOrbits, setMaxPolyphony as setStrudelMaxPolyphony, getAudioContext } from '@strudel/webaudio'; +import { + setMultiChannelOrbits as setStrudelMultiChannelOrbits, + setMaxPolyphony as setStrudelMaxPolyphony, + getAudioContext, +} from '@strudel/webaudio'; import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon'; function Checkbox({ label, value, onChange, disabled = false }) { - return ( - - ); + return ( + + ); } function FormItem({ label, children, disabled }) { - return ( -
- - {children} -
- ); + return ( +
+ + {children} +
+ ); } export default function ExportModal(Props) { - const { started, isEmbedded, handleExport } = Props; + const { started, isEmbedded, handleExport } = Props; - const [downloadName, setDownloadName] = useState("") // TODO: make a form? - const [startCycle, setStartCycle] = useState(0) - const [endCycle, setEndCycle] = useState(1) - const [sampleRate, setSampleRate] = useState(48000) - const [multiChannelOrbits, setMultiChannelOrbits] = useState(true) - const [maxPolyphony, setMaxPolyphony] = useState(1024) - const [exporting, setExporting] = useState(false) - const [progress, setProgress] = useState(0) - const [length, setLength] = useState(1) + const [downloadName, setDownloadName] = useState(''); // TODO: make a form? + const [startCycle, setStartCycle] = useState(0); + const [endCycle, setEndCycle] = useState(1); + const [sampleRate, setSampleRate] = useState(48000); + const [multiChannelOrbits, setMultiChannelOrbits] = useState(true); + const [maxPolyphony, setMaxPolyphony] = useState(1024); + const [exporting, setExporting] = useState(false); + const [progress, setProgress] = useState(0); + const [length, setLength] = useState(1); - const refreshProgress = () => { - const audioContext = getAudioContext() - if (audioContext instanceof OfflineAudioContext) { - setProgress(audioContext.currentTime) - setLength(audioContext.length / sampleRate) - setTimeout(refreshProgress, 100); - } + const refreshProgress = () => { + const audioContext = getAudioContext(); + if (audioContext instanceof OfflineAudioContext) { + setProgress(audioContext.currentTime); + setLength(audioContext.length / sampleRate); + setTimeout(refreshProgress, 100); } + }; - return ( - <> - + + +
+ + { + setDownloadName(e.target.value); + }} + onChange={(v) => { + setDownloadName(v); + }} + disabled={exporting} + placeholder="Leave empty to use current date" + className={cx('placeholder:opacity-50', exporting && 'opacity-50 border-opacity-50')} + value={downloadName ?? ''} + /> + +
+ + { + let v = parseInt(e.target.value); + v = isNaN(v) ? 0 : Math.max(0, v); + setStartCycle(v); }} - title="export" - className={cx( - 'flex items-center space-x-1', - !isEmbedded ? 'p-2' : 'px-2', - started ? 'opacity-50' : 'hover:opacity-50', - )} - > - {!isEmbedded && export} - - - -
- - { - setDownloadName(e.target.value) - }} - onChange={v => { - setDownloadName(v) - }} - disabled={exporting} - placeholder="Leave empty to use current date" - className={cx("placeholder:opacity-50", exporting && "opacity-50 border-opacity-50")} - value={downloadName ?? ''} - /> - -
- - { - let v = parseInt(e.target.value); - v = isNaN(v) ? 0 : Math.max(0, v); - setStartCycle(v); - }} - onChange={v => { - v = parseInt(v) - setStartCycle(v); - }} - type="number" - placeholder="" - disabled={exporting} - className={cx(exporting && "opacity-50 border-opacity-50")} - value={startCycle ?? ''} - /> - - - { - let v = parseInt(e.target.value); - v = isNaN(v) ? Math.max(startCycle + 1, parseInt(v)) : v; - setEndCycle(v); - }} - onChange={(v) => { - v = parseInt(v) - setEndCycle(v); - }} - type="number" - placeholder="" - disabled={exporting} - className={cx(exporting && "opacity-50 border-opacity-50")} - value={endCycle ?? ''} - /> - -
-
- - { - let v = parseInt(e.target.value); - v = isNaN(v) ? 1 : Math.max(1, v); - setSampleRate(v); - }} - onChange={v => { - v = parseInt(v) - setSampleRate(v); - }} - type="number" - placeholder="" - disabled={exporting} - className={cx(exporting && "opacity-50 border-opacity-50")} - value={sampleRate ?? ''} - /> - - - { - let v = parseInt(e.target.value); - v = isNaN(v) ? Math.max(1, parseInt(v)) : v; - setMaxPolyphony(v); - }} - onChange={(v) => { - v = Math.max(1, parseInt(v)); - setMaxPolyphony(v); - }} - type="number" - placeholder="" - disabled={exporting} - className={cx(exporting && "opacity-50 border-opacity-50")} - value={maxPolyphony ?? ''} - /> - -
-
- { - const val = cbEvent.target.checked; - setMultiChannelOrbits(val) - }} - disabled={exporting} - value={multiChannelOrbits} - /> -
- -
-
- - ); + onChange={(v) => { + v = parseInt(v); + setStartCycle(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && 'opacity-50 border-opacity-50')} + value={startCycle ?? ''} + /> +
+ + { + let v = parseInt(e.target.value); + v = isNaN(v) ? Math.max(startCycle + 1, parseInt(v)) : v; + setEndCycle(v); + }} + onChange={(v) => { + v = parseInt(v); + setEndCycle(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && 'opacity-50 border-opacity-50')} + value={endCycle ?? ''} + /> + +
+
+ + { + let v = parseInt(e.target.value); + v = isNaN(v) ? 1 : Math.max(1, v); + setSampleRate(v); + }} + onChange={(v) => { + v = parseInt(v); + setSampleRate(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && 'opacity-50 border-opacity-50')} + value={sampleRate ?? ''} + /> + + + { + let v = parseInt(e.target.value); + v = isNaN(v) ? Math.max(1, parseInt(v)) : v; + setMaxPolyphony(v); + }} + onChange={(v) => { + v = Math.max(1, parseInt(v)); + setMaxPolyphony(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && 'opacity-50 border-opacity-50')} + value={maxPolyphony ?? ''} + /> + +
+
+ { + const val = cbEvent.target.checked; + setMultiChannelOrbits(val); + }} + disabled={exporting} + value={multiChannelOrbits} + /> +
+ +
+
+ + ); } diff --git a/website/src/repl/components/Header.jsx b/website/src/repl/components/Header.jsx index d5d8b5f7..dd9f684c 100644 --- a/website/src/repl/components/Header.jsx +++ b/website/src/repl/components/Header.jsx @@ -9,8 +9,17 @@ const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; export function Header({ context, embedded = false }) { - const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare, handleExport } = - context; + const { + started, + pending, + isDirty, + activeCode, + handleTogglePlay, + handleEvaluate, + handleShuffle, + handleShare, + handleExport, + } = context; const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location); const { isZen, isButtonRowHidden, isCSSAnimationDisabled, fontFamily } = useSettings(); From 120b46c4a516d0a5932b6162f83185761503f8dc Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 20:24:42 +0300 Subject: [PATCH 030/194] Add webaudio as a cyclist dependency (do we want that?) --- packages/core/cyclist.mjs | 2 +- packages/core/package-lock.json | 331 ++++++++++++++++++++++++++++++++ packages/core/package.json | 1 + 3 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 packages/core/package-lock.json diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 185d5588..119f1d51 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import createClock from './zyklus.mjs'; import { errorLogger, logger } from './logger.mjs'; -import { loadBuffer, renderPatternAudio, setAudioContext, superdough } from '@strudel/webaudio'; +import { renderPatternAudio } from '@strudel/webaudio'; export class Cyclist { constructor({ diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json new file mode 100644 index 00000000..dea7b53a --- /dev/null +++ b/packages/core/package-lock.json @@ -0,0 +1,331 @@ +{ + "name": "@strudel/core", + "version": "1.2.4", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@strudel/core", + "version": "1.2.4", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@strudel/webaudio": "^1.2.5", + "fraction.js": "^5.2.1" + }, + "devDependencies": { + "vite": "^6.0.11", + "vitest": "^3.0.4" + } + }, + "../../node_modules/.pnpm/fraction.js@5.2.1/node_modules/fraction.js": { + "version": "5.2.1", + "license": "MIT", + "devDependencies": { + "crude-build": "^0.1.2", + "mocha": "*" + }, + "engines": { + "node": ">= 12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "../../node_modules/.pnpm/vite@6.0.11_@types+node@22.10.10_jiti@2.4.2_lightningcss@1.29.1_terser@5.37.0_yaml@2.7.0/node_modules/vite": { + "version": "6.0.11", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.24.2", + "postcss": "^8.4.49", + "rollup": "^4.23.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "devDependencies": { + "@ampproject/remapping": "^2.3.0", + "@babel/parser": "^7.26.3", + "@jridgewell/trace-mapping": "^0.3.25", + "@polka/compression": "^1.0.0-next.25", + "@rollup/plugin-alias": "^5.1.1", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-dynamic-import-vars": "2.1.4", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "16.0.0", + "@rollup/pluginutils": "^5.1.4", + "@types/escape-html": "^1.0.4", + "@types/pnpapi": "^0.0.5", + "artichokie": "^0.3.1", + "cac": "^6.7.14", + "chokidar": "^3.6.0", + "connect": "^3.7.0", + "convert-source-map": "^2.0.0", + "cors": "^2.8.5", + "cross-spawn": "^7.0.6", + "debug": "^4.4.0", + "dep-types": "link:./src/types", + "dotenv": "^16.4.7", + "dotenv-expand": "^12.0.1", + "es-module-lexer": "^1.6.0", + "escape-html": "^1.0.3", + "estree-walker": "^3.0.3", + "etag": "^1.8.1", + "http-proxy": "^1.18.1", + "launch-editor-middleware": "^2.9.1", + "lightningcss": "^1.28.2", + "magic-string": "^0.30.17", + "mlly": "^1.7.3", + "mrmime": "^2.0.0", + "nanoid": "^5.0.9", + "open": "^10.1.0", + "parse5": "^7.2.1", + "pathe": "^2.0.0", + "periscopic": "^4.0.2", + "picocolors": "^1.1.1", + "picomatch": "^4.0.2", + "postcss-import": "^16.1.0", + "postcss-load-config": "^6.0.1", + "postcss-modules": "^6.0.1", + "resolve.exports": "^2.0.3", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-esbuild": "^6.1.1", + "rollup-plugin-license": "^3.5.3", + "sass": "^1.83.1", + "sass-embedded": "^1.83.1", + "sirv": "^3.0.0", + "source-map-support": "^0.5.21", + "strip-literal": "^2.1.1", + "terser": "^5.37.0", + "tinyglobby": "^0.2.10", + "tsconfck": "^3.1.4", + "tslib": "^2.8.1", + "types": "link:./types", + "ufo": "^1.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "../../node_modules/.pnpm/vitest@3.0.4_@types+debug@4.1.12_@types+node@22.10.10_@vitest+ui@3.0.4_jiti@2.4.2_light_04b28d5d33383b617211459e5738d579/node_modules/vitest": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.0.4", + "@vitest/mocker": "3.0.4", + "@vitest/pretty-format": "^3.0.4", + "@vitest/runner": "3.0.4", + "@vitest/snapshot": "3.0.4", + "@vitest/spy": "3.0.4", + "@vitest/utils": "3.0.4", + "chai": "^5.1.2", + "debug": "^4.4.0", + "expect-type": "^1.1.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.0.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "devDependencies": { + "@ampproject/remapping": "^2.3.0", + "@antfu/install-pkg": "^1.0.0", + "@edge-runtime/vm": "^5.0.0", + "@sinonjs/fake-timers": "14.0.0", + "@types/debug": "^4.1.12", + "@types/estree": "^1.0.6", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/jsdom": "^21.1.7", + "@types/micromatch": "^4.0.9", + "@types/node": "^22.10.9", + "@types/prompts": "^2.4.9", + "@types/sinonjs__fake-timers": "^8.1.5", + "acorn-walk": "^8.3.4", + "birpc": "0.2.19", + "cac": "^6.7.14", + "chai-subset": "^1.6.0", + "fast-glob": "3.3.3", + "find-up": "^6.3.0", + "flatted": "^3.3.2", + "get-tsconfig": "^4.10.0", + "happy-dom": "^16.7.2", + "jsdom": "^25.0.1", + "local-pkg": "^0.5.1", + "micromatch": "^4.0.8", + "pretty-format": "^29.7.0", + "prompts": "^2.4.2", + "strip-literal": "^2.1.1", + "ws": "^8.18.0" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.0.4", + "@vitest/ui": "3.0.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/@strudel/core": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@strudel/core/-/core-1.2.4.tgz", + "integrity": "sha512-sUm9/zjEEQuWfW3hNOCcfNbn8jq2G+Ye+mvpLUgcGyLfI5XPlPTn/PBIkv2hFjNs/basnMyrOUvg0Z85AA6+OA==", + "license": "AGPL-3.0-or-later", + "dependencies": { + "fraction.js": "^5.2.1" + } + }, + "node_modules/@strudel/draw": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@strudel/draw/-/draw-1.2.4.tgz", + "integrity": "sha512-9ui5+ZQQB+EEgwJQueGVMgpnPSHnFgRgivlAZbdjEoMdSm5vULWvKK3Psml8WKtOBNiuL+5Bci7a0INRTRusOA==", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@strudel/core": "1.2.4" + } + }, + "node_modules/@strudel/webaudio": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@strudel/webaudio/-/webaudio-1.2.5.tgz", + "integrity": "sha512-ilcyBsuwkH6Ghz0egaqIJ5DZi0xFBZwVWvZb0vnfU7kVESroD2j6yptyFMysJFZhQ5pfRiZs2ZeF6yTsviUjng==", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@strudel/core": "1.2.4", + "@strudel/draw": "1.2.4", + "superdough": "1.2.5" + } + }, + "node_modules/fraction.js": { + "resolved": "../../node_modules/.pnpm/fraction.js@5.2.1/node_modules/fraction.js", + "link": true + }, + "node_modules/nanostores": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-0.11.4.tgz", + "integrity": "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/superdough": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/superdough/-/superdough-1.2.5.tgz", + "integrity": "sha512-QVkMGGZjWzaRxYKXoZYMfsRHVk5vwNiqAaDOseccSTgqPPwFRTY2fmHAm5OmwwS8XqFOenZjn+XU3LEaCZMdJw==", + "license": "AGPL-3.0-or-later", + "dependencies": { + "nanostores": "^0.11.3" + } + }, + "node_modules/vite": { + "resolved": "../../node_modules/.pnpm/vite@6.0.11_@types+node@22.10.10_jiti@2.4.2_lightningcss@1.29.1_terser@5.37.0_yaml@2.7.0/node_modules/vite", + "link": true + }, + "node_modules/vitest": { + "resolved": "../../node_modules/.pnpm/vitest@3.0.4_@types+debug@4.1.12_@types+node@22.10.10_@vitest+ui@3.0.4_jiti@2.4.2_light_04b28d5d33383b617211459e5738d579/node_modules/vitest", + "link": true + } + } +} diff --git a/packages/core/package.json b/packages/core/package.json index 7cf20cea..daaa111d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -31,6 +31,7 @@ }, "homepage": "https://strudel.cc", "dependencies": { + "@strudel/webaudio": "^1.2.5", "fraction.js": "^5.2.1" }, "gitHead": "0e26d4e741500f5bae35b023608f062a794905c2", From 8b3c0d2a4aa8e9d2c89622e63d2d614ff15f2ef0 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 20:27:31 +0300 Subject: [PATCH 031/194] Refresh deps --- packages/osc/server.js | 0 pnpm-lock.yaml | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) mode change 100644 => 100755 packages/osc/server.js diff --git a/packages/osc/server.js b/packages/osc/server.js old mode 100644 new mode 100755 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c225057f..40d6eb57 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,6 +225,9 @@ importers: packages/core: dependencies: + '@strudel/webaudio': + specifier: ^1.2.5 + version: 1.2.5 fraction.js: specifier: ^5.2.1 version: 5.2.1 @@ -2468,6 +2471,15 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@strudel/core@1.2.4': + resolution: {integrity: sha512-sUm9/zjEEQuWfW3hNOCcfNbn8jq2G+Ye+mvpLUgcGyLfI5XPlPTn/PBIkv2hFjNs/basnMyrOUvg0Z85AA6+OA==} + + '@strudel/draw@1.2.4': + resolution: {integrity: sha512-9ui5+ZQQB+EEgwJQueGVMgpnPSHnFgRgivlAZbdjEoMdSm5vULWvKK3Psml8WKtOBNiuL+5Bci7a0INRTRusOA==} + + '@strudel/webaudio@1.2.5': + resolution: {integrity: sha512-ilcyBsuwkH6Ghz0egaqIJ5DZi0xFBZwVWvZb0vnfU7kVESroD2j6yptyFMysJFZhQ5pfRiZs2ZeF6yTsviUjng==} + '@supabase/auth-js@2.67.3': resolution: {integrity: sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==} @@ -6989,6 +7001,9 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + superdough@1.2.5: + resolution: {integrity: sha512-QVkMGGZjWzaRxYKXoZYMfsRHVk5vwNiqAaDOseccSTgqPPwFRTY2fmHAm5OmwwS8XqFOenZjn+XU3LEaCZMdJw==} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -9829,6 +9844,20 @@ snapshots: '@sinclair/typebox@0.27.8': {} + '@strudel/core@1.2.4': + dependencies: + fraction.js: 5.2.1 + + '@strudel/draw@1.2.4': + dependencies: + '@strudel/core': 1.2.4 + + '@strudel/webaudio@1.2.5': + dependencies: + '@strudel/core': 1.2.4 + '@strudel/draw': 1.2.4 + superdough: 1.2.5 + '@supabase/auth-js@2.67.3': dependencies: '@supabase/node-fetch': 2.6.15 @@ -15431,6 +15460,10 @@ snapshots: pirates: 4.0.6 ts-interface-checker: 0.1.13 + superdough@1.2.5: + dependencies: + nanostores: 0.11.3 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 From 329289fc3589661473447f0f1d7563d858cdd04d Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 20:33:04 +0300 Subject: [PATCH 032/194] Revert refreshing deps --- packages/core/cyclist.mjs | 2 +- packages/core/package-lock.json | 331 -------------------------------- packages/core/package.json | 1 - packages/osc/server.js | 0 pnpm-lock.yaml | 33 ---- 5 files changed, 1 insertion(+), 366 deletions(-) delete mode 100644 packages/core/package-lock.json mode change 100755 => 100644 packages/osc/server.js diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 119f1d51..185d5588 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import createClock from './zyklus.mjs'; import { errorLogger, logger } from './logger.mjs'; -import { renderPatternAudio } from '@strudel/webaudio'; +import { loadBuffer, renderPatternAudio, setAudioContext, superdough } from '@strudel/webaudio'; export class Cyclist { constructor({ diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json deleted file mode 100644 index dea7b53a..00000000 --- a/packages/core/package-lock.json +++ /dev/null @@ -1,331 +0,0 @@ -{ - "name": "@strudel/core", - "version": "1.2.4", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@strudel/core", - "version": "1.2.4", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@strudel/webaudio": "^1.2.5", - "fraction.js": "^5.2.1" - }, - "devDependencies": { - "vite": "^6.0.11", - "vitest": "^3.0.4" - } - }, - "../../node_modules/.pnpm/fraction.js@5.2.1/node_modules/fraction.js": { - "version": "5.2.1", - "license": "MIT", - "devDependencies": { - "crude-build": "^0.1.2", - "mocha": "*" - }, - "engines": { - "node": ">= 12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "../../node_modules/.pnpm/vite@6.0.11_@types+node@22.10.10_jiti@2.4.2_lightningcss@1.29.1_terser@5.37.0_yaml@2.7.0/node_modules/vite": { - "version": "6.0.11", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.24.2", - "postcss": "^8.4.49", - "rollup": "^4.23.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "devDependencies": { - "@ampproject/remapping": "^2.3.0", - "@babel/parser": "^7.26.3", - "@jridgewell/trace-mapping": "^0.3.25", - "@polka/compression": "^1.0.0-next.25", - "@rollup/plugin-alias": "^5.1.1", - "@rollup/plugin-commonjs": "^28.0.2", - "@rollup/plugin-dynamic-import-vars": "2.1.4", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "16.0.0", - "@rollup/pluginutils": "^5.1.4", - "@types/escape-html": "^1.0.4", - "@types/pnpapi": "^0.0.5", - "artichokie": "^0.3.1", - "cac": "^6.7.14", - "chokidar": "^3.6.0", - "connect": "^3.7.0", - "convert-source-map": "^2.0.0", - "cors": "^2.8.5", - "cross-spawn": "^7.0.6", - "debug": "^4.4.0", - "dep-types": "link:./src/types", - "dotenv": "^16.4.7", - "dotenv-expand": "^12.0.1", - "es-module-lexer": "^1.6.0", - "escape-html": "^1.0.3", - "estree-walker": "^3.0.3", - "etag": "^1.8.1", - "http-proxy": "^1.18.1", - "launch-editor-middleware": "^2.9.1", - "lightningcss": "^1.28.2", - "magic-string": "^0.30.17", - "mlly": "^1.7.3", - "mrmime": "^2.0.0", - "nanoid": "^5.0.9", - "open": "^10.1.0", - "parse5": "^7.2.1", - "pathe": "^2.0.0", - "periscopic": "^4.0.2", - "picocolors": "^1.1.1", - "picomatch": "^4.0.2", - "postcss-import": "^16.1.0", - "postcss-load-config": "^6.0.1", - "postcss-modules": "^6.0.1", - "resolve.exports": "^2.0.3", - "rollup-plugin-dts": "^6.1.1", - "rollup-plugin-esbuild": "^6.1.1", - "rollup-plugin-license": "^3.5.3", - "sass": "^1.83.1", - "sass-embedded": "^1.83.1", - "sirv": "^3.0.0", - "source-map-support": "^0.5.21", - "strip-literal": "^2.1.1", - "terser": "^5.37.0", - "tinyglobby": "^0.2.10", - "tsconfck": "^3.1.4", - "tslib": "^2.8.1", - "types": "link:./types", - "ufo": "^1.5.4", - "ws": "^8.18.0" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "../../node_modules/.pnpm/vitest@3.0.4_@types+debug@4.1.12_@types+node@22.10.10_@vitest+ui@3.0.4_jiti@2.4.2_light_04b28d5d33383b617211459e5738d579/node_modules/vitest": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "3.0.4", - "@vitest/mocker": "3.0.4", - "@vitest/pretty-format": "^3.0.4", - "@vitest/runner": "3.0.4", - "@vitest/snapshot": "3.0.4", - "@vitest/spy": "3.0.4", - "@vitest/utils": "3.0.4", - "chai": "^5.1.2", - "debug": "^4.4.0", - "expect-type": "^1.1.0", - "magic-string": "^0.30.17", - "pathe": "^2.0.2", - "std-env": "^3.8.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinypool": "^1.0.2", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.0.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "devDependencies": { - "@ampproject/remapping": "^2.3.0", - "@antfu/install-pkg": "^1.0.0", - "@edge-runtime/vm": "^5.0.0", - "@sinonjs/fake-timers": "14.0.0", - "@types/debug": "^4.1.12", - "@types/estree": "^1.0.6", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/jsdom": "^21.1.7", - "@types/micromatch": "^4.0.9", - "@types/node": "^22.10.9", - "@types/prompts": "^2.4.9", - "@types/sinonjs__fake-timers": "^8.1.5", - "acorn-walk": "^8.3.4", - "birpc": "0.2.19", - "cac": "^6.7.14", - "chai-subset": "^1.6.0", - "fast-glob": "3.3.3", - "find-up": "^6.3.0", - "flatted": "^3.3.2", - "get-tsconfig": "^4.10.0", - "happy-dom": "^16.7.2", - "jsdom": "^25.0.1", - "local-pkg": "^0.5.1", - "micromatch": "^4.0.8", - "pretty-format": "^29.7.0", - "prompts": "^2.4.2", - "strip-literal": "^2.1.1", - "ws": "^8.18.0" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.4", - "@vitest/ui": "3.0.4", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/@strudel/core": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@strudel/core/-/core-1.2.4.tgz", - "integrity": "sha512-sUm9/zjEEQuWfW3hNOCcfNbn8jq2G+Ye+mvpLUgcGyLfI5XPlPTn/PBIkv2hFjNs/basnMyrOUvg0Z85AA6+OA==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "fraction.js": "^5.2.1" - } - }, - "node_modules/@strudel/draw": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@strudel/draw/-/draw-1.2.4.tgz", - "integrity": "sha512-9ui5+ZQQB+EEgwJQueGVMgpnPSHnFgRgivlAZbdjEoMdSm5vULWvKK3Psml8WKtOBNiuL+5Bci7a0INRTRusOA==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@strudel/core": "1.2.4" - } - }, - "node_modules/@strudel/webaudio": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@strudel/webaudio/-/webaudio-1.2.5.tgz", - "integrity": "sha512-ilcyBsuwkH6Ghz0egaqIJ5DZi0xFBZwVWvZb0vnfU7kVESroD2j6yptyFMysJFZhQ5pfRiZs2ZeF6yTsviUjng==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@strudel/core": "1.2.4", - "@strudel/draw": "1.2.4", - "superdough": "1.2.5" - } - }, - "node_modules/fraction.js": { - "resolved": "../../node_modules/.pnpm/fraction.js@5.2.1/node_modules/fraction.js", - "link": true - }, - "node_modules/nanostores": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-0.11.4.tgz", - "integrity": "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/superdough": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/superdough/-/superdough-1.2.5.tgz", - "integrity": "sha512-QVkMGGZjWzaRxYKXoZYMfsRHVk5vwNiqAaDOseccSTgqPPwFRTY2fmHAm5OmwwS8XqFOenZjn+XU3LEaCZMdJw==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "nanostores": "^0.11.3" - } - }, - "node_modules/vite": { - "resolved": "../../node_modules/.pnpm/vite@6.0.11_@types+node@22.10.10_jiti@2.4.2_lightningcss@1.29.1_terser@5.37.0_yaml@2.7.0/node_modules/vite", - "link": true - }, - "node_modules/vitest": { - "resolved": "../../node_modules/.pnpm/vitest@3.0.4_@types+debug@4.1.12_@types+node@22.10.10_@vitest+ui@3.0.4_jiti@2.4.2_light_04b28d5d33383b617211459e5738d579/node_modules/vitest", - "link": true - } - } -} diff --git a/packages/core/package.json b/packages/core/package.json index daaa111d..7cf20cea 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -31,7 +31,6 @@ }, "homepage": "https://strudel.cc", "dependencies": { - "@strudel/webaudio": "^1.2.5", "fraction.js": "^5.2.1" }, "gitHead": "0e26d4e741500f5bae35b023608f062a794905c2", diff --git a/packages/osc/server.js b/packages/osc/server.js old mode 100755 new mode 100644 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40d6eb57..c225057f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,9 +225,6 @@ importers: packages/core: dependencies: - '@strudel/webaudio': - specifier: ^1.2.5 - version: 1.2.5 fraction.js: specifier: ^5.2.1 version: 5.2.1 @@ -2471,15 +2468,6 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - '@strudel/core@1.2.4': - resolution: {integrity: sha512-sUm9/zjEEQuWfW3hNOCcfNbn8jq2G+Ye+mvpLUgcGyLfI5XPlPTn/PBIkv2hFjNs/basnMyrOUvg0Z85AA6+OA==} - - '@strudel/draw@1.2.4': - resolution: {integrity: sha512-9ui5+ZQQB+EEgwJQueGVMgpnPSHnFgRgivlAZbdjEoMdSm5vULWvKK3Psml8WKtOBNiuL+5Bci7a0INRTRusOA==} - - '@strudel/webaudio@1.2.5': - resolution: {integrity: sha512-ilcyBsuwkH6Ghz0egaqIJ5DZi0xFBZwVWvZb0vnfU7kVESroD2j6yptyFMysJFZhQ5pfRiZs2ZeF6yTsviUjng==} - '@supabase/auth-js@2.67.3': resolution: {integrity: sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==} @@ -7001,9 +6989,6 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true - superdough@1.2.5: - resolution: {integrity: sha512-QVkMGGZjWzaRxYKXoZYMfsRHVk5vwNiqAaDOseccSTgqPPwFRTY2fmHAm5OmwwS8XqFOenZjn+XU3LEaCZMdJw==} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -9844,20 +9829,6 @@ snapshots: '@sinclair/typebox@0.27.8': {} - '@strudel/core@1.2.4': - dependencies: - fraction.js: 5.2.1 - - '@strudel/draw@1.2.4': - dependencies: - '@strudel/core': 1.2.4 - - '@strudel/webaudio@1.2.5': - dependencies: - '@strudel/core': 1.2.4 - '@strudel/draw': 1.2.4 - superdough: 1.2.5 - '@supabase/auth-js@2.67.3': dependencies: '@supabase/node-fetch': 2.6.15 @@ -15460,10 +15431,6 @@ snapshots: pirates: 4.0.6 ts-interface-checker: 0.1.13 - superdough@1.2.5: - dependencies: - nanostores: 0.11.3 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 From 7774364ac78b6be15d1fab04b95abf3f9be94ef9 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 20:36:19 +0300 Subject: [PATCH 033/194] Add webaudio as a workspace dependency --- packages/core/package.json | 1 + packages/osc/server.js | 0 pnpm-lock.yaml | 3 +++ 3 files changed, 4 insertions(+) mode change 100644 => 100755 packages/osc/server.js diff --git a/packages/core/package.json b/packages/core/package.json index 7cf20cea..e771f0a7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -31,6 +31,7 @@ }, "homepage": "https://strudel.cc", "dependencies": { + "@strudel/webaudio": "workspace:*", "fraction.js": "^5.2.1" }, "gitHead": "0e26d4e741500f5bae35b023608f062a794905c2", diff --git a/packages/osc/server.js b/packages/osc/server.js old mode 100644 new mode 100755 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c225057f..217a63d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,6 +225,9 @@ importers: packages/core: dependencies: + '@strudel/webaudio': + specifier: workspace:* + version: link:../webaudio fraction.js: specifier: ^5.2.1 version: 5.2.1 From f5fc0ff9d19dfb0dc89017be4aa8dd66960864d6 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 20:58:01 +0300 Subject: [PATCH 034/194] Remove resetGlobalEffects from AudioContext --- packages/superdough/audioContext.mjs | 3 --- packages/webaudio/webaudio.mjs | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/superdough/audioContext.mjs b/packages/superdough/audioContext.mjs index 9a5913d8..2c34f9da 100644 --- a/packages/superdough/audioContext.mjs +++ b/packages/superdough/audioContext.mjs @@ -1,10 +1,7 @@ -import { analysers, resetGlobalEffects } from './superdough.mjs'; - let audioContext; export const setDefaultAudioContext = () => { audioContext = new AudioContext(); - resetGlobalEffects(); return audioContext; }; diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 43c01146..b1247e34 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -21,6 +21,7 @@ import { loadWorklets, setMaxPolyphony, setMultiChannelOrbits, + resetGlobalEffects, } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; @@ -99,6 +100,7 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d .finally(async () => { setAudioContext(null); setSuperdoughAudioController(null); + resetGlobalEffects() await initAudio(); }); } From 2a0635879cf4fdbc21c933fcaaa46ced4acb6a25 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 20:58:27 +0300 Subject: [PATCH 035/194] Format --- packages/webaudio/webaudio.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index b1247e34..8141b536 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -100,7 +100,7 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d .finally(async () => { setAudioContext(null); setSuperdoughAudioController(null); - resetGlobalEffects() + resetGlobalEffects(); await initAudio(); }); } From 0f652e8b5bc73609daa1f52a77cea9db6bc5fb47 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 21:24:27 +0300 Subject: [PATCH 036/194] Remove webaudio from core dependencies --- packages/codemirror/codemirror.mjs | 11 +++------- packages/core/cyclist.mjs | 10 --------- packages/core/package.json | 3 +-- packages/core/repl.mjs | 32 ++++++++++++++--------------- website/src/repl/useReplContext.jsx | 6 +++++- 5 files changed, 24 insertions(+), 38 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 84cbfa96..a5a978bb 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -42,9 +42,9 @@ const extensions = { isMultiCursorEnabled: (on) => on ? [ - EditorState.allowMultipleSelections.of(true), - EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), - ] + EditorState.allowMultipleSelections.of(true), + EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), + ] : [], }; const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); @@ -268,11 +268,6 @@ export class StrudelMirror { this.flash(); await this.repl.evaluate(this.code); } - async exportAudio(begin, end, sampleRate, downloadName = undefined) { - await this.repl.evaluate(this.code, false); - await this.repl.exportAudio(begin, end, sampleRate, downloadName); - this.repl.scheduler.stop(); - } async stop() { this.repl.scheduler.stop(); } diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 185d5588..fdd8cf1c 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -6,7 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th import createClock from './zyklus.mjs'; import { errorLogger, logger } from './logger.mjs'; -import { loadBuffer, renderPatternAudio, setAudioContext, superdough } from '@strudel/webaudio'; export class Cyclist { constructor({ @@ -111,15 +110,6 @@ export class Cyclist { this.clock.start(); this.setStarted(true); } - - async exportAudio(begin, end, sampleRate, downloadName = undefined) { - if (!this.pattern) { - throw new Error('Scheduler: no pattern set! call .setPattern first.'); - } - logger('[cyclist] exporting'); - await renderPatternAudio(this.pattern, this.cps, begin, end, sampleRate, downloadName); - } - pause() { logger('[cyclist] pause'); this.clock.pause(); diff --git a/packages/core/package.json b/packages/core/package.json index e771f0a7..50e02e4b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -31,7 +31,6 @@ }, "homepage": "https://strudel.cc", "dependencies": { - "@strudel/webaudio": "workspace:*", "fraction.js": "^5.2.1" }, "gitHead": "0e26d4e741500f5bae35b023608f062a794905c2", @@ -39,4 +38,4 @@ "vite": "^6.0.11", "vitest": "^3.0.4" } -} +} \ No newline at end of file diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 4a35b932..6cbc9e9c 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -91,8 +91,6 @@ export function repl({ const stop = () => scheduler.stop(); const start = () => scheduler.start(); - const exportAudio = async (begin, end, sampleRate, downloadName = undefined) => - await scheduler.exportAudio(begin, end, sampleRate, downloadName); const pause = () => scheduler.pause(); const toggle = () => scheduler.toggle(); const setCps = (cps) => { @@ -259,23 +257,23 @@ export function repl({ } }; const setCode = (code) => updateState({ code }); - return { scheduler, evaluate, start, exportAudio, stop, pause, setCps, setPattern, setCode, toggle, state }; + return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state }; } export const getTrigger = ({ getTime, defaultOutput }) => - async (hap, deadline, duration, cps, t) => { - // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) - // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 - try { - if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t); + async (hap, deadline, duration, cps, t) => { + // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) + // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 + try { + if (!hap.context.onTrigger || !hap.context.dominantTrigger) { + await defaultOutput(hap, deadline, duration, cps, t); + } + if (hap.context.onTrigger) { + // call signature of output / onTrigger is different... + await hap.context.onTrigger(hap, getTime(), cps, t); + } + } catch (err) { + errorLogger(err, 'getTrigger'); } - if (hap.context.onTrigger) { - // call signature of output / onTrigger is different... - await hap.context.onTrigger(hap, getTime(), cps, t); - } - } catch (err) { - errorLogger(err, 'getTrigger'); - } - }; + }; diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 0e89fb68..11827708 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -9,6 +9,7 @@ import { getDrawContext } from '@strudel/draw'; import { transpiler } from '@strudel/transpiler'; import { getAudioContextCurrentTime, + renderPatternAudio, webaudioOutput, resetGlobalEffects, resetLoadedSounds, @@ -203,8 +204,11 @@ export function useReplContext() { const handleEvaluate = () => { editorRef.current.evaluate(); }; + const handleExport = async (begin, end, sampleRate, downloadName = undefined) => { - await editorRef.current.exportAudio(begin, end, sampleRate, downloadName); + await editorRef.current.evaluate(); + await renderPatternAudio(editorRef.current.repl.state.pattern, editorRef.current.repl.scheduler.cps, begin, end, sampleRate, downloadName) + editorRef.current.repl.scheduler.stop(); }; const handleShuffle = async () => { const patternData = await getRandomTune(); From a2b76bde142dbca85a993fc9f18e7bf2071d8095 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 21:24:54 +0300 Subject: [PATCH 037/194] Format --- packages/codemirror/codemirror.mjs | 6 +++--- packages/core/repl.mjs | 28 ++++++++++++++-------------- website/src/repl/useReplContext.jsx | 9 ++++++++- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index a5a978bb..4dc23996 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -42,9 +42,9 @@ const extensions = { isMultiCursorEnabled: (on) => on ? [ - EditorState.allowMultipleSelections.of(true), - EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), - ] + EditorState.allowMultipleSelections.of(true), + EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), + ] : [], }; const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 6cbc9e9c..171697eb 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -262,18 +262,18 @@ export function repl({ export const getTrigger = ({ getTime, defaultOutput }) => - async (hap, deadline, duration, cps, t) => { - // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) - // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 - try { - if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t); - } - if (hap.context.onTrigger) { - // call signature of output / onTrigger is different... - await hap.context.onTrigger(hap, getTime(), cps, t); - } - } catch (err) { - errorLogger(err, 'getTrigger'); + async (hap, deadline, duration, cps, t) => { + // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) + // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 + try { + if (!hap.context.onTrigger || !hap.context.dominantTrigger) { + await defaultOutput(hap, deadline, duration, cps, t); } - }; + if (hap.context.onTrigger) { + // call signature of output / onTrigger is different... + await hap.context.onTrigger(hap, getTime(), cps, t); + } + } catch (err) { + errorLogger(err, 'getTrigger'); + } + }; diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 11827708..aacd0872 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -207,7 +207,14 @@ export function useReplContext() { const handleExport = async (begin, end, sampleRate, downloadName = undefined) => { await editorRef.current.evaluate(); - await renderPatternAudio(editorRef.current.repl.state.pattern, editorRef.current.repl.scheduler.cps, begin, end, sampleRate, downloadName) + await renderPatternAudio( + editorRef.current.repl.state.pattern, + editorRef.current.repl.scheduler.cps, + begin, + end, + sampleRate, + downloadName, + ); editorRef.current.repl.scheduler.stop(); }; const handleShuffle = async () => { From a8ea9a1b6334e0792b404c7bdefa850a8d41329e Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 21:26:40 +0300 Subject: [PATCH 038/194] Update lockfile --- pnpm-lock.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 217a63d8..c225057f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,9 +225,6 @@ importers: packages/core: dependencies: - '@strudel/webaudio': - specifier: workspace:* - version: link:../webaudio fraction.js: specifier: ^5.2.1 version: 5.2.1 From bf702c3b2a12a76d1dbac81c2d5eb0d50f72acff Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 21 Oct 2025 16:30:18 +0300 Subject: [PATCH 039/194] Reviewed changes --- packages/core/cyclist.mjs | 1 - packages/superdough/superdough.mjs | 5 ++--- packages/webaudio/webaudio.mjs | 14 ++++++-------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index fdd8cf1c..59e410c5 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -98,7 +98,6 @@ export class Cyclist { this.started = v; this.onToggle?.(v); } - async start() { await this.beforeStart?.(); this.num_ticks_since_cps_change = 0; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 118a152b..c2bbba36 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -282,7 +282,7 @@ export async function initAudioOnFirstClick(options) { } let controller; -function getSuperdoughAudioController() { +export function getSuperdoughAudioController() { if (controller == null) { controller = new SuperdoughAudioController(getAudioContext()); } @@ -376,7 +376,6 @@ function mapChannelNumbers(channels) { } export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { - // controller = null; // new: t is always expected to be the absolute target onset time const ac = getAudioContext(); const audioController = getSuperdoughAudioController(); @@ -402,7 +401,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) `[superdough]: cannot schedule sounds in the past (target: ${t.toFixed(2)}, now: ${ac.currentTime.toFixed(2)})`, ); return; - } // FIXME: fix + } // destructure let { tremolo, diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 8141b536..46b5d8d0 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -45,12 +45,11 @@ export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => { }; export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, downloadName = undefined) { - const oldAudioCtx = getAudioContext(); - await oldAudioCtx.close(); - let audioContext = new OfflineAudioContext(2, ((end - begin) / cps) * sampleRate, sampleRate); + let audioContext = getAudioContext(); + await audioContext.close(); + audioContext = new OfflineAudioContext(2, ((end - begin) / cps) * sampleRate, sampleRate); setAudioContext(audioContext); - let context = getAudioContext(); - setSuperdoughAudioController(new SuperdoughAudioController(context)); + setSuperdoughAudioController(new SuperdoughAudioController(audioContext)); setMaxPolyphony(1024); setMultiChannelOrbits(true); await loadWorklets(); @@ -69,7 +68,7 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d let bank = getSound(s).data.samples; if (bank) { let { url: sampleUrl, label } = getSampleInfo(h.value, bank); - await loadBuffer(sampleUrl, context, label); + await loadBuffer(sampleUrl, audioContext, label); } } }), @@ -81,10 +80,9 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d } }); - return context + return audioContext .startRendering() .then((renderedBuffer) => { - console.log(renderedBuffer); const wavBuffer = audioBufferToWav(renderedBuffer); const blob = new Blob([wavBuffer], { type: 'audio/wav' }); const url = URL.createObjectURL(blob); From 498624f6fe24b57b83d5e1e028b0b23a9ad8d5b7 Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 21 Oct 2025 16:35:33 +0300 Subject: [PATCH 040/194] Make server.js a normal file --- packages/osc/server.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 packages/osc/server.js diff --git a/packages/osc/server.js b/packages/osc/server.js old mode 100755 new mode 100644 From 98031676d6a13e5ee8ceb76a0e82cb539f92552c Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 21 Oct 2025 16:44:42 +0300 Subject: [PATCH 041/194] Add an EOL character to package.json --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 50e02e4b..7cf20cea 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -38,4 +38,4 @@ "vite": "^6.0.11", "vitest": "^3.0.4" } -} \ No newline at end of file +} From 92d3fe496d7d893497724cfb436ca6c7965561a5 Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 21 Oct 2025 21:00:30 +0300 Subject: [PATCH 042/194] Bug fixes --- packages/codemirror/codemirror.mjs | 4 ++-- packages/webaudio/webaudio.mjs | 4 ++-- website/src/repl/useReplContext.jsx | 8 +++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 4dc23996..a42529cc 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -264,9 +264,9 @@ export class StrudelMirror { console.warn('first frame could not be painted'); } } - async evaluate() { + async evaluate(autoplay = true) { this.flash(); - await this.repl.evaluate(this.code); + await this.repl.evaluate(this.code, autoplay); } async stop() { this.repl.scheduler.stop(); diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 46b5d8d0..7a5ffac3 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -52,11 +52,11 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d setSuperdoughAudioController(new SuperdoughAudioController(audioContext)); setMaxPolyphony(1024); setMultiChannelOrbits(true); - await loadWorklets(); + await initAudio(); logger('[webaudio] start rendering'); let haps = pattern.queryArc(begin, end, { _cps: cps }); - Promise.all( + await Promise.all( haps.map(async (h) => { let s; if (h.value.s) { diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index aacd0872..49d49315 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -206,7 +206,8 @@ export function useReplContext() { }; const handleExport = async (begin, end, sampleRate, downloadName = undefined) => { - await editorRef.current.evaluate(); + await editorRef.current.evaluate(true); + editorRef.current.repl.scheduler.stop(); await renderPatternAudio( editorRef.current.repl.state.pattern, editorRef.current.repl.scheduler.cps, @@ -214,8 +215,9 @@ export function useReplContext() { end, sampleRate, downloadName, - ); - editorRef.current.repl.scheduler.stop(); + ).finally(() => { + editorRef.current.repl.scheduler.stop(); + }); }; const handleShuffle = async () => { const patternData = await getRandomTune(); From daea207aff9403ae7db8365136caa7702f08337a Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 21 Oct 2025 22:42:01 +0300 Subject: [PATCH 043/194] Better sample loading --- packages/codemirror/codemirror.mjs | 10 ++++----- packages/superdough/sampler.mjs | 2 +- packages/superdough/superdough.mjs | 2 +- packages/webaudio/webaudio.mjs | 32 ++++++++++++++++++++++------- website/src/repl/useReplContext.jsx | 2 +- 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index a42529cc..3411728a 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -42,9 +42,9 @@ const extensions = { isMultiCursorEnabled: (on) => on ? [ - EditorState.allowMultipleSelections.of(true), - EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), - ] + EditorState.allowMultipleSelections.of(true), + EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), + ] : [], }; const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); @@ -264,9 +264,9 @@ export class StrudelMirror { console.warn('first frame could not be painted'); } } - async evaluate(autoplay = true) { + async evaluate(autostart = true) { this.flash(); - await this.repl.evaluate(this.code, autoplay); + await this.repl.evaluate(this.code, autostart); } async stop() { this.repl.scheduler.stop(); diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 3232fa47..d996f3a2 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -289,7 +289,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl); // asny stuff above took too long? - if (ac.currentTime > t) { + if (ac.currentTime > t && !(ac instanceof OfflineAudioContext)) { logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight'); // console.warn('sample still loading:', s, n); return; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index c2bbba36..e6972ab7 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -562,7 +562,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) return; } - if (ac.currentTime > t) { + if (ac.currentTime > t && !(ac instanceof OfflineAudioContext)) { logger('[webaudio] skip hap: still loading', ac.currentTime - t); return; } diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 7a5ffac3..fae2c46c 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -22,11 +22,14 @@ import { setMaxPolyphony, setMultiChannelOrbits, resetGlobalEffects, + getDefaultValue, } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; import { SuperdoughAudioController } from 'superdough/superdoughoutput.mjs'; - +import { loadModules } from '@src/repl/util.mjs'; +import { prebake } from '@src/repl/prebake.mjs'; +import { getFontBufferSource, getFontPitch } from 'node_modules/@strudel/soundfonts/fontloader.mjs'; registerWorklet(workletUrl); const { Pattern, logger, repl } = strudel; @@ -56,7 +59,9 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d logger('[webaudio] start rendering'); let haps = pattern.queryArc(begin, end, { _cps: cps }); - await Promise.all( + await Promise.all([ + loadModules(), + prebake(), haps.map(async (h) => { let s; if (h.value.s) { @@ -65,13 +70,26 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d } else { s = h.value.s; } - let bank = getSound(s).data.samples; - if (bank) { - let { url: sampleUrl, label } = getSampleInfo(h.value, bank); - await loadBuffer(sampleUrl, audioContext, label); + // let bank = getSound(s).data.samples; + let sample = getSound(s); + if (sample.data.type == "soundfont") { + sample.data.fonts.forEach(async (font) => { + await getFontBufferSource(font, h.value, audioContext) + }) } + if (sample.data.type == "sample") { + let url; + if (Array.isArray(sample)) { + url = sample.data.samples[i % sample.data.samples.length]; + } else if (typeof sample === 'object') { + console.log(sample.data.samples) + url = Object.values(sample.data.samples).flat()[getDefaultValue("i") % Object.values(sample.data.samples).length]; + } + await loadBuffer(url, audioContext, s, 0); + } // TODO: waveform } - }), + }) + ], ); haps.forEach(async (hap) => { if (hap.hasOnset()) { diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 49d49315..b775df65 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -206,7 +206,7 @@ export function useReplContext() { }; const handleExport = async (begin, end, sampleRate, downloadName = undefined) => { - await editorRef.current.evaluate(true); + await editorRef.current.evaluate(false); editorRef.current.repl.scheduler.stop(); await renderPatternAudio( editorRef.current.repl.state.pattern, From 5e27358fe86706d36775ac6134c3d7025fc780f6 Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 22 Oct 2025 16:19:58 +0300 Subject: [PATCH 044/194] Better preloading --- packages/webaudio/webaudio.mjs | 65 ++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index fae2c46c..2bdb9a98 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -23,6 +23,7 @@ import { setMultiChannelOrbits, resetGlobalEffects, getDefaultValue, + getSampleBuffer, } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; @@ -62,45 +63,47 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d await Promise.all([ loadModules(), prebake(), - haps.map(async (h) => { - let s; - if (h.value.s) { - if (h.value.bank) { - s = `${h.value.bank}_${h.value.s}`; - } else { - s = h.value.s; - } - // let bank = getSound(s).data.samples; - let sample = getSound(s); - if (sample.data.type == "soundfont") { - sample.data.fonts.forEach(async (font) => { - await getFontBufferSource(font, h.value, audioContext) - }) - } - if (sample.data.type == "sample") { - let url; - if (Array.isArray(sample)) { - url = sample.data.samples[i % sample.data.samples.length]; - } else if (typeof sample === 'object') { - console.log(sample.data.samples) - url = Object.values(sample.data.samples).flat()[getDefaultValue("i") % Object.values(sample.data.samples).length]; - } - await loadBuffer(url, audioContext, s, 0); - } // TODO: waveform + ]); + + haps.forEach(async (h) => { + let s; + if (h.value.s) { + if (h.value.bank) { + s = `${h.value.bank}_${h.value.s}`; + } else { + s = h.value.s; } - }) - ], - ); - haps.forEach(async (hap) => { + // let bank = getSound(s).data.samples; + let sample = getSound(s); + if (sample.data.type == "soundfont") { + for (let index = 0; index < sample.data.fonts.length; index++) { + const font = array[index]; + await getFontBufferSource(font, h.value, audioContext) + } + } + if (sample.data.type == "sample") { + for (let index = 0; index < sample.data.samples.length; index++) { + const sample = array[index]; + await getSampleBuffer(h.value, bank, sample) + } + } // TODO: waveform + } + }) + + for (let index = 0; index < haps.length; index++) { + const hap = haps[index]; if (hap.hasOnset()) { - hap.ensureObjectValue(); + await hap.ensureObjectValue(); + console.log("dough") await superdough(hap.value, hap.whole.begin.valueOf() / cps, hap.duration, cps, hap.whole?.begin.valueOf()); } - }); + } + console.log("starting rendering") return audioContext .startRendering() .then((renderedBuffer) => { + console.log('downloading') const wavBuffer = audioBufferToWav(renderedBuffer); const blob = new Blob([wavBuffer], { type: 'audio/wav' }); const url = URL.createObjectURL(blob); From e7578be6c5c11756e20669337ca33f310c24a2ea Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 22 Oct 2025 17:16:05 +0300 Subject: [PATCH 045/194] Remove preloading code and instead await superdough --- packages/webaudio/webaudio.mjs | 42 ++++------------------------------ 1 file changed, 5 insertions(+), 37 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 2bdb9a98..9764516c 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -57,48 +57,16 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d setMaxPolyphony(1024); setMultiChannelOrbits(true); await initAudio(); - logger('[webaudio] start rendering'); + logger('[webaudio] preloading'); let haps = pattern.queryArc(begin, end, { _cps: cps }); - await Promise.all([ - loadModules(), - prebake(), - ]); - haps.forEach(async (h) => { - let s; - if (h.value.s) { - if (h.value.bank) { - s = `${h.value.bank}_${h.value.s}`; - } else { - s = h.value.s; - } - // let bank = getSound(s).data.samples; - let sample = getSound(s); - if (sample.data.type == "soundfont") { - for (let index = 0; index < sample.data.fonts.length; index++) { - const font = array[index]; - await getFontBufferSource(font, h.value, audioContext) - } - } - if (sample.data.type == "sample") { - for (let index = 0; index < sample.data.samples.length; index++) { - const sample = array[index]; - await getSampleBuffer(h.value, bank, sample) - } - } // TODO: waveform - } - }) - - for (let index = 0; index < haps.length; index++) { - const hap = haps[index]; + await Promise.all(haps.map(async (hap) => { if (hap.hasOnset()) { - await hap.ensureObjectValue(); - console.log("dough") - await superdough(hap.value, hap.whole.begin.valueOf() / cps, hap.duration, cps, hap.whole?.begin.valueOf()); + await superdough(hap2value(hap), hap.whole.begin.valueOf() / cps, hap.duration, cps, hap.whole?.begin.valueOf()); } - } - console.log("starting rendering") + })); + logger('[webaudio] start rendering'); return audioContext .startRendering() From 95700e11967b9be35c984f67e9a8510573fbe80f Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 22 Oct 2025 20:00:36 +0300 Subject: [PATCH 046/194] Fix wavetables breaking after playback --- packages/codemirror/codemirror.mjs | 6 +++--- packages/superdough/superdough.mjs | 2 ++ packages/superdough/wavetable.mjs | 5 +++++ packages/webaudio/webaudio.mjs | 20 ++++++++++++++------ 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 3411728a..ab3a3c99 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -42,9 +42,9 @@ const extensions = { isMultiCursorEnabled: (on) => on ? [ - EditorState.allowMultipleSelections.of(true), - EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), - ] + EditorState.allowMultipleSelections.of(true), + EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), + ] : [], }; const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index e6972ab7..7fe60ae4 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -15,6 +15,7 @@ import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; import { getAudioContext, setAudioContext } from './audioContext.mjs'; import { SuperdoughAudioController } from './superdoughoutput.mjs'; +import { resetSeenKeys } from './wavetable.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; @@ -231,6 +232,7 @@ export async function initAudio(options = {}) { setMaxPolyphony(maxPolyphony); setMultiChannelOrbits(multiChannelOrbits); + resetSeenKeys(); if (typeof window === 'undefined') { return; } diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 01d4eb83..902fb4ed 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -40,6 +40,11 @@ export const Warpmode = Object.freeze({ }); const seenKeys = new Set(); + +export function resetSeenKeys() { + seenKeys.clear(); +} + async function getPayload(url, label, frameLen = 2048) { const key = `${url},${frameLen}`; if (!seenKeys.has(key)) { diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 9764516c..777a271a 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -61,17 +61,25 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d let haps = pattern.queryArc(begin, end, { _cps: cps }); - await Promise.all(haps.map(async (hap) => { - if (hap.hasOnset()) { - await superdough(hap2value(hap), hap.whole.begin.valueOf() / cps, hap.duration, cps, hap.whole?.begin.valueOf()); - } - })); + await Promise.all( + haps.map(async (hap) => { + if (hap.hasOnset()) { + await superdough( + hap2value(hap), + hap.whole.begin.valueOf() / cps, + hap.duration, + cps, + hap.whole?.begin.valueOf(), + ); + } + }), + ); logger('[webaudio] start rendering'); return audioContext .startRendering() .then((renderedBuffer) => { - console.log('downloading') + console.log('downloading'); const wavBuffer = audioBufferToWav(renderedBuffer); const blob = new Blob([wavBuffer], { type: 'audio/wav' }); const url = URL.createObjectURL(blob); From 7b74a902a21ffcf3aa171f2b28ebe29d821d25a6 Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 22 Oct 2025 20:05:03 +0300 Subject: [PATCH 047/194] Remove unneeded imports --- packages/webaudio/webaudio.mjs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 777a271a..c87f8ab0 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -12,25 +12,15 @@ import { doughTrigger, registerWorklet, setAudioContext, - getSampleBufferSource, - loadBuffer, - getSampleInfo, - getSound, initAudio, setSuperdoughAudioController, - loadWorklets, setMaxPolyphony, setMultiChannelOrbits, resetGlobalEffects, - getDefaultValue, - getSampleBuffer, } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; import { SuperdoughAudioController } from 'superdough/superdoughoutput.mjs'; -import { loadModules } from '@src/repl/util.mjs'; -import { prebake } from '@src/repl/prebake.mjs'; -import { getFontBufferSource, getFontPitch } from 'node_modules/@strudel/soundfonts/fontloader.mjs'; registerWorklet(workletUrl); const { Pattern, logger, repl } = strudel; @@ -79,7 +69,6 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d return audioContext .startRendering() .then((renderedBuffer) => { - console.log('downloading'); const wavBuffer = audioBufferToWav(renderedBuffer); const blob = new Blob([wavBuffer], { type: 'audio/wav' }); const url = URL.createObjectURL(blob); From 5a70599aac6907fbcb32b4e2a3cb85f55d8410de Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 22 Oct 2025 22:48:04 +0300 Subject: [PATCH 048/194] Ignore superdough errors when exporting, instead skip sound. --- packages/webaudio/webaudio.mjs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index c87f8ab0..81643488 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -54,13 +54,17 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d await Promise.all( haps.map(async (hap) => { if (hap.hasOnset()) { - await superdough( - hap2value(hap), - hap.whole.begin.valueOf() / cps, - hap.duration, - cps, - hap.whole?.begin.valueOf(), - ); + try { + await superdough( + hap2value(hap), + hap.whole.begin.valueOf() / cps, + hap.duration, + cps, + hap.whole?.begin.valueOf(), + ); + } catch (error) { + logger("[webaudio] An error had occured while calling superdough. Skipping hap.") + } } }), ); @@ -81,12 +85,12 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d document.body.removeChild(a); URL.revokeObjectURL(url); }) - .finally(async () => { - setAudioContext(null); - setSuperdoughAudioController(null); - resetGlobalEffects(); - await initAudio(); - }); + // .finally(async () => { + // setAudioContext(null); + // setSuperdoughAudioController(null); + // resetGlobalEffects(); + // await initAudio(); + // }); } export function webaudioRepl(options = {}) { From 16f885c010d9fc27f7afd7a49a3419476b767f86 Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 22 Oct 2025 23:09:30 +0300 Subject: [PATCH 049/194] Use errorLogger instead --- packages/webaudio/webaudio.mjs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 81643488..4ad778d1 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -17,6 +17,7 @@ import { setMaxPolyphony, setMultiChannelOrbits, resetGlobalEffects, + errorLogger, } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; @@ -62,8 +63,8 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d cps, hap.whole?.begin.valueOf(), ); - } catch (error) { - logger("[webaudio] An error had occured while calling superdough. Skipping hap.") + } catch (err) { + errorLogger(err, 'webaudio') } } }), @@ -85,12 +86,12 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d document.body.removeChild(a); URL.revokeObjectURL(url); }) - // .finally(async () => { - // setAudioContext(null); - // setSuperdoughAudioController(null); - // resetGlobalEffects(); - // await initAudio(); - // }); + .finally(async () => { + setAudioContext(null); + setSuperdoughAudioController(null); + resetGlobalEffects(); + await initAudio(); + }); } export function webaudioRepl(options = {}) { From 6baca67cd3f7ab18970dfff68d8e72dbfccd77f5 Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 23 Oct 2025 17:07:53 +0300 Subject: [PATCH 050/194] Fix maxPolyphony and multiChannelOrbits not applying to export --- packages/webaudio/webaudio.mjs | 8 ++++---- website/src/repl/components/ExportModal.jsx | 2 +- website/src/repl/useReplContext.jsx | 4 +++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 4ad778d1..3fc4df8b 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -39,15 +39,15 @@ export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => { return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf()); }; -export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, downloadName = undefined) { +export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, maxPolyphony, multiChannelOrbits, downloadName = undefined) { let audioContext = getAudioContext(); await audioContext.close(); audioContext = new OfflineAudioContext(2, ((end - begin) / cps) * sampleRate, sampleRate); setAudioContext(audioContext); setSuperdoughAudioController(new SuperdoughAudioController(audioContext)); - setMaxPolyphony(1024); - setMultiChannelOrbits(true); await initAudio(); + setMaxPolyphony(maxPolyphony); + setMultiChannelOrbits(multiChannelOrbits); logger('[webaudio] preloading'); let haps = pattern.queryArc(begin, end, { _cps: cps }); @@ -64,7 +64,7 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d hap.whole?.begin.valueOf(), ); } catch (err) { - errorLogger(err, 'webaudio') + errorLogger(err, 'webaudio'); } } }), diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx index 036f6f36..990eb113 100644 --- a/website/src/repl/components/ExportModal.jsx +++ b/website/src/repl/components/ExportModal.jsx @@ -204,7 +204,7 @@ export default function ExportModal(Props) { setStrudelMaxPolyphony(maxPolyphony); setStrudelMultiChannelOrbits(multiChannelOrbits); setTimeout(refreshProgress, 1000); - await handleExport(startCycle, endCycle, sampleRate, downloadName).finally(() => { + await handleExport(startCycle, endCycle, sampleRate, maxPolyphony, multiChannelOrbits, downloadName).finally(() => { setExporting(false); const modal = document.getElementById('exportModal'); setProgress(0); diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index b775df65..15b3fab1 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -205,7 +205,7 @@ export function useReplContext() { editorRef.current.evaluate(); }; - const handleExport = async (begin, end, sampleRate, downloadName = undefined) => { + const handleExport = async (begin, end, sampleRate, maxPolyphony, multiChannelOrbits, downloadName = undefined) => { await editorRef.current.evaluate(false); editorRef.current.repl.scheduler.stop(); await renderPatternAudio( @@ -214,6 +214,8 @@ export function useReplContext() { begin, end, sampleRate, + maxPolyphony, + multiChannelOrbits, downloadName, ).finally(() => { editorRef.current.repl.scheduler.stop(); From fe70a2df43c874639ff0fd778fe03e9261212de5 Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 23 Oct 2025 17:08:11 +0300 Subject: [PATCH 051/194] Format --- packages/webaudio/webaudio.mjs | 11 ++++++++++- website/src/repl/components/ExportModal.jsx | 9 ++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 3fc4df8b..0df487be 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -39,7 +39,16 @@ export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => { return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf()); }; -export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, maxPolyphony, multiChannelOrbits, downloadName = undefined) { +export async function renderPatternAudio( + pattern, + cps, + begin, + end, + sampleRate, + maxPolyphony, + multiChannelOrbits, + downloadName = undefined, +) { let audioContext = getAudioContext(); await audioContext.close(); audioContext = new OfflineAudioContext(2, ((end - begin) / cps) * sampleRate, sampleRate); diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx index 990eb113..ad4ddad5 100644 --- a/website/src/repl/components/ExportModal.jsx +++ b/website/src/repl/components/ExportModal.jsx @@ -204,7 +204,14 @@ export default function ExportModal(Props) { setStrudelMaxPolyphony(maxPolyphony); setStrudelMultiChannelOrbits(multiChannelOrbits); setTimeout(refreshProgress, 1000); - await handleExport(startCycle, endCycle, sampleRate, maxPolyphony, multiChannelOrbits, downloadName).finally(() => { + await handleExport( + startCycle, + endCycle, + sampleRate, + maxPolyphony, + multiChannelOrbits, + downloadName, + ).finally(() => { setExporting(false); const modal = document.getElementById('exportModal'); setProgress(0); From ca936aada775ec734c25769fa10af2a35f4da402 Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 23 Oct 2025 21:46:13 +0300 Subject: [PATCH 052/194] Fix incorrect duration calculation --- packages/webaudio/webaudio.mjs | 2 +- website/src/repl/components/ExportModal.jsx | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 0df487be..99e5be5e 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -68,7 +68,7 @@ export async function renderPatternAudio( await superdough( hap2value(hap), hap.whole.begin.valueOf() / cps, - hap.duration, + hap.duration / cps, cps, hap.whole?.begin.valueOf(), ); diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx index ad4ddad5..9f32d693 100644 --- a/website/src/repl/components/ExportModal.jsx +++ b/website/src/repl/components/ExportModal.jsx @@ -211,12 +211,13 @@ export default function ExportModal(Props) { maxPolyphony, multiChannelOrbits, downloadName, - ).finally(() => { - setExporting(false); + ).then(() => { const modal = document.getElementById('exportModal'); + modal.close(); + }).finally(() => { + setExporting(false); setProgress(0); setLength(1); - modal.close(); }); }} > From 9cbeaef88defc63150db0eb094ca3e7e2ad21275 Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 23 Oct 2025 22:21:40 +0300 Subject: [PATCH 053/194] Format --- website/src/repl/components/ExportModal.jsx | 25 +++++++++------------ 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx index 9f32d693..93624fa0 100644 --- a/website/src/repl/components/ExportModal.jsx +++ b/website/src/repl/components/ExportModal.jsx @@ -204,21 +204,16 @@ export default function ExportModal(Props) { setStrudelMaxPolyphony(maxPolyphony); setStrudelMultiChannelOrbits(multiChannelOrbits); setTimeout(refreshProgress, 1000); - await handleExport( - startCycle, - endCycle, - sampleRate, - maxPolyphony, - multiChannelOrbits, - downloadName, - ).then(() => { - const modal = document.getElementById('exportModal'); - modal.close(); - }).finally(() => { - setExporting(false); - setProgress(0); - setLength(1); - }); + await handleExport(startCycle, endCycle, sampleRate, maxPolyphony, multiChannelOrbits, downloadName) + .then(() => { + const modal = document.getElementById('exportModal'); + modal.close(); + }) + .finally(() => { + setExporting(false); + setProgress(0); + setLength(1); + }); }} > {exporting ? 'Exporting...' : 'Export to WAV'} From ff370cf86f5d5a2b7bc21b59f74fb1839cd3c755 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 24 Oct 2025 14:07:57 +0300 Subject: [PATCH 054/194] Reapply realtimeOptions after export --- packages/superdough/sampler.mjs | 2 +- packages/superdough/superdough.mjs | 8 ++++---- packages/webaudio/webaudio.mjs | 15 +++++++++++---- website/src/repl/components/ExportModal.jsx | 4 ---- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 742be592..84bac358 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -287,7 +287,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl); // asny stuff above took too long? - if (ac.currentTime > t && !(ac instanceof OfflineAudioContext)) { + if (ac.currentTime > t) { logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight'); // console.warn('sample still loading:', s, n); return; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index ae722b7c..e7d62a8e 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -20,13 +20,13 @@ import { resetSeenKeys } from './wavetable.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; -let maxPolyphony = DEFAULT_MAX_POLYPHONY; +export let maxPolyphony = DEFAULT_MAX_POLYPHONY; export function setMaxPolyphony(polyphony) { maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY; } -let multiChannelOrbits = false; +export let multiChannelOrbits = false; export function setMultiChannelOrbits(bool) { multiChannelOrbits = bool == true; } @@ -398,7 +398,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // duration is passed as value too.. value.duration = hapDuration; // calculate absolute time - if (t < ac.currentTime && !(ac instanceof OfflineAudioContext)) { + if (t < ac.currentTime) { console.warn( `[superdough]: cannot schedule sounds in the past (target: ${t.toFixed(2)}, now: ${ac.currentTime.toFixed(2)})`, ); @@ -564,7 +564,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) return; } - if (ac.currentTime > t && !(ac instanceof OfflineAudioContext)) { + if (ac.currentTime > t) { logger('[webaudio] skip hap: still loading', ac.currentTime - t); return; } diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 99e5be5e..c9b5cbda 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -18,6 +18,8 @@ import { setMultiChannelOrbits, resetGlobalEffects, errorLogger, + maxPolyphony as superdoughMaxPolyphony, + multiChannelOrbits as superdoughMultiChannelOrbits } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; @@ -49,14 +51,19 @@ export async function renderPatternAudio( multiChannelOrbits, downloadName = undefined, ) { + let realtimeOptions = { + maxPolyphony: superdoughMaxPolyphony, + multiChannelOrbits: superdoughMultiChannelOrbits + } let audioContext = getAudioContext(); await audioContext.close(); audioContext = new OfflineAudioContext(2, ((end - begin) / cps) * sampleRate, sampleRate); setAudioContext(audioContext); setSuperdoughAudioController(new SuperdoughAudioController(audioContext)); - await initAudio(); - setMaxPolyphony(maxPolyphony); - setMultiChannelOrbits(multiChannelOrbits); + await initAudio({ + maxPolyphony, + multiChannelOrbits + }); logger('[webaudio] preloading'); let haps = pattern.queryArc(begin, end, { _cps: cps }); @@ -99,7 +106,7 @@ export async function renderPatternAudio( setAudioContext(null); setSuperdoughAudioController(null); resetGlobalEffects(); - await initAudio(); + await initAudio(realtimeOptions); }); } diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx index 93624fa0..4bc8990e 100644 --- a/website/src/repl/components/ExportModal.jsx +++ b/website/src/repl/components/ExportModal.jsx @@ -4,8 +4,6 @@ import NumberInput from './NumberInput'; import { useEffect, useState } from 'react'; import { Textbox } from './textbox/Textbox'; import { - setMultiChannelOrbits as setStrudelMultiChannelOrbits, - setMaxPolyphony as setStrudelMaxPolyphony, getAudioContext, } from '@strudel/webaudio'; import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon'; @@ -201,8 +199,6 @@ export default function ExportModal(Props) { disabled={exporting} onClick={async () => { setExporting(true); - setStrudelMaxPolyphony(maxPolyphony); - setStrudelMultiChannelOrbits(multiChannelOrbits); setTimeout(refreshProgress, 1000); await handleExport(startCycle, endCycle, sampleRate, maxPolyphony, multiChannelOrbits, downloadName) .then(() => { From fb7f686519130ed07fe66f561e6ab46cbc609a27 Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 16 Nov 2025 15:22:34 -0600 Subject: [PATCH 055/194] Add seeding --- packages/core/signal.mjs | 111 +- test/__snapshots__/examples.test.mjs.snap | 1627 +++++++++++---------- 2 files changed, 893 insertions(+), 845 deletions(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 492f6a7f..9f3a264f 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -16,7 +16,7 @@ export function steady(value) { } export const signal = (func) => { - const query = (state) => [new Hap(undefined, state.span, func(state.span.begin))]; + const query = (state) => [new Hap(undefined, state.span, func(state.span.begin, state.controls))]; return new Pattern(query); }; @@ -191,7 +191,7 @@ export const mouseX = signal(() => _mouseX); // Produce "Avalanche effect" where flipping a single bit of x // results in all output bits flipping with probability 0.5 // See e.g. https://github.com/aappleby/smhasher/blob/0ff96f7835817a27d0487325b6c16033e2992eb5/src/MurmurHash3.cpp#L68-L77 -function _murmurHashFinalizer(x) { +const _murmurHashFinalizer = (x) => { x |= 0; x ^= x >>> 16; x = Math.imul(x, 0x85ebca6b); @@ -199,36 +199,37 @@ function _murmurHashFinalizer(x) { x = Math.imul(x, 0xc2b2ae35); x ^= x >>> 16; return x >>> 0; // unsigned -} +}; // Convert t to a 32 bit integer, preserving temporal resolution down to 1/2^29 -function _tToT(t) { +const _tToT = (t) => { return Math.floor(t * 536870912); -} +}; -// Used to decorrelate nearby T and i prior to hashing -function _decorrelate(T, i = 0) { +// Used to decorrelate nearby T, i, and seed prior to hashing +const _decorrelate = (T, i = 0, seed = 0) => { const lowBits = (T >>> 0) >>> 0; const highBits = Math.floor(T / 4294967296) >>> 0; // 2^32 let key = lowBits ^ Math.imul(highBits ^ 0x85ebca6b, 0xc2b2ae35); key ^= Math.imul(i ^ 0x7f4a7c15, 0x9e3779b9); + key ^= Math.imul(seed ^ 0x165667b1, 0x27d4eb2d); return key >>> 0; -} +}; -function randAt(T, i = 0) { - return _murmurHashFinalizer(_decorrelate(T, i)) / 4294967296; // 2^32 -} +const randAt = (T, i = 0, seed = 0) => { + return _murmurHashFinalizer(_decorrelate(T, i, seed)) / 4294967296; // 2^32 +}; // n samples at time t -function timeToRands(t, n) { +const timeToRands = (t, n, seed = 0) => { const T = _tToT(t); if (n === 1) { - return randAt(T, 0); + return randAt(T, 0, seed); } const out = new Array(n); - for (let i = 0; i < n; i++) out[i] = randAt(T, i); + for (let i = 0; i < n; i++) out[i] = randAt(T, i, seed); return out; -} +}; // Deprecated: Old random signals. Configuration `useOldRandom` may be used for legacy songs @@ -257,8 +258,8 @@ const __timeToRands = (t, n) => __timeToRandsPrime(__timeToIntSeed(t), n); // End old random let useOldRandomBool = false; -const getRandsAtTime = (t, n = 1) => { - return useOldRandomBool ? __timeToRands(t, n) : timeToRands(t, n); +export const getRandsAtTime = (t, n = 1, seed = 0) => { + return useOldRandomBool ? __timeToRands(t + seed, n) : timeToRands(t, n, seed); }; /** @@ -314,9 +315,9 @@ export const binaryN = (n, nBits = 16) => { }; export const randrun = (n) => { - return signal((t) => { + return signal((t, controls) => { // Without adding 0.5, the first cycle is always 0,1,2,3,... - const rands = getRandsAtTime(t.floor().add(0.5), n); + const rands = getRandsAtTime(t.floor().add(0.5), n, controls.randSeed); const nums = rands .map((n, i) => [n, i]) .sort((a, b) => (a[0] > b[0]) - (a[0] < b[0])) @@ -357,6 +358,50 @@ export const scramble = register('scramble', (n, pat) => { return _rearrangeWith(_irand(n)._segment(n), n, pat); }); +/** + * Modify a pattern by applying a function to the `randomSeed` control if present + * + * @param {Function} func Function from seed (or undefined) to seed (or undefined) + * @param {Pattern} pat Pattern to update + * @returns Pattern + */ +function withSeed(func, pat) { + return new Pattern((state) => { + let { randSeed, ...controls } = state.controls; + randSeed = func(randSeed); + return pat.query(state.setControls({ ...controls, randSeed })); + }, pat._steps); +} + +/** + * Change the seed for random signals. Normally, random signals depend on time, + * so two patterns at the same time will have the same random values. Specifying + * a new seed changes the signal output by `rand`. This also affects other functions + * that use randomness, like `shuffle` and `sometimes`. + * + * @name seed + * @param {number} n A new seed. Can be any number. + * @example + * $: s("hh*4").degrade(); + * $: s("bd*4").degrade().seed(1); // Will degrade different events from the hi-hat + */ +export const seed = register('seed', (n, pat) => { + return withSeed((prev) => (prev !== undefined ? randAt(prev, n) * Number.MAX_SAFE_INTEGER : n), pat); +}); + +/** + * Set the seed for random signals. Differs from `seed` in that `seed` can be used + * multiple times with the final signal depending on all seeds. `setSeed` on the + * other hand overwrites the previous calls to `seed`. + * + * @name setSeed + * @param {number} n An arbitrary number to be used as the new seed. 0 is the same + * as the default random behavior. + */ +export const setSeed = register('setSeed', (n, pat) => { + return withSeed(() => n, pat); +}); + /** * A continuous pattern of random numbers, between 0 and 1. * @@ -366,7 +411,7 @@ export const scramble = register('scramble', (n, pat) => { * s("bd*4,hh*8").cutoff(rand.range(500,8000)) * */ -export const rand = signal(getRandsAtTime); +export const rand = signal((t, controls) => getRandsAtTime(t, 1, controls.randSeed)); /** * A continuous pattern of random numbers, between -1 and 1 */ @@ -543,36 +588,32 @@ export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pair export const wrandcat = wchooseCycles; -function _perlin(t) { +function _perlin(t, seed = 0) { let ta = Math.floor(t); let tb = ta + 1; const smootherStep = (x) => 6.0 * x ** 5 - 15.0 * x ** 4 + 10.0 * x ** 3; const interp = (x) => (a) => (b) => a + smootherStep(x) * (b - a); - const v = interp(t - ta)(getRandsAtTime(ta))(getRandsAtTime(tb)); + const ra = getRandsAtTime(ta, 1, seed); + const rb = getRandsAtTime(tb, 1, seed); + const v = interp(t - ta)(ra)(rb); return v; } -export const perlinWith = (tpat) => { - return tpat.fmap(_perlin); -}; -function _berlin(t) { +function _berlin(t, seed = 0) { const prevRidgeStartIndex = Math.floor(t); const nextRidgeStartIndex = prevRidgeStartIndex + 1; - const prevRidgeBottomPoint = getRandsAtTime(prevRidgeStartIndex); - const nextRidgeTopPoint = getRandsAtTime(nextRidgeStartIndex) + prevRidgeBottomPoint; + const prevRidgeBottomPoint = getRandsAtTime(prevRidgeStartIndex, 1, seed); + const height = getRandsAtTime(nextRidgeStartIndex, 1, seed); + const nextRidgeTopPoint = prevRidgeBottomPoint + height; const currentPercent = (t - prevRidgeStartIndex) / (nextRidgeStartIndex - prevRidgeStartIndex); const interp = (a, b, t) => { - return a + (b - a) * t; + return a + t * (b - a); }; return interp(prevRidgeBottomPoint, nextRidgeTopPoint, currentPercent) / 2; } -export const berlinWith = (tpat) => { - return tpat.fmap(_berlin); -}; - /** * Generates a continuous pattern of [perlin noise](https://en.wikipedia.org/wiki/Perlin_noise), in the range 0..1. * @@ -582,7 +623,7 @@ export const berlinWith = (tpat) => { * s("bd*4,hh*8").cutoff(perlin.range(500,8000)) * */ -export const perlin = perlinWith(time.fmap((v) => Number(v))); +export const perlin = signal((t, controls) => _perlin(t, controls.randSeed)); /** * Generates a continuous pattern of [berlin noise](conceived by Jame Coyne and Jade Rowland as a joke but turned out to be surprisingly cool and useful, @@ -594,7 +635,7 @@ export const perlin = perlinWith(time.fmap((v) => Number(v))); * n("0!16".add(berlin.fast(4).mul(14))).scale("d:minor") * */ -export const berlin = berlinWith(time.fmap((v) => Number(v))); +export const berlin = signal((t, controls) => _berlin(t, controls.randSeed)); export const degradeByWith = register( 'degradeByWith', diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index c045133f..b26823a9 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -677,10 +677,10 @@ exports[`runs examples > example "almostAlways" example index 0 1`] = ` "[ 1/2 → 5/8 | s:hh speed:0.5 ]", "[ 5/8 → 3/4 | s:hh speed:0.5 ]", "[ 3/4 → 7/8 | s:hh speed:0.5 ]", - "[ 7/8 → 1/1 | s:hh speed:0.5 ]", + "[ 7/8 → 1/1 | s:hh ]", "[ 1/1 → 9/8 | s:hh speed:0.5 ]", "[ 9/8 → 5/4 | s:hh speed:0.5 ]", - "[ 5/4 → 11/8 | s:hh speed:0.5 ]", + "[ 5/4 → 11/8 | s:hh ]", "[ 11/8 → 3/2 | s:hh speed:0.5 ]", "[ 3/2 → 13/8 | s:hh speed:0.5 ]", "[ 13/8 → 7/4 | s:hh speed:0.5 ]", @@ -693,10 +693,10 @@ exports[`runs examples > example "almostAlways" example index 0 1`] = ` "[ 5/2 → 21/8 | s:hh speed:0.5 ]", "[ 21/8 → 11/4 | s:hh speed:0.5 ]", "[ 11/4 → 23/8 | s:hh speed:0.5 ]", - "[ 23/8 → 3/1 | s:hh ]", + "[ 23/8 → 3/1 | s:hh speed:0.5 ]", "[ 3/1 → 25/8 | s:hh speed:0.5 ]", "[ 25/8 → 13/4 | s:hh speed:0.5 ]", - "[ 13/4 → 27/8 | s:hh speed:0.5 ]", + "[ 13/4 → 27/8 | s:hh ]", "[ 27/8 → 7/2 | s:hh speed:0.5 ]", "[ 7/2 → 29/8 | s:hh speed:0.5 ]", "[ 29/8 → 15/4 | s:hh speed:0.5 ]", @@ -715,10 +715,10 @@ exports[`runs examples > example "almostNever" example index 0 1`] = ` "[ 5/8 → 3/4 | s:hh ]", "[ 3/4 → 7/8 | s:hh ]", "[ 7/8 → 1/1 | s:hh ]", - "[ 1/1 → 9/8 | s:hh speed:0.5 ]", - "[ 9/8 → 5/4 | s:hh ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 9/8 → 5/4 | s:hh speed:0.5 ]", "[ 5/4 → 11/8 | s:hh ]", - "[ 11/8 → 3/2 | s:hh ]", + "[ 11/8 → 3/2 | s:hh speed:0.5 ]", "[ 3/2 → 13/8 | s:hh ]", "[ 13/8 → 7/4 | s:hh ]", "[ 7/4 → 15/8 | s:hh ]", @@ -728,17 +728,17 @@ exports[`runs examples > example "almostNever" example index 0 1`] = ` "[ 9/4 → 19/8 | s:hh ]", "[ 19/8 → 5/2 | s:hh ]", "[ 5/2 → 21/8 | s:hh ]", - "[ 21/8 → 11/4 | s:hh speed:0.5 ]", + "[ 21/8 → 11/4 | s:hh ]", "[ 11/4 → 23/8 | s:hh ]", "[ 23/8 → 3/1 | s:hh ]", "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", "[ 13/4 → 27/8 | s:hh ]", - "[ 27/8 → 7/2 | s:hh ]", + "[ 27/8 → 7/2 | s:hh speed:0.5 ]", "[ 7/2 → 29/8 | s:hh ]", "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:hh speed:0.5 ]", - "[ 31/8 → 4/1 | s:hh speed:0.5 ]", + "[ 15/4 → 31/8 | s:hh ]", + "[ 31/8 → 4/1 | s:hh ]", ] `; @@ -1078,70 +1078,70 @@ exports[`runs examples > example "begin" example index 0 1`] = ` exports[`runs examples > example "berlin" example index 0 1`] = ` [ - "[ 0/1 → 1/16 | note:A3 ]", - "[ 1/16 → 1/8 | note:A3 ]", + "[ 0/1 → 1/16 | note:G3 ]", + "[ 1/16 → 1/8 | note:G3 ]", "[ 1/8 → 3/16 | note:A3 ]", "[ 3/16 → 1/4 | note:A3 ]", "[ 1/4 → 5/16 | note:E3 ]", "[ 5/16 → 3/8 | note:F3 ]", - "[ 3/8 → 7/16 | note:F3 ]", - "[ 7/16 → 1/2 | note:G3 ]", - "[ 1/2 → 9/16 | note:A3 ]", - "[ 9/16 → 5/8 | note:Bb3 ]", - "[ 5/8 → 11/16 | note:Bb3 ]", - "[ 11/16 → 3/4 | note:C4 ]", - "[ 3/4 → 13/16 | note:G3 ]", - "[ 13/16 → 7/8 | note:A3 ]", - "[ 7/8 → 15/16 | note:A3 ]", - "[ 15/16 → 1/1 | note:Bb3 ]", - "[ 1/1 → 17/16 | note:F3 ]", + "[ 3/8 → 7/16 | note:G3 ]", + "[ 7/16 → 1/2 | note:A3 ]", + "[ 1/2 → 9/16 | note:Bb3 ]", + "[ 9/16 → 5/8 | note:C4 ]", + "[ 5/8 → 11/16 | note:D4 ]", + "[ 11/16 → 3/4 | note:E4 ]", + "[ 3/4 → 13/16 | note:Bb3 ]", + "[ 13/16 → 7/8 | note:Bb3 ]", + "[ 7/8 → 15/16 | note:Bb3 ]", + "[ 15/16 → 1/1 | note:C4 ]", + "[ 1/1 → 17/16 | note:E3 ]", "[ 17/16 → 9/8 | note:G3 ]", - "[ 9/8 → 19/16 | note:G3 ]", - "[ 19/16 → 5/4 | note:A3 ]", - "[ 5/4 → 21/16 | note:G3 ]", - "[ 21/16 → 11/8 | note:Bb3 ]", + "[ 9/8 → 19/16 | note:A3 ]", + "[ 19/16 → 5/4 | note:C4 ]", + "[ 5/4 → 21/16 | note:C4 ]", + "[ 21/16 → 11/8 | note:D4 ]", "[ 11/8 → 23/16 | note:D4 ]", "[ 23/16 → 3/2 | note:E4 ]", - "[ 3/2 → 25/16 | note:D4 ]", - "[ 25/16 → 13/8 | note:E4 ]", - "[ 13/8 → 27/16 | note:E4 ]", - "[ 27/16 → 7/4 | note:E4 ]", - "[ 7/4 → 29/16 | note:F3 ]", - "[ 29/16 → 15/8 | note:A3 ]", - "[ 15/8 → 31/16 | note:Bb3 ]", - "[ 31/16 → 2/1 | note:C4 ]", - "[ 2/1 → 33/16 | note:C4 ]", - "[ 33/16 → 17/8 | note:E4 ]", - "[ 17/8 → 35/16 | note:G4 ]", - "[ 35/16 → 9/4 | note:A4 ]", - "[ 9/4 → 37/16 | note:D4 ]", - "[ 37/16 → 19/8 | note:F4 ]", - "[ 19/8 → 39/16 | note:G4 ]", - "[ 39/16 → 5/2 | note:A4 ]", - "[ 5/2 → 41/16 | note:Bb3 ]", - "[ 41/16 → 21/8 | note:C4 ]", - "[ 21/8 → 43/16 | note:C4 ]", - "[ 43/16 → 11/4 | note:C4 ]", - "[ 11/4 → 45/16 | note:F3 ]", - "[ 45/16 → 23/8 | note:G3 ]", - "[ 23/8 → 47/16 | note:G3 ]", - "[ 47/16 → 3/1 | note:A3 ]", - "[ 3/1 → 49/16 | note:F3 ]", - "[ 49/16 → 25/8 | note:G3 ]", - "[ 25/8 → 51/16 | note:G3 ]", - "[ 51/16 → 13/4 | note:A3 ]", - "[ 13/4 → 53/16 | note:G3 ]", - "[ 53/16 → 27/8 | note:A3 ]", - "[ 27/8 → 55/16 | note:C4 ]", - "[ 55/16 → 7/2 | note:E4 ]", - "[ 7/2 → 57/16 | note:D4 ]", - "[ 57/16 → 29/8 | note:E4 ]", - "[ 29/8 → 59/16 | note:F4 ]", - "[ 59/16 → 15/4 | note:G4 ]", - "[ 15/4 → 61/16 | note:Bb3 ]", - "[ 61/16 → 31/8 | note:C4 ]", - "[ 31/8 → 63/16 | note:D4 ]", - "[ 63/16 → 4/1 | note:E4 ]", + "[ 3/2 → 25/16 | note:F3 ]", + "[ 25/16 → 13/8 | note:A3 ]", + "[ 13/8 → 27/16 | note:Bb3 ]", + "[ 27/16 → 7/4 | note:D4 ]", + "[ 7/4 → 29/16 | note:C4 ]", + "[ 29/16 → 15/8 | note:D4 ]", + "[ 15/8 → 31/16 | note:E4 ]", + "[ 31/16 → 2/1 | note:F4 ]", + "[ 2/1 → 33/16 | note:Bb3 ]", + "[ 33/16 → 17/8 | note:C4 ]", + "[ 17/8 → 35/16 | note:D4 ]", + "[ 35/16 → 9/4 | note:E4 ]", + "[ 9/4 → 37/16 | note:Bb3 ]", + "[ 37/16 → 19/8 | note:C4 ]", + "[ 19/8 → 39/16 | note:D4 ]", + "[ 39/16 → 5/2 | note:F4 ]", + "[ 5/2 → 41/16 | note:C4 ]", + "[ 41/16 → 21/8 | note:D4 ]", + "[ 21/8 → 43/16 | note:F4 ]", + "[ 43/16 → 11/4 | note:A4 ]", + "[ 11/4 → 45/16 | note:D4 ]", + "[ 45/16 → 23/8 | note:F4 ]", + "[ 23/8 → 47/16 | note:G4 ]", + "[ 47/16 → 3/1 | note:A4 ]", + "[ 3/1 → 49/16 | note:C4 ]", + "[ 49/16 → 25/8 | note:C4 ]", + "[ 25/8 → 51/16 | note:C4 ]", + "[ 51/16 → 13/4 | note:D4 ]", + "[ 13/4 → 53/16 | note:F3 ]", + "[ 53/16 → 27/8 | note:F3 ]", + "[ 27/8 → 55/16 | note:F3 ]", + "[ 55/16 → 7/2 | note:F3 ]", + "[ 7/2 → 57/16 | note:E3 ]", + "[ 57/16 → 29/8 | note:E3 ]", + "[ 29/8 → 59/16 | note:F3 ]", + "[ 59/16 → 15/4 | note:F3 ]", + "[ 15/4 → 61/16 | note:F3 ]", + "[ 61/16 → 31/8 | note:F3 ]", + "[ 31/8 → 63/16 | note:F3 ]", + "[ 63/16 → 4/1 | note:G3 ]", ] `; @@ -1524,44 +1524,44 @@ exports[`runs examples > example "brand" example index 0 1`] = ` [ "[ 0/1 → 1/10 | s:hh pan:true ]", "[ 1/10 → 1/5 | s:hh pan:true ]", - "[ 1/5 → 3/10 | s:hh pan:true ]", - "[ 3/10 → 2/5 | s:hh pan:false ]", - "[ 2/5 → 1/2 | s:hh pan:true ]", + "[ 1/5 → 3/10 | s:hh pan:false ]", + "[ 3/10 → 2/5 | s:hh pan:true ]", + "[ 2/5 → 1/2 | s:hh pan:false ]", "[ 1/2 → 3/5 | s:hh pan:true ]", - "[ 3/5 → 7/10 | s:hh pan:false ]", - "[ 7/10 → 4/5 | s:hh pan:false ]", - "[ 4/5 → 9/10 | s:hh pan:false ]", - "[ 9/10 → 1/1 | s:hh pan:true ]", + "[ 3/5 → 7/10 | s:hh pan:true ]", + "[ 7/10 → 4/5 | s:hh pan:true ]", + "[ 4/5 → 9/10 | s:hh pan:true ]", + "[ 9/10 → 1/1 | s:hh pan:false ]", "[ 1/1 → 11/10 | s:hh pan:true ]", - "[ 11/10 → 6/5 | s:hh pan:false ]", + "[ 11/10 → 6/5 | s:hh pan:true ]", "[ 6/5 → 13/10 | s:hh pan:false ]", "[ 13/10 → 7/5 | s:hh pan:true ]", - "[ 7/5 → 3/2 | s:hh pan:true ]", + "[ 7/5 → 3/2 | s:hh pan:false ]", "[ 3/2 → 8/5 | s:hh pan:true ]", "[ 8/5 → 17/10 | s:hh pan:false ]", - "[ 17/10 → 9/5 | s:hh pan:false ]", - "[ 9/5 → 19/10 | s:hh pan:true ]", + "[ 17/10 → 9/5 | s:hh pan:true ]", + "[ 9/5 → 19/10 | s:hh pan:false ]", "[ 19/10 → 2/1 | s:hh pan:true ]", "[ 2/1 → 21/10 | s:hh pan:false ]", "[ 21/10 → 11/5 | s:hh pan:true ]", "[ 11/5 → 23/10 | s:hh pan:false ]", "[ 23/10 → 12/5 | s:hh pan:true ]", - "[ 12/5 → 5/2 | s:hh pan:true ]", - "[ 5/2 → 13/5 | s:hh pan:true ]", + "[ 12/5 → 5/2 | s:hh pan:false ]", + "[ 5/2 → 13/5 | s:hh pan:false ]", "[ 13/5 → 27/10 | s:hh pan:false ]", "[ 27/10 → 14/5 | s:hh pan:true ]", - "[ 14/5 → 29/10 | s:hh pan:true ]", - "[ 29/10 → 3/1 | s:hh pan:true ]", - "[ 3/1 → 31/10 | s:hh pan:true ]", - "[ 31/10 → 16/5 | s:hh pan:false ]", + "[ 14/5 → 29/10 | s:hh pan:false ]", + "[ 29/10 → 3/1 | s:hh pan:false ]", + "[ 3/1 → 31/10 | s:hh pan:false ]", + "[ 31/10 → 16/5 | s:hh pan:true ]", "[ 16/5 → 33/10 | s:hh pan:true ]", "[ 33/10 → 17/5 | s:hh pan:false ]", - "[ 17/5 → 7/2 | s:hh pan:false ]", + "[ 17/5 → 7/2 | s:hh pan:true ]", "[ 7/2 → 18/5 | s:hh pan:true ]", - "[ 18/5 → 37/10 | s:hh pan:false ]", + "[ 18/5 → 37/10 | s:hh pan:true ]", "[ 37/10 → 19/5 | s:hh pan:true ]", "[ 19/5 → 39/10 | s:hh pan:false ]", - "[ 39/10 → 4/1 | s:hh pan:false ]", + "[ 39/10 → 4/1 | s:hh pan:true ]", ] `; @@ -1581,32 +1581,32 @@ exports[`runs examples > example "brandBy" example index 0 1`] = ` "[ 11/10 → 6/5 | s:hh pan:false ]", "[ 6/5 → 13/10 | s:hh pan:false ]", "[ 13/10 → 7/5 | s:hh pan:true ]", - "[ 7/5 → 3/2 | s:hh pan:true ]", - "[ 3/2 → 8/5 | s:hh pan:false ]", + "[ 7/5 → 3/2 | s:hh pan:false ]", + "[ 3/2 → 8/5 | s:hh pan:true ]", "[ 8/5 → 17/10 | s:hh pan:false ]", "[ 17/10 → 9/5 | s:hh pan:false ]", "[ 9/5 → 19/10 | s:hh pan:false ]", - "[ 19/10 → 2/1 | s:hh pan:true ]", + "[ 19/10 → 2/1 | s:hh pan:false ]", "[ 2/1 → 21/10 | s:hh pan:false ]", "[ 21/10 → 11/5 | s:hh pan:true ]", "[ 11/5 → 23/10 | s:hh pan:false ]", - "[ 23/10 → 12/5 | s:hh pan:true ]", + "[ 23/10 → 12/5 | s:hh pan:false ]", "[ 12/5 → 5/2 | s:hh pan:false ]", "[ 5/2 → 13/5 | s:hh pan:false ]", "[ 13/5 → 27/10 | s:hh pan:false ]", - "[ 27/10 → 14/5 | s:hh pan:true ]", - "[ 14/5 → 29/10 | s:hh pan:true ]", + "[ 27/10 → 14/5 | s:hh pan:false ]", + "[ 14/5 → 29/10 | s:hh pan:false ]", "[ 29/10 → 3/1 | s:hh pan:false ]", "[ 3/1 → 31/10 | s:hh pan:false ]", - "[ 31/10 → 16/5 | s:hh pan:false ]", - "[ 16/5 → 33/10 | s:hh pan:true ]", + "[ 31/10 → 16/5 | s:hh pan:true ]", + "[ 16/5 → 33/10 | s:hh pan:false ]", "[ 33/10 → 17/5 | s:hh pan:false ]", "[ 17/5 → 7/2 | s:hh pan:false ]", "[ 7/2 → 18/5 | s:hh pan:false ]", "[ 18/5 → 37/10 | s:hh pan:false ]", - "[ 37/10 → 19/5 | s:hh pan:true ]", + "[ 37/10 → 19/5 | s:hh pan:false ]", "[ 19/5 → 39/10 | s:hh pan:false ]", - "[ 39/10 → 4/1 | s:hh pan:false ]", + "[ 39/10 → 4/1 | s:hh pan:true ]", ] `; @@ -1738,23 +1738,23 @@ exports[`runs examples > example "channels" example index 0 1`] = ` exports[`runs examples > example "choose" example index 0 1`] = ` [ "[ 0/1 → 1/5 | note:c2 s:triangle ]", - "[ 1/5 → 2/5 | note:g2 s:sine ]", + "[ 1/5 → 2/5 | note:g2 s:triangle ]", "[ 2/5 → 3/5 | note:g2 s:triangle ]", - "[ 3/5 → 4/5 | note:d2 s:bd n:6 ]", - "[ 4/5 → 1/1 | note:f1 s:bd n:6 ]", + "[ 3/5 → 4/5 | note:d2 s:sine ]", + "[ 4/5 → 1/1 | note:f1 s:triangle ]", "[ 1/1 → 6/5 | note:c2 s:sine ]", - "[ 6/5 → 7/5 | note:g2 s:bd n:6 ]", - "[ 7/5 → 8/5 | note:g2 s:sine ]", - "[ 8/5 → 9/5 | note:d2 s:bd n:6 ]", - "[ 9/5 → 2/1 | note:f1 s:sine ]", + "[ 6/5 → 7/5 | note:g2 s:triangle ]", + "[ 7/5 → 8/5 | note:g2 s:bd n:6 ]", + "[ 8/5 → 9/5 | note:d2 s:triangle ]", + "[ 9/5 → 2/1 | note:f1 s:triangle ]", "[ 2/1 → 11/5 | note:c2 s:triangle ]", "[ 11/5 → 12/5 | note:g2 s:bd n:6 ]", - "[ 12/5 → 13/5 | note:g2 s:sine ]", + "[ 12/5 → 13/5 | note:g2 s:bd n:6 ]", "[ 13/5 → 14/5 | note:d2 s:bd n:6 ]", - "[ 14/5 → 3/1 | note:f1 s:sine ]", + "[ 14/5 → 3/1 | note:f1 s:bd n:6 ]", "[ 3/1 → 16/5 | note:c2 s:triangle ]", - "[ 16/5 → 17/5 | note:g2 s:sine ]", - "[ 17/5 → 18/5 | note:g2 s:bd n:6 ]", + "[ 16/5 → 17/5 | note:g2 s:triangle ]", + "[ 17/5 → 18/5 | note:g2 s:sine ]", "[ 18/5 → 19/5 | note:d2 s:triangle ]", "[ 19/5 → 4/1 | note:f1 s:bd n:6 ]", ] @@ -1767,33 +1767,33 @@ exports[`runs examples > example "chooseCycles" example index 0 1`] = ` "[ 1/4 → 3/8 | s:hh ]", "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:bd ]", - "[ 5/8 → 3/4 | s:hh ]", - "[ 3/4 → 7/8 | s:sd ]", - "[ 7/8 → 1/1 | s:bd ]", - "[ 1/1 → 9/8 | s:sd ]", - "[ 9/8 → 5/4 | s:sd ]", - "[ 5/4 → 11/8 | s:hh ]", - "[ 11/8 → 3/2 | s:bd ]", - "[ 3/2 → 13/8 | s:bd ]", + "[ 5/8 → 3/4 | s:sd ]", + "[ 3/4 → 7/8 | s:bd ]", + "[ 7/8 → 1/1 | s:sd ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 9/8 → 5/4 | s:hh ]", + "[ 5/4 → 11/8 | s:sd ]", + "[ 11/8 → 3/2 | s:sd ]", + "[ 3/2 → 13/8 | s:sd ]", "[ 13/8 → 7/4 | s:bd ]", - "[ 7/4 → 15/8 | s:sd ]", - "[ 15/8 → 2/1 | s:hh ]", - "[ 2/1 → 17/8 | s:sd ]", + "[ 7/4 → 15/8 | s:bd ]", + "[ 15/8 → 2/1 | s:bd ]", + "[ 2/1 → 17/8 | s:bd ]", "[ 17/8 → 9/4 | s:sd ]", - "[ 9/4 → 19/8 | s:bd ]", - "[ 19/8 → 5/2 | s:hh ]", + "[ 9/4 → 19/8 | s:sd ]", + "[ 19/8 → 5/2 | s:sd ]", "[ 5/2 → 21/8 | s:bd ]", - "[ 21/8 → 11/4 | s:sd ]", - "[ 11/4 → 23/8 | s:hh ]", - "[ 23/8 → 3/1 | s:bd ]", - "[ 3/1 → 25/8 | s:sd ]", + "[ 21/8 → 11/4 | s:hh ]", + "[ 11/4 → 23/8 | s:sd ]", + "[ 23/8 → 3/1 | s:sd ]", + "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:sd ]", - "[ 13/4 → 27/8 | s:sd ]", + "[ 13/4 → 27/8 | s:hh ]", "[ 27/8 → 7/2 | s:bd ]", "[ 7/2 → 29/8 | s:sd ]", - "[ 29/8 → 15/4 | s:bd ]", + "[ 29/8 → 15/4 | s:hh ]", "[ 15/4 → 31/8 | s:bd ]", - "[ 31/8 → 4/1 | s:bd ]", + "[ 31/8 → 4/1 | s:sd ]", ] `; @@ -1804,33 +1804,33 @@ exports[`runs examples > example "chooseCycles" example index 1 1`] = ` "[ 1/4 → 3/8 | s:hh ]", "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:bd ]", - "[ 5/8 → 3/4 | s:hh ]", - "[ 3/4 → 7/8 | s:sd ]", - "[ 7/8 → 1/1 | s:bd ]", - "[ 1/1 → 9/8 | s:sd ]", - "[ 9/8 → 5/4 | s:sd ]", - "[ 5/4 → 11/8 | s:hh ]", - "[ 11/8 → 3/2 | s:bd ]", - "[ 3/2 → 13/8 | s:bd ]", + "[ 5/8 → 3/4 | s:sd ]", + "[ 3/4 → 7/8 | s:bd ]", + "[ 7/8 → 1/1 | s:sd ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 9/8 → 5/4 | s:hh ]", + "[ 5/4 → 11/8 | s:sd ]", + "[ 11/8 → 3/2 | s:sd ]", + "[ 3/2 → 13/8 | s:sd ]", "[ 13/8 → 7/4 | s:bd ]", - "[ 7/4 → 15/8 | s:sd ]", - "[ 15/8 → 2/1 | s:hh ]", - "[ 2/1 → 17/8 | s:sd ]", + "[ 7/4 → 15/8 | s:bd ]", + "[ 15/8 → 2/1 | s:bd ]", + "[ 2/1 → 17/8 | s:bd ]", "[ 17/8 → 9/4 | s:sd ]", - "[ 9/4 → 19/8 | s:bd ]", - "[ 19/8 → 5/2 | s:hh ]", + "[ 9/4 → 19/8 | s:sd ]", + "[ 19/8 → 5/2 | s:sd ]", "[ 5/2 → 21/8 | s:bd ]", - "[ 21/8 → 11/4 | s:sd ]", - "[ 11/4 → 23/8 | s:hh ]", - "[ 23/8 → 3/1 | s:bd ]", - "[ 3/1 → 25/8 | s:sd ]", + "[ 21/8 → 11/4 | s:hh ]", + "[ 11/4 → 23/8 | s:sd ]", + "[ 23/8 → 3/1 | s:sd ]", + "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:sd ]", - "[ 13/4 → 27/8 | s:sd ]", + "[ 13/4 → 27/8 | s:hh ]", "[ 27/8 → 7/2 | s:bd ]", "[ 7/2 → 29/8 | s:sd ]", - "[ 29/8 → 15/4 | s:bd ]", + "[ 29/8 → 15/4 | s:hh ]", "[ 15/4 → 31/8 | s:bd ]", - "[ 31/8 → 4/1 | s:bd ]", + "[ 31/8 → 4/1 | s:sd ]", ] `; @@ -2494,45 +2494,44 @@ exports[`runs examples > example "decay" example index 0 1`] = ` exports[`runs examples > example "degrade" example index 0 1`] = ` [ "[ 1/8 → 1/4 | s:hh ]", - "[ 1/4 → 3/8 | s:hh ]", "[ 3/8 → 1/2 | s:hh ]", "[ 5/8 → 3/4 | s:hh ]", - "[ 3/4 → 7/8 | s:hh ]", "[ 7/8 → 1/1 | s:hh ]", - "[ 9/8 → 5/4 | s:hh ]", "[ 5/4 → 11/8 | s:hh ]", - "[ 11/8 → 3/2 | s:hh ]", - "[ 7/4 → 15/8 | s:hh ]", + "[ 13/8 → 7/4 | s:hh ]", "[ 15/8 → 2/1 | s:hh ]", "[ 2/1 → 17/8 | s:hh ]", - "[ 17/8 → 9/4 | s:hh ]", - "[ 9/4 → 19/8 | s:hh ]", "[ 19/8 → 5/2 | s:hh ]", - "[ 11/4 → 23/8 | s:hh ]", + "[ 5/2 → 21/8 | s:hh ]", "[ 23/8 → 3/1 | s:hh ]", + "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", - "[ 27/8 → 7/2 | s:hh ]", + "[ 13/4 → 27/8 | s:hh ]", "[ 29/8 → 15/4 | s:hh ]", + "[ 15/4 → 31/8 | s:hh ]", ] `; exports[`runs examples > example "degrade" example index 1 1`] = ` [ "[ 1/4 → 3/8 | s:hh ]", - "[ 3/4 → 7/8 | s:hh ]", + "[ 3/8 → 1/2 | s:hh ]", + "[ 5/8 → 3/4 | s:hh ]", + "[ 7/8 → 1/1 | s:hh ]", "[ 1/1 → 9/8 | s:hh ]", "[ 9/8 → 5/4 | s:hh ]", "[ 5/4 → 11/8 | s:hh ]", - "[ 7/4 → 15/8 | s:hh ]", - "[ 15/8 → 2/1 | s:hh ]", - "[ 2/1 → 17/8 | s:hh ]", + "[ 11/8 → 3/2 | s:hh ]", + "[ 3/2 → 13/8 | s:hh ]", "[ 17/8 → 9/4 | s:hh ]", + "[ 9/4 → 19/8 | s:hh ]", "[ 19/8 → 5/2 | s:hh ]", - "[ 21/8 → 11/4 | s:hh ]", + "[ 11/4 → 23/8 | s:hh ]", + "[ 23/8 → 3/1 | s:hh ]", "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", - "[ 13/4 → 27/8 | s:hh ]", "[ 7/2 → 29/8 | s:hh ]", + "[ 31/8 → 4/1 | s:hh ]", ] `; @@ -2540,16 +2539,12 @@ exports[`runs examples > example "degradeBy" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh ]", "[ 1/8 → 1/4 | s:hh ]", - "[ 1/4 → 3/8 | s:hh ]", "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:hh ]", "[ 5/8 → 3/4 | s:hh ]", "[ 3/4 → 7/8 | s:hh ]", "[ 7/8 → 1/1 | s:hh ]", - "[ 9/8 → 5/4 | s:hh ]", "[ 5/4 → 11/8 | s:hh ]", - "[ 11/8 → 3/2 | s:hh ]", - "[ 3/2 → 13/8 | s:hh ]", "[ 13/8 → 7/4 | s:hh ]", "[ 7/4 → 15/8 | s:hh ]", "[ 15/8 → 2/1 | s:hh ]", @@ -2558,14 +2553,15 @@ exports[`runs examples > example "degradeBy" example index 0 1`] = ` "[ 9/4 → 19/8 | s:hh ]", "[ 19/8 → 5/2 | s:hh ]", "[ 5/2 → 21/8 | s:hh ]", + "[ 21/8 → 11/4 | s:hh ]", "[ 11/4 → 23/8 | s:hh ]", "[ 23/8 → 3/1 | s:hh ]", "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", "[ 13/4 → 27/8 | s:hh ]", - "[ 27/8 → 7/2 | s:hh ]", "[ 7/2 → 29/8 | s:hh ]", "[ 29/8 → 15/4 | s:hh ]", + "[ 15/4 → 31/8 | s:hh ]", ] `; @@ -2574,7 +2570,6 @@ exports[`runs examples > example "degradeBy" example index 1 1`] = ` "[ 0/1 → 1/8 | s:hh ]", "[ 1/4 → 3/8 | s:hh ]", "[ 3/8 → 1/2 | s:hh ]", - "[ 1/2 → 5/8 | s:hh ]", "[ 5/8 → 3/4 | s:hh ]", "[ 3/4 → 7/8 | s:hh ]", "[ 7/8 → 1/1 | s:hh ]", @@ -2583,66 +2578,56 @@ exports[`runs examples > example "degradeBy" example index 1 1`] = ` "[ 5/4 → 11/8 | s:hh ]", "[ 11/8 → 3/2 | s:hh ]", "[ 3/2 → 13/8 | s:hh ]", - "[ 13/8 → 7/4 | s:hh ]", - "[ 7/4 → 15/8 | s:hh ]", "[ 15/8 → 2/1 | s:hh ]", - "[ 2/1 → 17/8 | s:hh ]", "[ 17/8 → 9/4 | s:hh ]", + "[ 9/4 → 19/8 | s:hh ]", "[ 19/8 → 5/2 | s:hh ]", - "[ 5/2 → 21/8 | s:hh ]", "[ 21/8 → 11/4 | s:hh ]", "[ 11/4 → 23/8 | s:hh ]", "[ 23/8 → 3/1 | s:hh ]", "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", "[ 13/4 → 27/8 | s:hh ]", - "[ 27/8 → 7/2 | s:hh ]", "[ 7/2 → 29/8 | s:hh ]", + "[ 29/8 → 15/4 | s:hh ]", + "[ 31/8 → 4/1 | s:hh ]", ] `; exports[`runs examples > example "degradeBy" example index 2 1`] = ` [ - "[ 0/1 → 1/16 | s:bd ]", "[ 1/16 → 1/8 | s:bd ]", "[ 1/8 → 3/16 | s:bd ]", + "[ 3/16 → 1/4 | s:bd ]", + "[ 5/16 → 3/8 | s:bd ]", "[ 3/8 → 7/16 | s:bd ]", "[ 1/2 → 9/16 | s:bd ]", - "[ 9/16 → 5/8 | s:bd ]", - "[ 5/8 → 11/16 | s:bd ]", - "[ 11/16 → 3/4 | s:bd ]", - "[ 3/4 → 13/16 | s:bd ]", - "[ 15/16 → 1/1 | s:bd ]", - "[ 1/1 → 17/16 | s:bd ]", + "[ 13/16 → 7/8 | s:bd ]", + "[ 7/8 → 15/16 | s:bd ]", "[ 17/16 → 9/8 | s:bd ]", "[ 9/8 → 19/16 | s:bd ]", + "[ 19/16 → 5/4 | s:bd ]", + "[ 21/16 → 11/8 | s:bd ]", "[ 11/8 → 23/16 | s:bd ]", "[ 3/2 → 25/16 | s:bd ]", - "[ 25/16 → 13/8 | s:bd ]", - "[ 13/8 → 27/16 | s:bd ]", - "[ 27/16 → 7/4 | s:bd ]", - "[ 7/4 → 29/16 | s:bd ]", - "[ 31/16 → 2/1 | s:bd ]", - "[ 2/1 → 33/16 | s:bd ]", + "[ 29/16 → 15/8 | s:bd ]", + "[ 15/8 → 31/16 | s:bd ]", "[ 33/16 → 17/8 | s:bd ]", "[ 17/8 → 35/16 | s:bd ]", + "[ 35/16 → 9/4 | s:bd ]", + "[ 37/16 → 19/8 | s:bd ]", "[ 19/8 → 39/16 | s:bd ]", "[ 5/2 → 41/16 | s:bd ]", - "[ 41/16 → 21/8 | s:bd ]", - "[ 21/8 → 43/16 | s:bd ]", - "[ 43/16 → 11/4 | s:bd ]", - "[ 11/4 → 45/16 | s:bd ]", - "[ 47/16 → 3/1 | s:bd ]", - "[ 3/1 → 49/16 | s:bd ]", + "[ 45/16 → 23/8 | s:bd ]", + "[ 23/8 → 47/16 | s:bd ]", "[ 49/16 → 25/8 | s:bd ]", "[ 25/8 → 51/16 | s:bd ]", + "[ 51/16 → 13/4 | s:bd ]", + "[ 53/16 → 27/8 | s:bd ]", "[ 27/8 → 55/16 | s:bd ]", "[ 7/2 → 57/16 | s:bd ]", - "[ 57/16 → 29/8 | s:bd ]", - "[ 29/8 → 59/16 | s:bd ]", - "[ 59/16 → 15/4 | s:bd ]", - "[ 15/4 → 61/16 | s:bd ]", - "[ 63/16 → 4/1 | s:bd ]", + "[ 61/16 → 31/8 | s:bd ]", + "[ 31/8 → 63/16 | s:bd ]", ] `; @@ -2965,14 +2950,14 @@ exports[`runs examples > example "distorttype" example index 0 1`] = ` exports[`runs examples > example "distorttype" example index 1 1`] = ` [ - "[ (0/1 → 1/2) ⇝ 1/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1 distorttype:fold ]", - "[ 0/1 ⇜ (1/2 → 1/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1 distorttype:fold ]", - "[ (1/1 → 3/2) ⇝ 2/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:4.6367951557040215 distorttype:chebyshev ]", - "[ 1/1 ⇜ (3/2 → 2/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:4.6367951557040215 distorttype:chebyshev ]", - "[ (2/1 → 5/2) ⇝ 3/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:7.716689839959145 distorttype:scurve ]", - "[ 2/1 ⇜ (5/2 → 3/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:7.716689839959145 distorttype:scurve ]", - "[ (3/1 → 7/2) ⇝ 4/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:2.5210237745195627 distorttype:diode ]", - "[ 3/1 ⇜ (7/2 → 4/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:2.5210237745195627 distorttype:diode ]", + "[ (0/1 → 1/2) ⇝ 1/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:3.71662513515912 distorttype:fold ]", + "[ 0/1 ⇜ (1/2 → 1/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:3.71662513515912 distorttype:fold ]", + "[ (1/1 → 3/2) ⇝ 2/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1.8276505067478865 distorttype:chebyshev ]", + "[ 1/1 ⇜ (3/2 → 2/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1.8276505067478865 distorttype:chebyshev ]", + "[ (2/1 → 5/2) ⇝ 3/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:5.068953953683376 distorttype:scurve ]", + "[ 2/1 ⇜ (5/2 → 3/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:5.068953953683376 distorttype:scurve ]", + "[ (3/1 → 7/2) ⇝ 4/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:5.303743980126455 distorttype:diode ]", + "[ 3/1 ⇜ (7/2 → 4/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:5.303743980126455 distorttype:diode ]", ] `; @@ -2999,38 +2984,38 @@ exports[`runs examples > example "distortvol" example index 0 1`] = ` exports[`runs examples > example "djf" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | note:D3 s:supersaw djf:0.5 ]", + "[ 0/1 → 1/8 | note:C4 s:supersaw djf:0.5 ]", "[ 1/8 → 1/4 | note:G4 s:supersaw djf:0.5 ]", - "[ 1/4 → 3/8 | note:Bb3 s:supersaw djf:0.5 ]", - "[ 3/8 → 1/2 | note:C4 s:supersaw djf:0.5 ]", - "[ 1/2 → 5/8 | note:A3 s:supersaw djf:0.5 ]", - "[ 5/8 → 3/4 | note:F3 s:supersaw djf:0.5 ]", - "[ 3/4 → 7/8 | note:G3 s:supersaw djf:0.5 ]", - "[ 7/8 → 1/1 | note:C4 s:supersaw djf:0.5 ]", - "[ 1/1 → 9/8 | note:Eb4 s:supersaw djf:0.3 ]", - "[ 9/8 → 5/4 | note:G4 s:supersaw djf:0.3 ]", - "[ 5/4 → 11/8 | note:A4 s:supersaw djf:0.3 ]", - "[ 11/8 → 3/2 | note:F3 s:supersaw djf:0.3 ]", - "[ 3/2 → 13/8 | note:F4 s:supersaw djf:0.3 ]", - "[ 13/8 → 7/4 | note:D4 s:supersaw djf:0.3 ]", - "[ 7/4 → 15/8 | note:G3 s:supersaw djf:0.3 ]", - "[ 15/8 → 2/1 | note:F4 s:supersaw djf:0.3 ]", - "[ 2/1 → 17/8 | note:Eb5 s:supersaw djf:0.2 ]", - "[ 17/8 → 9/4 | note:D5 s:supersaw djf:0.2 ]", - "[ 9/4 → 19/8 | note:Bb3 s:supersaw djf:0.2 ]", + "[ 1/4 → 3/8 | note:F3 s:supersaw djf:0.5 ]", + "[ 3/8 → 1/2 | note:C5 s:supersaw djf:0.5 ]", + "[ 1/2 → 5/8 | note:Bb3 s:supersaw djf:0.5 ]", + "[ 5/8 → 3/4 | note:A4 s:supersaw djf:0.5 ]", + "[ 3/4 → 7/8 | note:A3 s:supersaw djf:0.5 ]", + "[ 7/8 → 1/1 | note:D5 s:supersaw djf:0.5 ]", + "[ 1/1 → 9/8 | note:Eb3 s:supersaw djf:0.3 ]", + "[ 9/8 → 5/4 | note:D3 s:supersaw djf:0.3 ]", + "[ 5/4 → 11/8 | note:Eb5 s:supersaw djf:0.3 ]", + "[ 11/8 → 3/2 | note:D3 s:supersaw djf:0.3 ]", + "[ 3/2 → 13/8 | note:F3 s:supersaw djf:0.3 ]", + "[ 13/8 → 7/4 | note:G4 s:supersaw djf:0.3 ]", + "[ 7/4 → 15/8 | note:Bb3 s:supersaw djf:0.3 ]", + "[ 15/8 → 2/1 | note:G4 s:supersaw djf:0.3 ]", + "[ 2/1 → 17/8 | note:F4 s:supersaw djf:0.2 ]", + "[ 17/8 → 9/4 | note:D4 s:supersaw djf:0.2 ]", + "[ 9/4 → 19/8 | note:G3 s:supersaw djf:0.2 ]", "[ 19/8 → 5/2 | note:C5 s:supersaw djf:0.2 ]", - "[ 5/2 → 21/8 | note:D4 s:supersaw djf:0.2 ]", - "[ 21/8 → 11/4 | note:F3 s:supersaw djf:0.2 ]", - "[ 11/4 → 23/8 | note:G4 s:supersaw djf:0.2 ]", - "[ 23/8 → 3/1 | note:D3 s:supersaw djf:0.2 ]", - "[ 3/1 → 25/8 | note:G3 s:supersaw djf:0.75 ]", - "[ 25/8 → 13/4 | note:Bb3 s:supersaw djf:0.75 ]", - "[ 13/4 → 27/8 | note:Eb5 s:supersaw djf:0.75 ]", - "[ 27/8 → 7/2 | note:C4 s:supersaw djf:0.75 ]", - "[ 7/2 → 29/8 | note:C4 s:supersaw djf:0.75 ]", - "[ 29/8 → 15/4 | note:Eb5 s:supersaw djf:0.75 ]", - "[ 15/4 → 31/8 | note:Bb4 s:supersaw djf:0.75 ]", - "[ 31/8 → 4/1 | note:A4 s:supersaw djf:0.75 ]", + "[ 5/2 → 21/8 | note:A4 s:supersaw djf:0.2 ]", + "[ 21/8 → 11/4 | note:Bb3 s:supersaw djf:0.2 ]", + "[ 11/4 → 23/8 | note:Bb3 s:supersaw djf:0.2 ]", + "[ 23/8 → 3/1 | note:C5 s:supersaw djf:0.2 ]", + "[ 3/1 → 25/8 | note:F4 s:supersaw djf:0.75 ]", + "[ 25/8 → 13/4 | note:F4 s:supersaw djf:0.75 ]", + "[ 13/4 → 27/8 | note:D5 s:supersaw djf:0.75 ]", + "[ 27/8 → 7/2 | note:D3 s:supersaw djf:0.75 ]", + "[ 7/2 → 29/8 | note:G3 s:supersaw djf:0.75 ]", + "[ 29/8 → 15/4 | note:F4 s:supersaw djf:0.75 ]", + "[ 15/4 → 31/8 | note:F4 s:supersaw djf:0.75 ]", + "[ 31/8 → 4/1 | note:F3 s:supersaw djf:0.75 ]", ] `; @@ -5189,31 +5174,31 @@ exports[`runs examples > example "invert" example index 0 1`] = ` exports[`runs examples > example "irand" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:F3 ]", - "[ 1/4 → 3/8 | note:G3 ]", - "[ 3/8 → 1/2 | note:G3 ]", - "[ 1/2 → 3/4 | note:F3 ]", - "[ 3/4 → 5/6 | note:Ab3 ]", - "[ 5/6 → 11/12 | note:Bb3 ]", - "[ 11/12 → 1/1 | note:C4 ]", + "[ 1/4 → 3/8 | note:D3 ]", + "[ 3/8 → 1/2 | note:Bb3 ]", + "[ 1/2 → 3/4 | note:Eb3 ]", + "[ 3/4 → 5/6 | note:Eb3 ]", + "[ 5/6 → 11/12 | note:Ab3 ]", + "[ 11/12 → 1/1 | note:D3 ]", "[ 1/1 → 5/4 | note:C3 ]", - "[ 5/4 → 11/8 | note:Ab3 ]", - "[ 11/8 → 3/2 | note:Bb3 ]", + "[ 5/4 → 11/8 | note:C4 ]", + "[ 11/8 → 3/2 | note:C3 ]", "[ 3/2 → 7/4 | note:D3 ]", - "[ 7/4 → 11/6 | note:Ab3 ]", - "[ 11/6 → 23/12 | note:Bb3 ]", - "[ 23/12 → 2/1 | note:C3 ]", + "[ 7/4 → 11/6 | note:Eb3 ]", + "[ 11/6 → 23/12 | note:C3 ]", + "[ 23/12 → 2/1 | note:Ab3 ]", "[ 2/1 → 9/4 | note:G3 ]", - "[ 9/4 → 19/8 | note:Ab3 ]", - "[ 19/8 → 5/2 | note:Ab3 ]", - "[ 5/2 → 11/4 | note:Eb3 ]", - "[ 11/4 → 17/6 | note:Ab3 ]", - "[ 17/6 → 35/12 | note:Bb3 ]", - "[ 35/12 → 3/1 | note:Ab3 ]", - "[ 3/1 → 13/4 | note:F3 ]", - "[ 13/4 → 27/8 | note:Eb3 ]", - "[ 27/8 → 7/2 | note:Ab3 ]", - "[ 7/2 → 15/4 | note:Eb3 ]", - "[ 15/4 → 23/6 | note:C3 ]", + "[ 9/4 → 19/8 | note:D3 ]", + "[ 19/8 → 5/2 | note:Bb3 ]", + "[ 5/2 → 11/4 | note:Ab3 ]", + "[ 11/4 → 17/6 | note:Eb3 ]", + "[ 17/6 → 35/12 | note:Ab3 ]", + "[ 35/12 → 3/1 | note:D3 ]", + "[ 3/1 → 13/4 | note:G3 ]", + "[ 13/4 → 27/8 | note:C4 ]", + "[ 27/8 → 7/2 | note:C3 ]", + "[ 7/2 → 15/4 | note:D3 ]", + "[ 15/4 → 23/6 | note:G3 ]", "[ 23/6 → 47/12 | note:Ab3 ]", "[ 47/12 → 4/1 | note:Ab3 ]", ] @@ -5221,38 +5206,38 @@ exports[`runs examples > example "irand" example index 0 1`] = ` exports[`runs examples > example "irbegin" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", "[ 1/8 → 1/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.5 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.0625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", "[ 19/8 → 5/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.75 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", ] `; @@ -5279,38 +5264,38 @@ exports[`runs examples > example "iresponse" example index 0 1`] = ` exports[`runs examples > example "irspeed" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", "[ 1/8 → 1/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.5 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.0625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", "[ 19/8 → 5/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.75 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", ] `; @@ -6998,35 +6983,35 @@ exports[`runs examples > example "offset" example index 0 1`] = ` exports[`runs examples > example "often" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh speed:0.5 ]", - "[ 1/8 → 1/4 | s:hh ]", + "[ 1/8 → 1/4 | s:hh speed:0.5 ]", "[ 1/4 → 3/8 | s:hh speed:0.5 ]", - "[ 3/8 → 1/2 | s:hh speed:0.5 ]", + "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:hh speed:0.5 ]", - "[ 5/8 → 3/4 | s:hh ]", + "[ 5/8 → 3/4 | s:hh speed:0.5 ]", "[ 3/4 → 7/8 | s:hh speed:0.5 ]", "[ 7/8 → 1/1 | s:hh ]", "[ 1/1 → 9/8 | s:hh speed:0.5 ]", - "[ 9/8 → 5/4 | s:hh ]", - "[ 5/4 → 11/8 | s:hh speed:0.5 ]", - "[ 11/8 → 3/2 | s:hh ]", + "[ 9/8 → 5/4 | s:hh speed:0.5 ]", + "[ 5/4 → 11/8 | s:hh ]", + "[ 11/8 → 3/2 | s:hh speed:0.5 ]", "[ 3/2 → 13/8 | s:hh speed:0.5 ]", "[ 13/8 → 7/4 | s:hh speed:0.5 ]", "[ 7/4 → 15/8 | s:hh speed:0.5 ]", - "[ 15/8 → 2/1 | s:hh ]", + "[ 15/8 → 2/1 | s:hh speed:0.5 ]", "[ 2/1 → 17/8 | s:hh speed:0.5 ]", - "[ 17/8 → 9/4 | s:hh ]", + "[ 17/8 → 9/4 | s:hh speed:0.5 ]", "[ 9/4 → 19/8 | s:hh speed:0.5 ]", - "[ 19/8 → 5/2 | s:hh speed:0.5 ]", + "[ 19/8 → 5/2 | s:hh ]", "[ 5/2 → 21/8 | s:hh speed:0.5 ]", "[ 21/8 → 11/4 | s:hh speed:0.5 ]", "[ 11/4 → 23/8 | s:hh speed:0.5 ]", "[ 23/8 → 3/1 | s:hh ]", "[ 3/1 → 25/8 | s:hh speed:0.5 ]", "[ 25/8 → 13/4 | s:hh speed:0.5 ]", - "[ 13/4 → 27/8 | s:hh speed:0.5 ]", + "[ 13/4 → 27/8 | s:hh ]", "[ 27/8 → 7/2 | s:hh speed:0.5 ]", "[ 7/2 → 29/8 | s:hh speed:0.5 ]", - "[ 29/8 → 15/4 | s:hh ]", + "[ 29/8 → 15/4 | s:hh speed:0.5 ]", "[ 15/4 → 31/8 | s:hh speed:0.5 ]", "[ 31/8 → 4/1 | s:hh speed:0.5 ]", ] @@ -7298,54 +7283,54 @@ exports[`runs examples > example "penv" example index 0 1`] = ` exports[`runs examples > example "perlin" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:hh cutoff:3801.944299018942 ]", - "[ 0/1 → 1/4 | s:bd cutoff:3801.944299018942 ]", - "[ 1/8 → 1/4 | s:hh cutoff:3752.180364182138 ]", - "[ 1/4 → 3/8 | s:hh cutoff:3481.0331450903504 ]", - "[ 1/4 → 1/2 | s:bd cutoff:3481.0331450903504 ]", - "[ 3/8 → 1/2 | s:hh cutoff:2948.767180467044 ]", - "[ 1/2 → 5/8 | s:hh cutoff:2251.8828762695193 ]", - "[ 1/2 → 3/4 | s:bd cutoff:2251.8828762695193 ]", - "[ 5/8 → 3/4 | s:hh cutoff:1554.9985720719947 ]", - "[ 3/4 → 7/8 | s:hh cutoff:1022.7326074486882 ]", - "[ 3/4 → 1/1 | s:bd cutoff:1022.7326074486882 ]", - "[ 7/8 → 1/1 | s:hh cutoff:751.5853883569008 ]", - "[ 1/1 → 9/8 | s:hh cutoff:701.8214535200968 ]", - "[ 1/1 → 5/4 | s:bd cutoff:701.8214535200968 ]", - "[ 9/8 → 5/4 | s:hh cutoff:758.9508705680288 ]", - "[ 5/4 → 11/8 | s:hh cutoff:1070.2301657379394 ]", - "[ 5/4 → 3/2 | s:bd cutoff:1070.2301657379394 ]", - "[ 11/8 → 3/2 | s:hh cutoff:1681.2759838209531 ]", - "[ 3/2 → 13/8 | s:hh cutoff:2481.3050446100533 ]", - "[ 3/2 → 7/4 | s:bd cutoff:2481.3050446100533 ]", - "[ 13/8 → 7/4 | s:hh cutoff:3281.3341053991535 ]", - "[ 7/4 → 15/8 | s:hh cutoff:3892.379923482167 ]", - "[ 7/4 → 2/1 | s:bd cutoff:3892.379923482167 ]", - "[ 15/8 → 2/1 | s:hh cutoff:4203.6592186520775 ]", - "[ 2/1 → 17/8 | s:hh cutoff:4260.78863570001 ]", - "[ 2/1 → 9/4 | s:bd cutoff:4260.78863570001 ]", - "[ 17/8 → 9/4 | s:hh cutoff:4250.943561981614 ]", - "[ 9/4 → 19/8 | s:hh cutoff:4197.301012025491 ]", - "[ 9/4 → 5/2 | s:bd cutoff:4197.301012025491 ]", - "[ 19/8 → 5/2 | s:hh cutoff:4091.9999003530734 ]", - "[ 5/2 → 21/8 | s:hh cutoff:3954.1314345551655 ]", - "[ 5/2 → 11/4 | s:bd cutoff:3954.1314345551655 ]", - "[ 21/8 → 11/4 | s:hh cutoff:3816.2629687572576 ]", - "[ 11/4 → 23/8 | s:hh cutoff:3710.9618570848397 ]", - "[ 11/4 → 3/1 | s:bd cutoff:3710.9618570848397 ]", - "[ 23/8 → 3/1 | s:hh cutoff:3657.3193071287164 ]", - "[ 3/1 → 25/8 | s:hh cutoff:3647.474233410321 ]", - "[ 3/1 → 13/4 | s:bd cutoff:3647.474233410321 ]", - "[ 25/8 → 13/4 | s:hh cutoff:3623.927675406968 ]", - "[ 13/4 → 27/8 | s:hh cutoff:3495.6302700122706 ]", - "[ 13/4 → 7/2 | s:bd cutoff:3495.6302700122706 ]", - "[ 27/8 → 7/2 | s:hh cutoff:3243.7805830790653 ]", - "[ 7/2 → 29/8 | s:hh cutoff:2914.039240393322 ]", - "[ 7/2 → 15/4 | s:bd cutoff:2914.039240393322 ]", - "[ 29/8 → 15/4 | s:hh cutoff:2584.2978977075786 ]", - "[ 15/4 → 31/8 | s:hh cutoff:2332.4482107743734 ]", - "[ 15/4 → 4/1 | s:bd cutoff:2332.4482107743734 ]", - "[ 31/8 → 4/1 | s:hh cutoff:2204.150805379676 ]", + "[ 0/1 → 1/8 | s:hh cutoff:3410.6697876704857 ]", + "[ 0/1 → 1/4 | s:bd cutoff:3410.6697876704857 ]", + "[ 1/8 → 1/4 | s:hh cutoff:3378.181624527514 ]", + "[ 1/4 → 3/8 | s:hh cutoff:3201.164370596416 ]", + "[ 1/4 → 1/2 | s:bd cutoff:3201.164370596416 ]", + "[ 3/8 → 1/2 | s:hh cutoff:2853.676907017785 ]", + "[ 1/2 → 5/8 | s:hh cutoff:2398.7190938787535 ]", + "[ 1/2 → 3/4 | s:bd cutoff:2398.7190938787535 ]", + "[ 5/8 → 3/4 | s:hh cutoff:1943.7612807397215 ]", + "[ 3/4 → 7/8 | s:hh cutoff:1596.2738171610908 ]", + "[ 3/4 → 1/1 | s:bd cutoff:1596.2738171610908 ]", + "[ 7/8 → 1/1 | s:hh cutoff:1419.2565632299932 ]", + "[ 1/1 → 9/8 | s:hh cutoff:1386.7684000870213 ]", + "[ 1/1 → 5/4 | s:bd cutoff:1386.7684000870213 ]", + "[ 9/8 → 5/4 | s:hh cutoff:1442.5150435813734 ]", + "[ 5/4 → 11/8 | s:hh cutoff:1746.2600630772158 ]", + "[ 5/4 → 3/2 | s:bd cutoff:1746.2600630772158 ]", + "[ 11/8 → 3/2 | s:hh cutoff:2342.5159876004573 ]", + "[ 3/2 → 13/8 | s:hh cutoff:3123.1809609453194 ]", + "[ 3/2 → 7/4 | s:bd cutoff:3123.1809609453194 ]", + "[ 13/8 → 7/4 | s:hh cutoff:3903.8459342901815 ]", + "[ 7/4 → 15/8 | s:hh cutoff:4500.101858813423 ]", + "[ 7/4 → 2/1 | s:bd cutoff:4500.101858813423 ]", + "[ 15/8 → 2/1 | s:hh cutoff:4803.846878309266 ]", + "[ 2/1 → 17/8 | s:hh cutoff:4859.5935218036175 ]", + "[ 2/1 → 9/4 | s:bd cutoff:4859.5935218036175 ]", + "[ 17/8 → 9/4 | s:hh cutoff:4863.631636751641 ]", + "[ 9/4 → 19/8 | s:hh cutoff:4885.633989301141 ]", + "[ 9/4 → 5/2 | s:bd cutoff:4885.633989301141 ]", + "[ 19/8 → 5/2 | s:hh cutoff:4928.824929790842 ]", + "[ 5/2 → 21/8 | s:hh cutoff:4985.37389311241 ]", + "[ 5/2 → 11/4 | s:bd cutoff:4985.37389311241 ]", + "[ 21/8 → 11/4 | s:hh cutoff:5041.922856433977 ]", + "[ 11/4 → 23/8 | s:hh cutoff:5085.113796923679 ]", + "[ 11/4 → 3/1 | s:bd cutoff:5085.113796923679 ]", + "[ 23/8 → 3/1 | s:hh cutoff:5107.116149473179 ]", + "[ 3/1 → 25/8 | s:hh cutoff:5111.154264421202 ]", + "[ 3/1 → 13/4 | s:bd cutoff:5111.154264421202 ]", + "[ 25/8 → 13/4 | s:hh cutoff:5054.286698772394 ]", + "[ 13/4 → 27/8 | s:hh cutoff:4744.434145256264 ]", + "[ 13/4 → 7/2 | s:bd cutoff:4744.434145256264 ]", + "[ 27/8 → 7/2 | s:hh cutoff:4136.189041947908 ]", + "[ 7/2 → 29/8 | s:hh cutoff:3339.826896379236 ]", + "[ 7/2 → 15/4 | s:bd cutoff:3339.826896379236 ]", + "[ 29/8 → 15/4 | s:hh cutoff:2543.464750810564 ]", + "[ 15/4 → 31/8 | s:hh cutoff:1935.2196475022083 ]", + "[ 15/4 → 4/1 | s:bd cutoff:1935.2196475022083 ]", + "[ 31/8 → 4/1 | s:hh cutoff:1625.3670939860783 ]", ] `; @@ -8167,54 +8152,54 @@ exports[`runs examples > example "queryArc" example index 0 1`] = `[]`; exports[`runs examples > example "rand" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:hh cutoff:3801.944299018942 ]", - "[ 0/1 → 1/4 | s:bd cutoff:3801.944299018942 ]", - "[ 1/8 → 1/4 | s:hh cutoff:6647.100417292677 ]", - "[ 1/4 → 3/8 | s:hh cutoff:4935.184325557202 ]", - "[ 1/4 → 1/2 | s:bd cutoff:4935.184325557202 ]", - "[ 3/8 → 1/2 | s:hh cutoff:4274.936107802205 ]", - "[ 1/2 → 5/8 | s:hh cutoff:3599.519268143922 ]", - "[ 1/2 → 3/4 | s:bd cutoff:3599.519268143922 ]", - "[ 5/8 → 3/4 | s:hh cutoff:7203.329019364901 ]", - "[ 3/4 → 7/8 | s:hh cutoff:5311.3214410841465 ]", - "[ 3/4 → 1/1 | s:bd cutoff:5311.3214410841465 ]", - "[ 7/8 → 1/1 | s:hh cutoff:7230.677842278965 ]", - "[ 1/1 → 9/8 | s:hh cutoff:701.8214535200968 ]", - "[ 1/1 → 5/4 | s:bd cutoff:701.8214535200968 ]", - "[ 9/8 → 5/4 | s:hh cutoff:6626.493136398494 ]", - "[ 5/4 → 11/8 | s:hh cutoff:5536.982340388931 ]", - "[ 5/4 → 3/2 | s:bd cutoff:5536.982340388931 ]", - "[ 11/8 → 3/2 | s:hh cutoff:6926.114265923388 ]", - "[ 3/2 → 13/8 | s:hh cutoff:2300.4843831295148 ]", - "[ 3/2 → 7/4 | s:bd cutoff:2300.4843831295148 ]", - "[ 13/8 → 7/4 | s:hh cutoff:2510.1786771556363 ]", - "[ 7/4 → 15/8 | s:hh cutoff:5990.686780540273 ]", - "[ 7/4 → 2/1 | s:bd cutoff:5990.686780540273 ]", - "[ 15/8 → 2/1 | s:hh cutoff:6187.015319708735 ]", - "[ 2/1 → 17/8 | s:hh cutoff:4260.78863570001 ]", - "[ 2/1 → 9/4 | s:bd cutoff:4260.78863570001 ]", - "[ 17/8 → 9/4 | s:hh cutoff:6301.981445983984 ]", - "[ 9/4 → 19/8 | s:hh cutoff:5212.422806769609 ]", - "[ 9/4 → 5/2 | s:bd cutoff:5212.422806769609 ]", - "[ 19/8 → 5/2 | s:hh cutoff:5561.523957410827 ]", - "[ 5/2 → 21/8 | s:hh cutoff:3020.9835229907185 ]", - "[ 5/2 → 11/4 | s:bd cutoff:3020.9835229907185 ]", - "[ 21/8 → 11/4 | s:hh cutoff:576.5502714784816 ]", - "[ 11/4 → 23/8 | s:hh cutoff:5743.939216597937 ]", - "[ 11/4 → 3/1 | s:bd cutoff:5743.939216597937 ]", - "[ 23/8 → 3/1 | s:hh cutoff:7271.8438868178055 ]", - "[ 3/1 → 25/8 | s:hh cutoff:3647.474233410321 ]", - "[ 3/1 → 13/4 | s:bd cutoff:3647.474233410321 ]", - "[ 25/8 → 13/4 | s:hh cutoff:5633.619675179943 ]", - "[ 13/4 → 27/8 | s:hh cutoff:2873.2354695675895 ]", - "[ 13/4 → 7/2 | s:bd cutoff:2873.2354695675895 ]", - "[ 27/8 → 7/2 | s:hh cutoff:5596.839588950388 ]", - "[ 7/2 → 29/8 | s:hh cutoff:2628.3173956908286 ]", - "[ 7/2 → 15/4 | s:bd cutoff:2628.3173956908286 ]", - "[ 29/8 → 15/4 | s:hh cutoff:7124.103914946318 ]", - "[ 15/4 → 31/8 | s:hh cutoff:814.1072567086667 ]", - "[ 15/4 → 4/1 | s:bd cutoff:814.1072567086667 ]", - "[ 31/8 → 4/1 | s:hh cutoff:518.1868247454986 ]", + "[ 0/1 → 1/8 | s:hh cutoff:3410.6697876704857 ]", + "[ 0/1 → 1/4 | s:bd cutoff:3410.6697876704857 ]", + "[ 1/8 → 1/4 | s:hh cutoff:5409.978067153133 ]", + "[ 1/4 → 3/8 | s:hh cutoff:1470.4432229045779 ]", + "[ 1/4 → 1/2 | s:bd cutoff:1470.4432229045779 ]", + "[ 3/8 → 1/2 | s:hh cutoff:6906.840303097852 ]", + "[ 1/2 → 5/8 | s:hh cutoff:2951.713348273188 ]", + "[ 1/2 → 3/4 | s:bd cutoff:2951.713348273188 ]", + "[ 5/8 → 3/4 | s:hh cutoff:5732.849565800279 ]", + "[ 3/4 → 7/8 | s:hh cutoff:2621.97931134142 ]", + "[ 3/4 → 1/1 | s:bd cutoff:2621.97931134142 ]", + "[ 7/8 → 1/1 | s:hh cutoff:7323.85477644857 ]", + "[ 1/1 → 9/8 | s:hh cutoff:1386.7684000870213 ]", + "[ 1/1 → 5/4 | s:bd cutoff:1386.7684000870213 ]", + "[ 9/8 → 5/4 | s:hh cutoff:968.4956459095702 ]", + "[ 5/4 → 11/8 | s:hh cutoff:7674.6532345423475 ]", + "[ 5/4 → 3/2 | s:bd cutoff:7674.6532345423475 ]", + "[ 11/8 → 3/2 | s:hh cutoff:863.651579245925 ]", + "[ 3/2 → 13/8 | s:hh cutoff:1476.3008129084483 ]", + "[ 3/2 → 7/4 | s:bd cutoff:1476.3008129084483 ]", + "[ 13/8 → 7/4 | s:hh cutoff:5232.728436938487 ]", + "[ 7/4 → 15/8 | s:hh cutoff:3057.402160600759 ]", + "[ 7/4 → 2/1 | s:bd cutoff:3057.402160600759 ]", + "[ 15/8 → 2/1 | s:hh cutoff:5307.644687825814 ]", + "[ 2/1 → 17/8 | s:hh cutoff:4859.5935218036175 ]", + "[ 2/1 → 9/4 | s:bd cutoff:4859.5935218036175 ]", + "[ 17/8 → 9/4 | s:hh cutoff:3798.551473999396 ]", + "[ 9/4 → 19/8 | s:hh cutoff:2020.6333762034774 ]", + "[ 9/4 → 5/2 | s:bd cutoff:2020.6333762034774 ]", + "[ 19/8 → 5/2 | s:hh cutoff:6903.368854080327 ]", + "[ 5/2 → 21/8 | s:hh cutoff:5992.341757635586 ]", + "[ 5/2 → 11/4 | s:bd cutoff:5992.341757635586 ]", + "[ 21/8 → 11/4 | s:hh cutoff:3192.4760919064283 ]", + "[ 11/4 → 23/8 | s:hh cutoff:3188.529685838148 ]", + "[ 11/4 → 3/1 | s:bd cutoff:3188.529685838148 ]", + "[ 23/8 → 3/1 | s:hh cutoff:6837.369324173778 ]", + "[ 3/1 → 25/8 | s:hh cutoff:5111.154264421202 ]", + "[ 3/1 → 13/4 | s:bd cutoff:5111.154264421202 ]", + "[ 25/8 → 13/4 | s:hh cutoff:4812.422384973615 ]", + "[ 13/4 → 27/8 | s:hh cutoff:7517.148802988231 ]", + "[ 13/4 → 7/2 | s:bd cutoff:7517.148802988231 ]", + "[ 27/8 → 7/2 | s:hh cutoff:821.3959728600457 ]", + "[ 7/2 → 29/8 | s:hh cutoff:2354.0139601100236 ]", + "[ 7/2 → 15/4 | s:bd cutoff:2354.0139601100236 ]", + "[ 29/8 → 15/4 | s:hh cutoff:4901.923676487058 ]", + "[ 15/4 → 31/8 | s:hh cutoff:4723.308931104839 ]", + "[ 15/4 → 4/1 | s:bd cutoff:4723.308931104839 ]", + "[ 31/8 → 4/1 | s:hh cutoff:1649.9183619162068 ]", ] `; @@ -8381,35 +8366,35 @@ exports[`runs examples > example "rarely" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh ]", "[ 1/8 → 1/4 | s:hh ]", - "[ 1/4 → 3/8 | s:hh ]", + "[ 1/4 → 3/8 | s:hh speed:0.5 ]", "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:hh ]", "[ 5/8 → 3/4 | s:hh ]", "[ 3/4 → 7/8 | s:hh ]", "[ 7/8 → 1/1 | s:hh ]", "[ 1/1 → 9/8 | s:hh speed:0.5 ]", - "[ 9/8 → 5/4 | s:hh ]", + "[ 9/8 → 5/4 | s:hh speed:0.5 ]", "[ 5/4 → 11/8 | s:hh ]", - "[ 11/8 → 3/2 | s:hh ]", + "[ 11/8 → 3/2 | s:hh speed:0.5 ]", "[ 3/2 → 13/8 | s:hh speed:0.5 ]", "[ 13/8 → 7/4 | s:hh ]", "[ 7/4 → 15/8 | s:hh ]", "[ 15/8 → 2/1 | s:hh ]", "[ 2/1 → 17/8 | s:hh ]", "[ 17/8 → 9/4 | s:hh ]", - "[ 9/4 → 19/8 | s:hh ]", + "[ 9/4 → 19/8 | s:hh speed:0.5 ]", "[ 19/8 → 5/2 | s:hh ]", "[ 5/2 → 21/8 | s:hh ]", - "[ 21/8 → 11/4 | s:hh speed:0.5 ]", + "[ 21/8 → 11/4 | s:hh ]", "[ 11/4 → 23/8 | s:hh ]", "[ 23/8 → 3/1 | s:hh ]", "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", "[ 13/4 → 27/8 | s:hh ]", - "[ 27/8 → 7/2 | s:hh ]", - "[ 7/2 → 29/8 | s:hh ]", + "[ 27/8 → 7/2 | s:hh speed:0.5 ]", + "[ 7/2 → 29/8 | s:hh speed:0.5 ]", "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:hh speed:0.5 ]", + "[ 15/4 → 31/8 | s:hh ]", "[ 31/8 → 4/1 | s:hh speed:0.5 ]", ] `; @@ -8454,22 +8439,22 @@ exports[`runs examples > example "release" example index 0 1`] = ` exports[`runs examples > example "repeatCycles" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:39 s:gm_acoustic_guitar_nylon ]", - "[ 1/4 → 1/2 | note:41 s:gm_acoustic_guitar_nylon ]", - "[ 1/2 → 3/4 | note:38 s:gm_acoustic_guitar_nylon ]", - "[ 3/4 → 1/1 | note:41 s:gm_acoustic_guitar_nylon ]", - "[ 1/1 → 5/4 | note:39 s:gm_acoustic_guitar_nylon ]", - "[ 5/4 → 3/2 | note:41 s:gm_acoustic_guitar_nylon ]", - "[ 3/2 → 7/4 | note:38 s:gm_acoustic_guitar_nylon ]", - "[ 7/4 → 2/1 | note:41 s:gm_acoustic_guitar_nylon ]", - "[ 2/1 → 9/4 | note:34 s:gm_acoustic_guitar_nylon ]", - "[ 9/4 → 5/2 | note:42 s:gm_acoustic_guitar_nylon ]", - "[ 5/2 → 11/4 | note:36 s:gm_acoustic_guitar_nylon ]", - "[ 11/4 → 3/1 | note:42 s:gm_acoustic_guitar_nylon ]", - "[ 3/1 → 13/4 | note:34 s:gm_acoustic_guitar_nylon ]", - "[ 13/4 → 7/2 | note:42 s:gm_acoustic_guitar_nylon ]", - "[ 7/2 → 15/4 | note:36 s:gm_acoustic_guitar_nylon ]", - "[ 15/4 → 4/1 | note:42 s:gm_acoustic_guitar_nylon ]", + "[ 0/1 → 1/4 | note:38 s:gm_acoustic_guitar_nylon ]", + "[ 1/4 → 1/2 | note:35 s:gm_acoustic_guitar_nylon ]", + "[ 1/2 → 3/4 | note:37 s:gm_acoustic_guitar_nylon ]", + "[ 3/4 → 1/1 | note:37 s:gm_acoustic_guitar_nylon ]", + "[ 1/1 → 5/4 | note:38 s:gm_acoustic_guitar_nylon ]", + "[ 5/4 → 3/2 | note:35 s:gm_acoustic_guitar_nylon ]", + "[ 3/2 → 7/4 | note:37 s:gm_acoustic_guitar_nylon ]", + "[ 7/4 → 2/1 | note:37 s:gm_acoustic_guitar_nylon ]", + "[ 2/1 → 9/4 | note:35 s:gm_acoustic_guitar_nylon ]", + "[ 9/4 → 5/2 | note:45 s:gm_acoustic_guitar_nylon ]", + "[ 5/2 → 11/4 | note:35 s:gm_acoustic_guitar_nylon ]", + "[ 11/4 → 3/1 | note:38 s:gm_acoustic_guitar_nylon ]", + "[ 3/1 → 13/4 | note:35 s:gm_acoustic_guitar_nylon ]", + "[ 13/4 → 7/2 | note:45 s:gm_acoustic_guitar_nylon ]", + "[ 7/2 → 15/4 | note:35 s:gm_acoustic_guitar_nylon ]", + "[ 15/4 → 4/1 | note:38 s:gm_acoustic_guitar_nylon ]", ] `; @@ -8634,43 +8619,43 @@ exports[`runs examples > example "ribbon" example index 0 1`] = ` exports[`runs examples > example "ribbon" example index 1 1`] = ` [ - "[ 0/1 → 1/4 | note:E4 ]", - "[ 1/4 → 1/2 | note:G3 ]", - "[ 1/2 → 3/4 | note:C3 ]", - "[ 3/4 → 1/1 | note:E4 ]", - "[ 1/1 → 5/4 | note:A3 ]", + "[ 0/1 → 1/4 | note:D3 ]", + "[ 1/4 → 1/2 | note:A3 ]", + "[ 1/2 → 3/4 | note:D3 ]", + "[ 3/4 → 1/1 | note:E3 ]", + "[ 1/1 → 5/4 | note:C3 ]", "[ 5/4 → 3/2 | note:D3 ]", - "[ 3/2 → 7/4 | note:A3 ]", - "[ 7/4 → 2/1 | note:G3 ]", - "[ 2/1 → 9/4 | note:E4 ]", - "[ 9/4 → 5/2 | note:G3 ]", - "[ 5/2 → 11/4 | note:C3 ]", - "[ 11/4 → 3/1 | note:E4 ]", - "[ 3/1 → 13/4 | note:A3 ]", + "[ 3/2 → 7/4 | note:D3 ]", + "[ 7/4 → 2/1 | note:C4 ]", + "[ 2/1 → 9/4 | note:D3 ]", + "[ 9/4 → 5/2 | note:A3 ]", + "[ 5/2 → 11/4 | note:D3 ]", + "[ 11/4 → 3/1 | note:E3 ]", + "[ 3/1 → 13/4 | note:C3 ]", "[ 13/4 → 7/2 | note:D3 ]", - "[ 7/2 → 15/4 | note:A3 ]", - "[ 15/4 → 4/1 | note:G3 ]", + "[ 7/2 → 15/4 | note:D3 ]", + "[ 15/4 → 4/1 | note:C4 ]", ] `; exports[`runs examples > example "ribbon" example index 2 1`] = ` [ - "[ 1/8 → 3/16 | s:bd ]", - "[ 7/16 → 1/2 | s:bd ]", - "[ 5/8 → 11/16 | s:bd ]", - "[ 15/16 → 1/1 | s:bd ]", - "[ 9/8 → 19/16 | s:bd ]", - "[ 23/16 → 3/2 | s:bd ]", - "[ 13/8 → 27/16 | s:bd ]", - "[ 31/16 → 2/1 | s:bd ]", - "[ 17/8 → 35/16 | s:bd ]", - "[ 39/16 → 5/2 | s:bd ]", - "[ 21/8 → 43/16 | s:bd ]", - "[ 47/16 → 3/1 | s:bd ]", - "[ 25/8 → 51/16 | s:bd ]", - "[ 55/16 → 7/2 | s:bd ]", - "[ 29/8 → 59/16 | s:bd ]", - "[ 63/16 → 4/1 | s:bd ]", + "[ 1/16 → 1/8 | s:bd ]", + "[ 3/16 → 1/4 | s:bd ]", + "[ 9/16 → 5/8 | s:bd ]", + "[ 11/16 → 3/4 | s:bd ]", + "[ 17/16 → 9/8 | s:bd ]", + "[ 19/16 → 5/4 | s:bd ]", + "[ 25/16 → 13/8 | s:bd ]", + "[ 27/16 → 7/4 | s:bd ]", + "[ 33/16 → 17/8 | s:bd ]", + "[ 35/16 → 9/4 | s:bd ]", + "[ 41/16 → 21/8 | s:bd ]", + "[ 43/16 → 11/4 | s:bd ]", + "[ 49/16 → 25/8 | s:bd ]", + "[ 51/16 → 13/4 | s:bd ]", + "[ 57/16 → 29/8 | s:bd ]", + "[ 59/16 → 15/4 | s:bd ]", ] `; @@ -9201,38 +9186,38 @@ exports[`runs examples > example "scale" example index 1 1`] = ` exports[`runs examples > example "scale" example index 2 1`] = ` [ - "[ 0/1 → 1/8 | note:D4 s:piano ]", - "[ 1/8 → 1/4 | note:C5 s:piano ]", - "[ 1/4 → 3/8 | note:G4 s:piano ]", - "[ 3/8 → 1/2 | note:F4 s:piano ]", - "[ 1/2 → 5/8 | note:C4 s:piano ]", - "[ 5/8 → 3/4 | note:D5 s:piano ]", - "[ 3/4 → 7/8 | note:G4 s:piano ]", + "[ 0/1 → 1/8 | note:C4 s:piano ]", + "[ 1/8 → 1/4 | note:G4 s:piano ]", + "[ 1/4 → 3/8 | note:F3 s:piano ]", + "[ 3/8 → 1/2 | note:D5 s:piano ]", + "[ 1/2 → 5/8 | note:A3 s:piano ]", + "[ 5/8 → 3/4 | note:A4 s:piano ]", + "[ 3/4 → 7/8 | note:A3 s:piano ]", "[ 7/8 → 1/1 | note:D5 s:piano ]", - "[ 1/1 → 9/8 | note:D3 s:piano ]", - "[ 9/8 → 5/4 | note:C5 s:piano ]", - "[ 5/4 → 11/8 | note:A4 s:piano ]", - "[ 11/8 → 3/2 | note:D5 s:piano ]", - "[ 3/2 → 13/8 | note:G3 s:piano ]", - "[ 13/8 → 7/4 | note:A3 s:piano ]", - "[ 7/4 → 15/8 | note:A4 s:piano ]", - "[ 15/8 → 2/1 | note:C5 s:piano ]", + "[ 1/1 → 9/8 | note:F3 s:piano ]", + "[ 9/8 → 5/4 | note:D3 s:piano ]", + "[ 5/4 → 11/8 | note:F5 s:piano ]", + "[ 11/8 → 3/2 | note:D3 s:piano ]", + "[ 3/2 → 13/8 | note:F3 s:piano ]", + "[ 13/8 → 7/4 | note:G4 s:piano ]", + "[ 7/4 → 15/8 | note:C4 s:piano ]", + "[ 15/8 → 2/1 | note:G4 s:piano ]", "[ 2/1 → 17/8 | note:F4 s:piano ]", - "[ 17/8 → 9/4 | note:C5 s:piano ]", - "[ 9/4 → 19/8 | note:G4 s:piano ]", - "[ 19/8 → 5/2 | note:A4 s:piano ]", - "[ 5/2 → 21/8 | note:C4 s:piano ]", - "[ 21/8 → 11/4 | note:D3 s:piano ]", - "[ 11/4 → 23/8 | note:A4 s:piano ]", + "[ 17/8 → 9/4 | note:D4 s:piano ]", + "[ 9/4 → 19/8 | note:G3 s:piano ]", + "[ 19/8 → 5/2 | note:D5 s:piano ]", + "[ 5/2 → 21/8 | note:A4 s:piano ]", + "[ 21/8 → 11/4 | note:C4 s:piano ]", + "[ 11/4 → 23/8 | note:C4 s:piano ]", "[ 23/8 → 3/1 | note:D5 s:piano ]", - "[ 3/1 → 25/8 | note:D4 s:piano ]", - "[ 25/8 → 13/4 | note:A4 s:piano ]", - "[ 13/4 → 27/8 | note:A3 s:piano ]", - "[ 27/8 → 7/2 | note:A4 s:piano ]", - "[ 7/2 → 29/8 | note:A3 s:piano ]", - "[ 29/8 → 15/4 | note:D5 s:piano ]", - "[ 15/4 → 31/8 | note:D3 s:piano ]", - "[ 31/8 → 4/1 | note:D3 s:piano ]", + "[ 3/1 → 25/8 | note:G4 s:piano ]", + "[ 25/8 → 13/4 | note:F4 s:piano ]", + "[ 13/4 → 27/8 | note:F5 s:piano ]", + "[ 27/8 → 7/2 | note:D3 s:piano ]", + "[ 7/2 → 29/8 | note:G3 s:piano ]", + "[ 29/8 → 15/4 | note:G4 s:piano ]", + "[ 15/4 → 31/8 | note:F4 s:piano ]", + "[ 31/8 → 4/1 | note:F3 s:piano ]", ] `; @@ -9271,70 +9256,70 @@ exports[`runs examples > example "scale" example index 3 1`] = ` exports[`runs examples > example "scale" example index 4 1`] = ` [ - "[ 0/1 → 1/16 | note:Gb1 ]", - "[ 1/16 → 1/8 | note:Gb1 ]", - "[ 1/8 → 3/16 | note:Gb1 ]", - "[ 3/16 → 1/4 | note:Gb1 ]", - "[ 1/4 → 5/16 | note:Gb1 ]", - "[ 5/16 → 3/8 | note:Gb1 ]", - "[ 3/8 → 7/16 | note:Gb1 ]", - "[ 7/16 → 1/2 | note:Gb1 ]", - "[ 1/2 → 9/16 | note:Gb1 ]", - "[ 9/16 → 5/8 | note:Gb1 ]", - "[ 5/8 → 11/16 | note:Gb1 ]", - "[ 11/16 → 3/4 | note:Gb1 ]", - "[ 3/4 → 13/16 | note:Gb1 ]", - "[ 13/16 → 7/8 | note:Gb1 ]", - "[ 7/8 → 15/16 | note:Gb1 ]", - "[ 15/16 → 1/1 | note:Gb1 ]", - "[ 1/1 → 17/16 | note:Cb3 ]", - "[ 17/16 → 9/8 | note:Cb3 ]", - "[ 9/8 → 19/16 | note:Cb3 ]", - "[ 19/16 → 5/4 | note:Cb3 ]", - "[ 5/4 → 21/16 | note:Cb3 ]", - "[ 21/16 → 11/8 | note:Cb3 ]", - "[ 11/8 → 23/16 | note:Cb3 ]", - "[ 23/16 → 3/2 | note:Cb3 ]", - "[ 3/2 → 25/16 | note:Cb3 ]", - "[ 25/16 → 13/8 | note:Cb3 ]", - "[ 13/8 → 27/16 | note:Cb3 ]", - "[ 27/16 → 7/4 | note:Cb3 ]", - "[ 7/4 → 29/16 | note:Cb3 ]", - "[ 29/16 → 15/8 | note:Cb3 ]", - "[ 15/8 → 31/16 | note:Cb3 ]", - "[ 31/16 → 2/1 | note:Cb3 ]", - "[ 2/1 → 33/16 | note:Eb4 ]", - "[ 33/16 → 17/8 | note:Eb4 ]", - "[ 17/8 → 35/16 | note:Eb4 ]", - "[ 35/16 → 9/4 | note:Eb4 ]", - "[ 9/4 → 37/16 | note:Eb4 ]", - "[ 37/16 → 19/8 | note:Eb4 ]", - "[ 19/8 → 39/16 | note:Eb4 ]", - "[ 39/16 → 5/2 | note:Eb4 ]", - "[ 5/2 → 41/16 | note:Eb4 ]", - "[ 41/16 → 21/8 | note:Eb4 ]", - "[ 21/8 → 43/16 | note:Eb4 ]", - "[ 43/16 → 11/4 | note:Eb4 ]", - "[ 11/4 → 45/16 | note:Eb4 ]", - "[ 45/16 → 23/8 | note:Eb4 ]", - "[ 23/8 → 47/16 | note:Eb4 ]", - "[ 47/16 → 3/1 | note:Eb4 ]", - "[ 3/1 → 49/16 | note:Db2 ]", - "[ 49/16 → 25/8 | note:Db2 ]", - "[ 25/8 → 51/16 | note:Db2 ]", - "[ 51/16 → 13/4 | note:Db2 ]", - "[ 13/4 → 53/16 | note:Db2 ]", - "[ 53/16 → 27/8 | note:Db2 ]", - "[ 27/8 → 55/16 | note:Db2 ]", - "[ 55/16 → 7/2 | note:Db2 ]", - "[ 7/2 → 57/16 | note:Db2 ]", - "[ 57/16 → 29/8 | note:Db2 ]", - "[ 29/8 → 59/16 | note:Db2 ]", - "[ 59/16 → 15/4 | note:Db2 ]", - "[ 15/4 → 61/16 | note:Db2 ]", - "[ 61/16 → 31/8 | note:Db2 ]", - "[ 31/8 → 63/16 | note:Db2 ]", - "[ 63/16 → 4/1 | note:Db2 ]", + "[ 0/1 → 1/16 | note:Gb2 ]", + "[ 1/16 → 1/8 | note:Gb2 ]", + "[ 1/8 → 3/16 | note:Gb2 ]", + "[ 3/16 → 1/4 | note:Gb2 ]", + "[ 1/4 → 5/16 | note:Gb2 ]", + "[ 5/16 → 3/8 | note:Gb2 ]", + "[ 3/8 → 7/16 | note:Gb2 ]", + "[ 7/16 → 1/2 | note:Gb2 ]", + "[ 1/2 → 9/16 | note:Gb2 ]", + "[ 9/16 → 5/8 | note:Gb2 ]", + "[ 5/8 → 11/16 | note:Gb2 ]", + "[ 11/16 → 3/4 | note:Gb2 ]", + "[ 3/4 → 13/16 | note:Gb2 ]", + "[ 13/16 → 7/8 | note:Gb2 ]", + "[ 7/8 → 15/16 | note:Gb2 ]", + "[ 15/16 → 1/1 | note:Gb2 ]", + "[ 1/1 → 17/16 | note:Bb1 ]", + "[ 17/16 → 9/8 | note:Bb1 ]", + "[ 9/8 → 19/16 | note:Bb1 ]", + "[ 19/16 → 5/4 | note:Bb1 ]", + "[ 5/4 → 21/16 | note:Bb1 ]", + "[ 21/16 → 11/8 | note:Bb1 ]", + "[ 11/8 → 23/16 | note:Bb1 ]", + "[ 23/16 → 3/2 | note:Bb1 ]", + "[ 3/2 → 25/16 | note:Bb1 ]", + "[ 25/16 → 13/8 | note:Bb1 ]", + "[ 13/8 → 27/16 | note:Bb1 ]", + "[ 27/16 → 7/4 | note:Bb1 ]", + "[ 7/4 → 29/16 | note:Bb1 ]", + "[ 29/16 → 15/8 | note:Bb1 ]", + "[ 15/8 → 31/16 | note:Bb1 ]", + "[ 31/16 → 2/1 | note:Bb1 ]", + "[ 2/1 → 33/16 | note:Db3 ]", + "[ 33/16 → 17/8 | note:Db3 ]", + "[ 17/8 → 35/16 | note:Db3 ]", + "[ 35/16 → 9/4 | note:Db3 ]", + "[ 9/4 → 37/16 | note:Db3 ]", + "[ 37/16 → 19/8 | note:Db3 ]", + "[ 19/8 → 39/16 | note:Db3 ]", + "[ 39/16 → 5/2 | note:Db3 ]", + "[ 5/2 → 41/16 | note:Db3 ]", + "[ 41/16 → 21/8 | note:Db3 ]", + "[ 21/8 → 43/16 | note:Db3 ]", + "[ 43/16 → 11/4 | note:Db3 ]", + "[ 11/4 → 45/16 | note:Db3 ]", + "[ 45/16 → 23/8 | note:Db3 ]", + "[ 23/8 → 47/16 | note:Db3 ]", + "[ 47/16 → 3/1 | note:Db3 ]", + "[ 3/1 → 49/16 | note:Eb3 ]", + "[ 49/16 → 25/8 | note:Eb3 ]", + "[ 25/8 → 51/16 | note:Eb3 ]", + "[ 51/16 → 13/4 | note:Eb3 ]", + "[ 13/4 → 53/16 | note:Eb3 ]", + "[ 53/16 → 27/8 | note:Eb3 ]", + "[ 27/8 → 55/16 | note:Eb3 ]", + "[ 55/16 → 7/2 | note:Eb3 ]", + "[ 7/2 → 57/16 | note:Eb3 ]", + "[ 57/16 → 29/8 | note:Eb3 ]", + "[ 29/8 → 59/16 | note:Eb3 ]", + "[ 59/16 → 15/4 | note:Eb3 ]", + "[ 15/4 → 61/16 | note:Eb3 ]", + "[ 61/16 → 31/8 | note:Eb3 ]", + "[ 31/8 → 63/16 | note:Eb3 ]", + "[ 63/16 → 4/1 | note:Eb3 ]", ] `; @@ -9408,45 +9393,45 @@ exports[`runs examples > example "scope" example index 0 1`] = ` exports[`runs examples > example "scramble" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:d s:piano ]", - "[ 1/4 → 1/2 | note:e s:piano ]", + "[ 1/4 → 1/2 | note:c s:piano ]", "[ 1/2 → 3/4 | note:d s:piano ]", - "[ 3/4 → 1/1 | note:e s:piano ]", + "[ 3/4 → 1/1 | note:d s:piano ]", "[ 1/1 → 5/4 | note:c s:piano ]", - "[ 5/4 → 3/2 | note:e s:piano ]", + "[ 5/4 → 3/2 | note:f s:piano ]", "[ 3/2 → 7/4 | note:c s:piano ]", - "[ 7/4 → 2/1 | note:e s:piano ]", + "[ 7/4 → 2/1 | note:d s:piano ]", "[ 2/1 → 9/4 | note:e s:piano ]", - "[ 9/4 → 5/2 | note:e s:piano ]", - "[ 5/2 → 11/4 | note:d s:piano ]", - "[ 11/4 → 3/1 | note:e s:piano ]", - "[ 3/1 → 13/4 | note:d s:piano ]", - "[ 13/4 → 7/2 | note:d s:piano ]", - "[ 7/2 → 15/4 | note:d s:piano ]", - "[ 15/4 → 4/1 | note:c s:piano ]", + "[ 9/4 → 5/2 | note:c s:piano ]", + "[ 5/2 → 11/4 | note:e s:piano ]", + "[ 11/4 → 3/1 | note:d s:piano ]", + "[ 3/1 → 13/4 | note:e s:piano ]", + "[ 13/4 → 7/2 | note:f s:piano ]", + "[ 7/2 → 15/4 | note:c s:piano ]", + "[ 15/4 → 4/1 | note:e s:piano ]", ] `; exports[`runs examples > example "scramble" example index 1 1`] = ` [ "[ 0/1 → 1/8 | note:d s:piano ]", - "[ 1/8 → 1/4 | note:e s:piano ]", + "[ 1/8 → 1/4 | note:c s:piano ]", "[ 1/4 → 3/8 | note:d s:piano ]", - "[ 3/8 → 1/2 | note:e s:piano ]", + "[ 3/8 → 1/2 | note:d s:piano ]", "[ 1/2 → 1/1 | note:g s:piano ]", "[ 1/1 → 9/8 | note:c s:piano ]", - "[ 9/8 → 5/4 | note:e s:piano ]", + "[ 9/8 → 5/4 | note:f s:piano ]", "[ 5/4 → 11/8 | note:c s:piano ]", - "[ 11/8 → 3/2 | note:e s:piano ]", + "[ 11/8 → 3/2 | note:d s:piano ]", "[ 3/2 → 2/1 | note:g s:piano ]", "[ 2/1 → 17/8 | note:e s:piano ]", - "[ 17/8 → 9/4 | note:e s:piano ]", - "[ 9/4 → 19/8 | note:d s:piano ]", - "[ 19/8 → 5/2 | note:e s:piano ]", + "[ 17/8 → 9/4 | note:c s:piano ]", + "[ 9/4 → 19/8 | note:e s:piano ]", + "[ 19/8 → 5/2 | note:d s:piano ]", "[ 5/2 → 3/1 | note:g s:piano ]", - "[ 3/1 → 25/8 | note:d s:piano ]", - "[ 25/8 → 13/4 | note:d s:piano ]", - "[ 13/4 → 27/8 | note:d s:piano ]", - "[ 27/8 → 7/2 | note:c s:piano ]", + "[ 3/1 → 25/8 | note:e s:piano ]", + "[ 25/8 → 13/4 | note:f s:piano ]", + "[ 13/4 → 27/8 | note:c s:piano ]", + "[ 27/8 → 7/2 | note:e s:piano ]", "[ 7/2 → 4/1 | note:g s:piano ]", ] `; @@ -9497,6 +9482,23 @@ exports[`runs examples > example "scrub" example index 1 1`] = ` ] `; +exports[`runs examples > example "seed" example index 0 1`] = ` +[ + "[ 1/4 → 1/2 | s:bd ]", + "[ 1/2 → 3/4 | s:bd ]", + "[ 3/4 → 1/1 | s:bd ]", + "[ 1/1 → 5/4 | s:bd ]", + "[ 5/4 → 3/2 | s:bd ]", + "[ 3/2 → 7/4 | s:bd ]", + "[ 7/4 → 2/1 | s:bd ]", + "[ 2/1 → 9/4 | s:bd ]", + "[ 11/4 → 3/1 | s:bd ]", + "[ 3/1 → 13/4 | s:bd ]", + "[ 13/4 → 7/2 | s:bd ]", + "[ 7/2 → 15/4 | s:bd ]", +] +`; + exports[`runs examples > example "segment" example index 0 1`] = ` [ "[ 0/1 → 1/24 | note:40 ]", @@ -9703,38 +9705,38 @@ exports[`runs examples > example "setGainCurve" example index 0 1`] = ` exports[`runs examples > example "setMaxPolyphony" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | note:F#4 room:1 release:4 gain:0.5 ]", - "[ 1/8 → 1/4 | note:A5 room:1 release:4 gain:0.5 ]", - "[ 1/4 → 3/8 | note:C#5 room:1 release:4 gain:0.5 ]", - "[ 3/8 → 1/2 | note:A4 room:1 release:4 gain:0.5 ]", - "[ 1/2 → 5/8 | note:E4 room:1 release:4 gain:0.5 ]", - "[ 5/8 → 3/4 | note:C#6 room:1 release:4 gain:0.5 ]", - "[ 3/4 → 7/8 | note:D#5 room:1 release:4 gain:0.5 ]", + "[ 0/1 → 1/8 | note:E4 room:1 release:4 gain:0.5 ]", + "[ 1/8 → 1/4 | note:D#5 room:1 release:4 gain:0.5 ]", + "[ 1/4 → 3/8 | note:F#3 room:1 release:4 gain:0.5 ]", + "[ 3/8 → 1/2 | note:B5 room:1 release:4 gain:0.5 ]", + "[ 1/2 → 5/8 | note:C#4 room:1 release:4 gain:0.5 ]", + "[ 5/8 → 3/4 | note:E5 room:1 release:4 gain:0.5 ]", + "[ 3/4 → 7/8 | note:B3 room:1 release:4 gain:0.5 ]", "[ 7/8 → 1/1 | note:C#6 room:1 release:4 gain:0.5 ]", - "[ 1/1 → 9/8 | note:C#3 room:1 release:4 gain:0.5 ]", - "[ 9/8 → 5/4 | note:A5 room:1 release:4 gain:0.5 ]", - "[ 5/4 → 11/8 | note:E5 room:1 release:4 gain:0.5 ]", - "[ 11/8 → 3/2 | note:B5 room:1 release:4 gain:0.5 ]", - "[ 3/2 → 13/8 | note:A3 room:1 release:4 gain:0.5 ]", - "[ 13/8 → 7/4 | note:B3 room:1 release:4 gain:0.5 ]", - "[ 7/4 → 15/8 | note:F#5 room:1 release:4 gain:0.5 ]", - "[ 15/8 → 2/1 | note:G#5 room:1 release:4 gain:0.5 ]", - "[ 2/1 → 17/8 | note:A4 room:1 release:4 gain:0.5 ]", - "[ 17/8 → 9/4 | note:G#5 room:1 release:4 gain:0.5 ]", - "[ 9/4 → 19/8 | note:D#5 room:1 release:4 gain:0.5 ]", - "[ 19/8 → 5/2 | note:E5 room:1 release:4 gain:0.5 ]", - "[ 5/2 → 21/8 | note:D#4 room:1 release:4 gain:0.5 ]", - "[ 21/8 → 11/4 | note:C#3 room:1 release:4 gain:0.5 ]", - "[ 11/4 → 23/8 | note:E5 room:1 release:4 gain:0.5 ]", - "[ 23/8 → 3/1 | note:C#6 room:1 release:4 gain:0.5 ]", - "[ 3/1 → 25/8 | note:F#4 room:1 release:4 gain:0.5 ]", - "[ 25/8 → 13/4 | note:E5 room:1 release:4 gain:0.5 ]", - "[ 13/4 → 27/8 | note:C#4 room:1 release:4 gain:0.5 ]", - "[ 27/8 → 7/2 | note:E5 room:1 release:4 gain:0.5 ]", - "[ 7/2 → 29/8 | note:B3 room:1 release:4 gain:0.5 ]", - "[ 29/8 → 15/4 | note:C#6 room:1 release:4 gain:0.5 ]", - "[ 15/4 → 31/8 | note:D#3 room:1 release:4 gain:0.5 ]", - "[ 31/8 → 4/1 | note:C#3 room:1 release:4 gain:0.5 ]", + "[ 1/1 → 9/8 | note:E3 room:1 release:4 gain:0.5 ]", + "[ 9/8 → 5/4 | note:D#3 room:1 release:4 gain:0.5 ]", + "[ 5/4 → 11/8 | note:D#6 room:1 release:4 gain:0.5 ]", + "[ 11/8 → 3/2 | note:D#3 room:1 release:4 gain:0.5 ]", + "[ 3/2 → 13/8 | note:F#3 room:1 release:4 gain:0.5 ]", + "[ 13/8 → 7/4 | note:D#5 room:1 release:4 gain:0.5 ]", + "[ 7/4 → 15/8 | note:D#4 room:1 release:4 gain:0.5 ]", + "[ 15/8 → 2/1 | note:D#5 room:1 release:4 gain:0.5 ]", + "[ 2/1 → 17/8 | note:B4 room:1 release:4 gain:0.5 ]", + "[ 17/8 → 9/4 | note:F#4 room:1 release:4 gain:0.5 ]", + "[ 9/4 → 19/8 | note:G#3 room:1 release:4 gain:0.5 ]", + "[ 19/8 → 5/2 | note:B5 room:1 release:4 gain:0.5 ]", + "[ 5/2 → 21/8 | note:F#5 room:1 release:4 gain:0.5 ]", + "[ 21/8 → 11/4 | note:D#4 room:1 release:4 gain:0.5 ]", + "[ 11/4 → 23/8 | note:D#4 room:1 release:4 gain:0.5 ]", + "[ 23/8 → 3/1 | note:B5 room:1 release:4 gain:0.5 ]", + "[ 3/1 → 25/8 | note:C#5 room:1 release:4 gain:0.5 ]", + "[ 25/8 → 13/4 | note:B4 room:1 release:4 gain:0.5 ]", + "[ 13/4 → 27/8 | note:D#6 room:1 release:4 gain:0.5 ]", + "[ 27/8 → 7/2 | note:D#3 room:1 release:4 gain:0.5 ]", + "[ 7/2 → 29/8 | note:A3 room:1 release:4 gain:0.5 ]", + "[ 29/8 → 15/4 | note:C#5 room:1 release:4 gain:0.5 ]", + "[ 15/4 → 31/8 | note:B4 room:1 release:4 gain:0.5 ]", + "[ 31/8 → 4/1 | note:F#3 room:1 release:4 gain:0.5 ]", ] `; @@ -9970,46 +9972,46 @@ exports[`runs examples > example "shrink" example index 3 1`] = ` exports[`runs examples > example "shuffle" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:e s:piano ]", - "[ 1/4 → 1/2 | note:c s:piano ]", - "[ 1/2 → 3/4 | note:f s:piano ]", - "[ 3/4 → 1/1 | note:d s:piano ]", - "[ 1/1 → 5/4 | note:f s:piano ]", - "[ 5/4 → 3/2 | note:c s:piano ]", - "[ 3/2 → 7/4 | note:e s:piano ]", - "[ 7/4 → 2/1 | note:d s:piano ]", - "[ 2/1 → 9/4 | note:c s:piano ]", - "[ 9/4 → 5/2 | note:f s:piano ]", - "[ 5/2 → 11/4 | note:d s:piano ]", - "[ 11/4 → 3/1 | note:e s:piano ]", - "[ 3/1 → 13/4 | note:c s:piano ]", - "[ 13/4 → 7/2 | note:d s:piano ]", - "[ 7/2 → 15/4 | note:f s:piano ]", - "[ 15/4 → 4/1 | note:e s:piano ]", + "[ 0/1 → 1/4 | note:f s:piano ]", + "[ 1/4 → 1/2 | note:d s:piano ]", + "[ 1/2 → 3/4 | note:c s:piano ]", + "[ 3/4 → 1/1 | note:e s:piano ]", + "[ 1/1 → 5/4 | note:c s:piano ]", + "[ 5/4 → 3/2 | note:f s:piano ]", + "[ 3/2 → 7/4 | note:d s:piano ]", + "[ 7/4 → 2/1 | note:e s:piano ]", + "[ 2/1 → 9/4 | note:e s:piano ]", + "[ 9/4 → 5/2 | note:d s:piano ]", + "[ 5/2 → 11/4 | note:f s:piano ]", + "[ 11/4 → 3/1 | note:c s:piano ]", + "[ 3/1 → 13/4 | note:f s:piano ]", + "[ 13/4 → 7/2 | note:c s:piano ]", + "[ 7/2 → 15/4 | note:e s:piano ]", + "[ 15/4 → 4/1 | note:d s:piano ]", ] `; exports[`runs examples > example "shuffle" example index 1 1`] = ` [ - "[ 0/1 → 1/8 | note:e s:piano ]", - "[ 1/8 → 1/4 | note:c s:piano ]", - "[ 1/4 → 3/8 | note:f s:piano ]", - "[ 3/8 → 1/2 | note:d s:piano ]", + "[ 0/1 → 1/8 | note:f s:piano ]", + "[ 1/8 → 1/4 | note:d s:piano ]", + "[ 1/4 → 3/8 | note:c s:piano ]", + "[ 3/8 → 1/2 | note:e s:piano ]", "[ 1/2 → 1/1 | note:g s:piano ]", - "[ 1/1 → 9/8 | note:f s:piano ]", - "[ 9/8 → 5/4 | note:c s:piano ]", - "[ 5/4 → 11/8 | note:e s:piano ]", - "[ 11/8 → 3/2 | note:d s:piano ]", + "[ 1/1 → 9/8 | note:c s:piano ]", + "[ 9/8 → 5/4 | note:f s:piano ]", + "[ 5/4 → 11/8 | note:d s:piano ]", + "[ 11/8 → 3/2 | note:e s:piano ]", "[ 3/2 → 2/1 | note:g s:piano ]", - "[ 2/1 → 17/8 | note:c s:piano ]", - "[ 17/8 → 9/4 | note:f s:piano ]", - "[ 9/4 → 19/8 | note:d s:piano ]", - "[ 19/8 → 5/2 | note:e s:piano ]", + "[ 2/1 → 17/8 | note:e s:piano ]", + "[ 17/8 → 9/4 | note:d s:piano ]", + "[ 9/4 → 19/8 | note:f s:piano ]", + "[ 19/8 → 5/2 | note:c s:piano ]", "[ 5/2 → 3/1 | note:g s:piano ]", - "[ 3/1 → 25/8 | note:c s:piano ]", - "[ 25/8 → 13/4 | note:d s:piano ]", - "[ 13/4 → 27/8 | note:f s:piano ]", - "[ 27/8 → 7/2 | note:e s:piano ]", + "[ 3/1 → 25/8 | note:f s:piano ]", + "[ 25/8 → 13/4 | note:c s:piano ]", + "[ 13/4 → 27/8 | note:e s:piano ]", + "[ 27/8 → 7/2 | note:d s:piano ]", "[ 7/2 → 4/1 | note:g s:piano ]", ] `; @@ -10214,15 +10216,15 @@ exports[`runs examples > example "someCycles" example index 0 1`] = ` "[ 21/8 → 11/4 | s:hh ]", "[ 11/4 → 23/8 | s:hh ]", "[ 23/8 → 3/1 | s:hh ]", - "[ 3/1 → 25/8 | s:hh speed:0.5 ]", - "[ 3/1 → 4/1 | s:bd speed:0.5 ]", - "[ 25/8 → 13/4 | s:hh speed:0.5 ]", - "[ 13/4 → 27/8 | s:hh speed:0.5 ]", - "[ 27/8 → 7/2 | s:hh speed:0.5 ]", - "[ 7/2 → 29/8 | s:hh speed:0.5 ]", - "[ 29/8 → 15/4 | s:hh speed:0.5 ]", - "[ 15/4 → 31/8 | s:hh speed:0.5 ]", - "[ 31/8 → 4/1 | s:hh speed:0.5 ]", + "[ 3/1 → 25/8 | s:hh ]", + "[ 3/1 → 4/1 | s:bd ]", + "[ 25/8 → 13/4 | s:hh ]", + "[ 13/4 → 27/8 | s:hh ]", + "[ 27/8 → 7/2 | s:hh ]", + "[ 7/2 → 29/8 | s:hh ]", + "[ 29/8 → 15/4 | s:hh ]", + "[ 15/4 → 31/8 | s:hh ]", + "[ 31/8 → 4/1 | s:hh ]", ] `; @@ -10271,72 +10273,72 @@ exports[`runs examples > example "sometimes" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh speed:0.5 ]", "[ 1/8 → 1/4 | s:hh ]", - "[ 1/4 → 3/8 | s:hh ]", + "[ 1/4 → 3/8 | s:hh speed:0.5 ]", "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:hh speed:0.5 ]", "[ 5/8 → 3/4 | s:hh ]", - "[ 3/4 → 7/8 | s:hh ]", + "[ 3/4 → 7/8 | s:hh speed:0.5 ]", "[ 7/8 → 1/1 | s:hh ]", "[ 1/1 → 9/8 | s:hh speed:0.5 ]", - "[ 9/8 → 5/4 | s:hh ]", + "[ 9/8 → 5/4 | s:hh speed:0.5 ]", "[ 5/4 → 11/8 | s:hh ]", - "[ 11/8 → 3/2 | s:hh ]", + "[ 11/8 → 3/2 | s:hh speed:0.5 ]", "[ 3/2 → 13/8 | s:hh speed:0.5 ]", - "[ 13/8 → 7/4 | s:hh speed:0.5 ]", - "[ 7/4 → 15/8 | s:hh ]", + "[ 13/8 → 7/4 | s:hh ]", + "[ 7/4 → 15/8 | s:hh speed:0.5 ]", "[ 15/8 → 2/1 | s:hh ]", "[ 2/1 → 17/8 | s:hh ]", - "[ 17/8 → 9/4 | s:hh ]", - "[ 9/4 → 19/8 | s:hh ]", + "[ 17/8 → 9/4 | s:hh speed:0.5 ]", + "[ 9/4 → 19/8 | s:hh speed:0.5 ]", "[ 19/8 → 5/2 | s:hh ]", - "[ 5/2 → 21/8 | s:hh speed:0.5 ]", + "[ 5/2 → 21/8 | s:hh ]", "[ 21/8 → 11/4 | s:hh speed:0.5 ]", - "[ 11/4 → 23/8 | s:hh ]", + "[ 11/4 → 23/8 | s:hh speed:0.5 ]", "[ 23/8 → 3/1 | s:hh ]", - "[ 3/1 → 25/8 | s:hh speed:0.5 ]", + "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", - "[ 13/4 → 27/8 | s:hh speed:0.5 ]", - "[ 27/8 → 7/2 | s:hh ]", + "[ 13/4 → 27/8 | s:hh ]", + "[ 27/8 → 7/2 | s:hh speed:0.5 ]", "[ 7/2 → 29/8 | s:hh speed:0.5 ]", "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:hh speed:0.5 ]", + "[ 15/4 → 31/8 | s:hh ]", "[ 31/8 → 4/1 | s:hh speed:0.5 ]", ] `; exports[`runs examples > example "sometimesBy" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:hh ]", + "[ 0/1 → 1/8 | s:hh speed:0.5 ]", "[ 1/8 → 1/4 | s:hh ]", - "[ 1/4 → 3/8 | s:hh ]", + "[ 1/4 → 3/8 | s:hh speed:0.5 ]", "[ 3/8 → 1/2 | s:hh ]", - "[ 1/2 → 5/8 | s:hh ]", + "[ 1/2 → 5/8 | s:hh speed:0.5 ]", "[ 5/8 → 3/4 | s:hh ]", - "[ 3/4 → 7/8 | s:hh ]", + "[ 3/4 → 7/8 | s:hh speed:0.5 ]", "[ 7/8 → 1/1 | s:hh ]", "[ 1/1 → 9/8 | s:hh speed:0.5 ]", - "[ 9/8 → 5/4 | s:hh ]", + "[ 9/8 → 5/4 | s:hh speed:0.5 ]", "[ 5/4 → 11/8 | s:hh ]", - "[ 11/8 → 3/2 | s:hh ]", + "[ 11/8 → 3/2 | s:hh speed:0.5 ]", "[ 3/2 → 13/8 | s:hh speed:0.5 ]", - "[ 13/8 → 7/4 | s:hh speed:0.5 ]", - "[ 7/4 → 15/8 | s:hh ]", + "[ 13/8 → 7/4 | s:hh ]", + "[ 7/4 → 15/8 | s:hh speed:0.5 ]", "[ 15/8 → 2/1 | s:hh ]", "[ 2/1 → 17/8 | s:hh ]", "[ 17/8 → 9/4 | s:hh ]", - "[ 9/4 → 19/8 | s:hh ]", + "[ 9/4 → 19/8 | s:hh speed:0.5 ]", "[ 19/8 → 5/2 | s:hh ]", - "[ 5/2 → 21/8 | s:hh speed:0.5 ]", + "[ 5/2 → 21/8 | s:hh ]", "[ 21/8 → 11/4 | s:hh speed:0.5 ]", - "[ 11/4 → 23/8 | s:hh ]", + "[ 11/4 → 23/8 | s:hh speed:0.5 ]", "[ 23/8 → 3/1 | s:hh ]", "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", - "[ 13/4 → 27/8 | s:hh speed:0.5 ]", - "[ 27/8 → 7/2 | s:hh ]", + "[ 13/4 → 27/8 | s:hh ]", + "[ 27/8 → 7/2 | s:hh speed:0.5 ]", "[ 7/2 → 29/8 | s:hh speed:0.5 ]", "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:hh speed:0.5 ]", + "[ 15/4 → 31/8 | s:hh ]", "[ 31/8 → 4/1 | s:hh speed:0.5 ]", ] `; @@ -11721,16 +11723,20 @@ exports[`runs examples > example "tri" example index 0 1`] = ` exports[`runs examples > example "undegrade" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh ]", + "[ 1/4 → 3/8 | s:hh ]", "[ 1/2 → 5/8 | s:hh ]", + "[ 3/4 → 7/8 | s:hh ]", "[ 1/1 → 9/8 | s:hh ]", + "[ 9/8 → 5/4 | s:hh ]", + "[ 11/8 → 3/2 | s:hh ]", "[ 3/2 → 13/8 | s:hh ]", - "[ 13/8 → 7/4 | s:hh ]", - "[ 5/2 → 21/8 | s:hh ]", + "[ 7/4 → 15/8 | s:hh ]", + "[ 17/8 → 9/4 | s:hh ]", + "[ 9/4 → 19/8 | s:hh ]", "[ 21/8 → 11/4 | s:hh ]", - "[ 3/1 → 25/8 | s:hh ]", - "[ 13/4 → 27/8 | s:hh ]", + "[ 11/4 → 23/8 | s:hh ]", + "[ 27/8 → 7/2 | s:hh ]", "[ 7/2 → 29/8 | s:hh ]", - "[ 15/4 → 31/8 | s:hh ]", "[ 31/8 → 4/1 | s:hh ]", ] `; @@ -11739,56 +11745,58 @@ exports[`runs examples > example "undegrade" example index 1 1`] = ` [ "[ 0/1 → 1/10 | s:hh pan:1 ]", "[ 1/10 → 1/5 | s:hh pan:1 ]", - "[ 1/5 → 3/10 | s:hh pan:1 ]", - "[ 3/10 → 2/5 | s:hh pan:0 ]", - "[ 2/5 → 1/2 | s:hh pan:1 ]", + "[ 1/5 → 3/10 | s:hh pan:0 ]", + "[ 3/10 → 2/5 | s:hh pan:1 ]", + "[ 2/5 → 1/2 | s:hh pan:0 ]", "[ 1/2 → 3/5 | s:hh pan:1 ]", - "[ 3/5 → 7/10 | s:hh pan:0 ]", - "[ 7/10 → 4/5 | s:hh pan:0 ]", - "[ 4/5 → 9/10 | s:hh pan:0 ]", - "[ 9/10 → 1/1 | s:hh pan:1 ]", + "[ 3/5 → 7/10 | s:hh pan:1 ]", + "[ 7/10 → 4/5 | s:hh pan:1 ]", + "[ 4/5 → 9/10 | s:hh pan:1 ]", + "[ 9/10 → 1/1 | s:hh pan:0 ]", "[ 1/1 → 11/10 | s:hh pan:1 ]", - "[ 11/10 → 6/5 | s:hh pan:0 ]", + "[ 11/10 → 6/5 | s:hh pan:1 ]", "[ 6/5 → 13/10 | s:hh pan:0 ]", "[ 13/10 → 7/5 | s:hh pan:1 ]", - "[ 7/5 → 3/2 | s:hh pan:1 ]", + "[ 7/5 → 3/2 | s:hh pan:0 ]", "[ 3/2 → 8/5 | s:hh pan:1 ]", "[ 8/5 → 17/10 | s:hh pan:0 ]", - "[ 17/10 → 9/5 | s:hh pan:0 ]", - "[ 9/5 → 19/10 | s:hh pan:1 ]", + "[ 17/10 → 9/5 | s:hh pan:1 ]", + "[ 9/5 → 19/10 | s:hh pan:0 ]", "[ 19/10 → 2/1 | s:hh pan:1 ]", "[ 2/1 → 21/10 | s:hh pan:0 ]", "[ 21/10 → 11/5 | s:hh pan:1 ]", "[ 11/5 → 23/10 | s:hh pan:0 ]", "[ 23/10 → 12/5 | s:hh pan:1 ]", - "[ 12/5 → 5/2 | s:hh pan:1 ]", - "[ 5/2 → 13/5 | s:hh pan:1 ]", + "[ 12/5 → 5/2 | s:hh pan:0 ]", + "[ 5/2 → 13/5 | s:hh pan:0 ]", "[ 13/5 → 27/10 | s:hh pan:0 ]", "[ 27/10 → 14/5 | s:hh pan:1 ]", - "[ 14/5 → 29/10 | s:hh pan:1 ]", - "[ 29/10 → 3/1 | s:hh pan:1 ]", - "[ 3/1 → 31/10 | s:hh pan:1 ]", - "[ 31/10 → 16/5 | s:hh pan:0 ]", + "[ 14/5 → 29/10 | s:hh pan:0 ]", + "[ 29/10 → 3/1 | s:hh pan:0 ]", + "[ 3/1 → 31/10 | s:hh pan:0 ]", + "[ 31/10 → 16/5 | s:hh pan:1 ]", "[ 16/5 → 33/10 | s:hh pan:1 ]", "[ 33/10 → 17/5 | s:hh pan:0 ]", - "[ 17/5 → 7/2 | s:hh pan:0 ]", + "[ 17/5 → 7/2 | s:hh pan:1 ]", "[ 7/2 → 18/5 | s:hh pan:1 ]", - "[ 18/5 → 37/10 | s:hh pan:0 ]", + "[ 18/5 → 37/10 | s:hh pan:1 ]", "[ 37/10 → 19/5 | s:hh pan:1 ]", "[ 19/5 → 39/10 | s:hh pan:0 ]", - "[ 39/10 → 4/1 | s:hh pan:0 ]", + "[ 39/10 → 4/1 | s:hh pan:1 ]", ] `; exports[`runs examples > example "undegradeBy" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh ]", + "[ 1/8 → 1/4 | s:hh ]", "[ 1/4 → 3/8 | s:hh ]", - "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:hh ]", + "[ 5/8 → 3/4 | s:hh ]", "[ 3/4 → 7/8 | s:hh ]", "[ 1/1 → 9/8 | s:hh ]", - "[ 5/4 → 11/8 | s:hh ]", + "[ 9/8 → 5/4 | s:hh ]", + "[ 11/8 → 3/2 | s:hh ]", "[ 3/2 → 13/8 | s:hh ]", "[ 13/8 → 7/4 | s:hh ]", "[ 7/4 → 15/8 | s:hh ]", @@ -11796,15 +11804,14 @@ exports[`runs examples > example "undegradeBy" example index 0 1`] = ` "[ 2/1 → 17/8 | s:hh ]", "[ 17/8 → 9/4 | s:hh ]", "[ 9/4 → 19/8 | s:hh ]", - "[ 19/8 → 5/2 | s:hh ]", "[ 5/2 → 21/8 | s:hh ]", "[ 21/8 → 11/4 | s:hh ]", "[ 11/4 → 23/8 | s:hh ]", "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", - "[ 13/4 → 27/8 | s:hh ]", "[ 27/8 → 7/2 | s:hh ]", "[ 7/2 → 29/8 | s:hh ]", + "[ 29/8 → 15/4 | s:hh ]", "[ 15/4 → 31/8 | s:hh ]", "[ 31/8 → 4/1 | s:hh ]", ] @@ -11826,32 +11833,32 @@ exports[`runs examples > example "undegradeBy" example index 1 1`] = ` "[ 11/10 → 6/5 | s:hh pan:0 ]", "[ 6/5 → 13/10 | s:hh pan:0 ]", "[ 13/10 → 7/5 | s:hh pan:1 ]", - "[ 7/5 → 3/2 | s:hh pan:1 ]", - "[ 3/2 → 8/5 | s:hh pan:0 ]", + "[ 7/5 → 3/2 | s:hh pan:0 ]", + "[ 3/2 → 8/5 | s:hh pan:1 ]", "[ 8/5 → 17/10 | s:hh pan:0 ]", "[ 17/10 → 9/5 | s:hh pan:0 ]", "[ 9/5 → 19/10 | s:hh pan:0 ]", - "[ 19/10 → 2/1 | s:hh pan:1 ]", + "[ 19/10 → 2/1 | s:hh pan:0 ]", "[ 2/1 → 21/10 | s:hh pan:0 ]", "[ 21/10 → 11/5 | s:hh pan:1 ]", "[ 11/5 → 23/10 | s:hh pan:0 ]", - "[ 23/10 → 12/5 | s:hh pan:1 ]", + "[ 23/10 → 12/5 | s:hh pan:0 ]", "[ 12/5 → 5/2 | s:hh pan:0 ]", "[ 5/2 → 13/5 | s:hh pan:0 ]", "[ 13/5 → 27/10 | s:hh pan:0 ]", - "[ 27/10 → 14/5 | s:hh pan:1 ]", - "[ 14/5 → 29/10 | s:hh pan:1 ]", + "[ 27/10 → 14/5 | s:hh pan:0 ]", + "[ 14/5 → 29/10 | s:hh pan:0 ]", "[ 29/10 → 3/1 | s:hh pan:0 ]", "[ 3/1 → 31/10 | s:hh pan:0 ]", - "[ 31/10 → 16/5 | s:hh pan:0 ]", - "[ 16/5 → 33/10 | s:hh pan:1 ]", + "[ 31/10 → 16/5 | s:hh pan:1 ]", + "[ 16/5 → 33/10 | s:hh pan:0 ]", "[ 33/10 → 17/5 | s:hh pan:0 ]", "[ 17/5 → 7/2 | s:hh pan:0 ]", "[ 7/2 → 18/5 | s:hh pan:0 ]", "[ 18/5 → 37/10 | s:hh pan:0 ]", - "[ 37/10 → 19/5 | s:hh pan:1 ]", + "[ 37/10 → 19/5 | s:hh pan:0 ]", "[ 19/5 → 39/10 | s:hh pan:0 ]", - "[ 39/10 → 4/1 | s:hh pan:0 ]", + "[ 39/10 → 4/1 | s:hh pan:1 ]", ] `; @@ -12156,14 +12163,14 @@ exports[`runs examples > example "vowel" example index 0 1`] = ` exports[`runs examples > example "vowel" example index 1 1`] = ` [ - "[ 0/1 → 1/8 | s:bd vowel:i ]", - "[ 1/8 → 1/4 | s:sd vowel:i ]", - "[ 1/4 → 3/8 | s:mt vowel:i ]", - "[ 3/8 → 1/2 | s:ht vowel:i ]", - "[ 1/2 → 5/8 | s:bd vowel:i ]", - "[ 11/16 → 3/4 | s:cp vowel:i ]", - "[ 3/4 → 7/8 | s:ht vowel:i ]", - "[ 7/8 → 1/1 | s:lt vowel:i ]", + "[ 0/1 → 1/8 | s:bd vowel:e ]", + "[ 1/8 → 1/4 | s:sd vowel:e ]", + "[ 1/4 → 3/8 | s:mt vowel:e ]", + "[ 3/8 → 1/2 | s:ht vowel:e ]", + "[ 1/2 → 5/8 | s:bd vowel:e ]", + "[ 11/16 → 3/4 | s:cp vowel:e ]", + "[ 3/4 → 7/8 | s:ht vowel:e ]", + "[ 7/8 → 1/1 | s:lt vowel:e ]", "[ 1/1 → 9/8 | s:bd vowel:a ]", "[ 9/8 → 5/4 | s:sd vowel:a ]", "[ 5/4 → 11/8 | s:mt vowel:a ]", @@ -12180,14 +12187,14 @@ exports[`runs examples > example "vowel" example index 1 1`] = ` "[ 43/16 → 11/4 | s:cp vowel:i ]", "[ 11/4 → 23/8 | s:ht vowel:i ]", "[ 23/8 → 3/1 | s:lt vowel:i ]", - "[ 3/1 → 25/8 | s:bd vowel:i ]", - "[ 25/8 → 13/4 | s:sd vowel:i ]", - "[ 13/4 → 27/8 | s:mt vowel:i ]", - "[ 27/8 → 7/2 | s:ht vowel:i ]", - "[ 7/2 → 29/8 | s:bd vowel:i ]", - "[ 59/16 → 15/4 | s:cp vowel:i ]", - "[ 15/4 → 31/8 | s:ht vowel:i ]", - "[ 31/8 → 4/1 | s:lt vowel:i ]", + "[ 3/1 → 25/8 | s:bd vowel:o ]", + "[ 25/8 → 13/4 | s:sd vowel:o ]", + "[ 13/4 → 27/8 | s:mt vowel:o ]", + "[ 27/8 → 7/2 | s:ht vowel:o ]", + "[ 7/2 → 29/8 | s:bd vowel:o ]", + "[ 59/16 → 15/4 | s:cp vowel:o ]", + "[ 15/4 → 31/8 | s:ht vowel:o ]", + "[ 31/8 → 4/1 | s:lt vowel:o ]", ] `; @@ -12302,23 +12309,23 @@ exports[`runs examples > example "wchoose" example index 0 1`] = ` "[ 0/1 → 1/5 | note:c2 s:sine ]", "[ 1/5 → 2/5 | note:g2 s:sine ]", "[ 2/5 → 3/5 | note:g2 s:sine ]", - "[ 3/5 → 4/5 | note:d2 s:bd n:6 ]", - "[ 4/5 → 1/1 | note:f1 s:bd n:6 ]", + "[ 3/5 → 4/5 | note:d2 s:sine ]", + "[ 4/5 → 1/1 | note:f1 s:sine ]", "[ 1/1 → 6/5 | note:c2 s:sine ]", "[ 6/5 → 7/5 | note:g2 s:sine ]", "[ 7/5 → 8/5 | note:g2 s:sine ]", "[ 8/5 → 9/5 | note:d2 s:sine ]", "[ 9/5 → 2/1 | note:f1 s:sine ]", "[ 2/1 → 11/5 | note:c2 s:sine ]", - "[ 11/5 → 12/5 | note:g2 s:triangle ]", + "[ 11/5 → 12/5 | note:g2 s:bd n:6 ]", "[ 12/5 → 13/5 | note:g2 s:sine ]", "[ 13/5 → 14/5 | note:d2 s:sine ]", "[ 14/5 → 3/1 | note:f1 s:sine ]", "[ 3/1 → 16/5 | note:c2 s:sine ]", "[ 16/5 → 17/5 | note:g2 s:sine ]", - "[ 17/5 → 18/5 | note:g2 s:triangle ]", + "[ 17/5 → 18/5 | note:g2 s:sine ]", "[ 18/5 → 19/5 | note:d2 s:sine ]", - "[ 19/5 → 4/1 | note:f1 s:sine ]", + "[ 19/5 → 4/1 | note:f1 s:bd n:6 ]", ] `; @@ -12329,30 +12336,30 @@ exports[`runs examples > example "wchooseCycles" example index 0 1`] = ` "[ 1/4 → 3/8 | s:bd ]", "[ 3/8 → 1/2 | s:bd ]", "[ 1/2 → 5/8 | s:bd ]", - "[ 5/8 → 3/4 | s:bd ]", - "[ 3/4 → 7/8 | s:sd ]", + "[ 5/8 → 3/4 | s:hh ]", + "[ 3/4 → 7/8 | s:bd ]", "[ 7/8 → 1/1 | s:bd ]", "[ 1/1 → 9/8 | s:bd ]", - "[ 9/8 → 5/4 | s:sd ]", + "[ 9/8 → 5/4 | s:bd ]", "[ 5/4 → 11/8 | s:bd ]", - "[ 11/8 → 3/2 | s:bd ]", + "[ 11/8 → 3/2 | s:sd ]", "[ 3/2 → 13/8 | s:bd ]", "[ 13/8 → 7/4 | s:bd ]", - "[ 7/4 → 15/8 | s:sd ]", + "[ 7/4 → 15/8 | s:bd ]", "[ 15/8 → 2/1 | s:bd ]", "[ 2/1 → 17/8 | s:bd ]", - "[ 17/8 → 9/4 | s:sd ]", - "[ 9/4 → 19/8 | s:bd ]", - "[ 19/8 → 5/2 | s:bd ]", + "[ 17/8 → 9/4 | s:hh ]", + "[ 9/4 → 19/8 | s:hh ]", + "[ 19/8 → 5/2 | s:hh ]", "[ 5/2 → 21/8 | s:bd ]", "[ 21/8 → 11/4 | s:bd ]", - "[ 11/4 → 23/8 | s:bd ]", - "[ 23/8 → 3/1 | s:bd ]", - "[ 3/1 → 25/8 | s:hh ]", - "[ 25/8 → 13/4 | s:bd ]", + "[ 11/4 → 23/8 | s:hh ]", + "[ 23/8 → 3/1 | s:sd ]", + "[ 3/1 → 25/8 | s:bd ]", + "[ 25/8 → 13/4 | s:hh ]", "[ 13/4 → 27/8 | s:bd ]", "[ 27/8 → 7/2 | s:bd ]", - "[ 7/2 → 29/8 | s:bd ]", + "[ 7/2 → 29/8 | s:sd ]", "[ 29/8 → 15/4 | s:bd ]", "[ 15/4 → 31/8 | s:bd ]", "[ 31/8 → 4/1 | s:bd ]", @@ -12367,48 +12374,48 @@ exports[`runs examples > example "wchooseCycles" example index 1 1`] = ` "[ 1/4 → 1/3 | s:bd ]", "[ 1/3 → 5/12 | s:bd ]", "[ 5/12 → 1/2 | s:bd ]", - "[ 1/2 → 7/12 | s:bd ]", - "[ 7/12 → 2/3 | s:bd ]", - "[ 2/3 → 3/4 | s:bd ]", - "[ 3/4 → 5/6 | s:bd ]", - "[ 5/6 → 11/12 | s:bd ]", - "[ 11/12 → 1/1 | s:bd ]", + "[ 1/2 → 7/12 | s:hh ]", + "[ 7/12 → 2/3 | s:hh ]", + "[ 2/3 → 3/4 | s:hh ]", + "[ 3/4 → 5/6 | s:hh ]", + "[ 5/6 → 11/12 | s:hh ]", + "[ 11/12 → 1/1 | s:hh ]", "[ 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:sd ]", - "[ 19/12 → 5/3 | s:sd ]", - "[ 5/3 → 7/4 | s:sd ]", - "[ 7/4 → 11/6 | s:bd ]", - "[ 11/6 → 23/12 | s:bd ]", - "[ 23/12 → 2/1 | s:bd ]", + "[ 5/4 → 4/3 | s:hh ]", + "[ 4/3 → 17/12 | s:hh ]", + "[ 17/12 → 3/2 | s:hh ]", + "[ 3/2 → 19/12 | s:bd ]", + "[ 19/12 → 5/3 | s:bd ]", + "[ 5/3 → 7/4 | s:bd ]", + "[ 7/4 → 11/6 | s:hh ]", + "[ 11/6 → 23/12 | s:hh ]", + "[ 23/12 → 2/1 | s:hh ]", "[ 2/1 → 25/12 | s:hh ]", "[ 25/12 → 13/6 | s:hh ]", "[ 13/6 → 9/4 | s:hh ]", - "[ 9/4 → 7/3 | s:sd ]", - "[ 7/3 → 29/12 | s:sd ]", - "[ 29/12 → 5/2 | s:sd ]", + "[ 9/4 → 7/3 | s:hh ]", + "[ 7/3 → 29/12 | s:hh ]", + "[ 29/12 → 5/2 | s:hh ]", "[ 5/2 → 31/12 | s:hh ]", "[ 31/12 → 8/3 | s:hh ]", "[ 8/3 → 11/4 | s:hh ]", - "[ 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 ]", + "[ 11/4 → 17/6 | s:sd ]", + "[ 17/6 → 35/12 | s:sd ]", + "[ 35/12 → 3/1 | s:sd ]", + "[ 3/1 → 37/12 | s:hh ]", + "[ 37/12 → 19/6 | s:hh ]", + "[ 19/6 → 13/4 | s:hh ]", "[ 13/4 → 10/3 | s:bd ]", "[ 10/3 → 41/12 | s:bd ]", "[ 41/12 → 7/2 | s:bd ]", - "[ 7/2 → 43/12 | s:sd ]", - "[ 43/12 → 11/3 | s:sd ]", - "[ 11/3 → 15/4 | s:sd ]", - "[ 15/4 → 23/6 | s:hh ]", - "[ 23/6 → 47/12 | s:hh ]", - "[ 47/12 → 4/1 | s:hh ]", + "[ 7/2 → 43/12 | s:bd ]", + "[ 43/12 → 11/3 | s:bd ]", + "[ 11/3 → 15/4 | s:bd ]", + "[ 15/4 → 23/6 | s:bd ]", + "[ 23/6 → 47/12 | s:bd ]", + "[ 47/12 → 4/1 | s:bd ]", ] `; @@ -12432,15 +12439,15 @@ exports[`runs examples > example "wchooseCycles" example index 2 1`] = ` "[ 5/4 → 4/3 | s:hh ]", "[ 4/3 → 17/12 | s:hh ]", "[ 17/12 → 3/2 | s:hh ]", - "[ 3/2 → 19/12 | s:hh ]", - "[ 19/12 → 5/3 | s:hh ]", - "[ 5/3 → 7/4 | s:hh ]", + "[ 3/2 → 49/32 | s:bd ]", + "[ 51/32 → 13/8 | s:bd ]", + "[ 27/16 → 55/32 | s:bd ]", "[ 7/4 → 11/6 | s:hh ]", "[ 11/6 → 23/12 | s:hh ]", "[ 23/12 → 2/1 | s:hh ]", - "[ 2/1 → 25/12 | s:hh ]", - "[ 25/12 → 13/6 | s:hh ]", - "[ 13/6 → 9/4 | s:hh ]", + "[ 2/1 → 65/32 | s:bd ]", + "[ 67/32 → 17/8 | s:bd ]", + "[ 35/16 → 71/32 | s:bd ]", "[ 9/4 → 7/3 | s:hh ]", "[ 7/3 → 29/12 | s:hh ]", "[ 29/12 → 5/2 | s:hh ]", @@ -12450,15 +12457,15 @@ exports[`runs examples > example "wchooseCycles" example index 2 1`] = ` "[ 11/4 → 17/6 | s:hh ]", "[ 17/6 → 35/12 | s:hh ]", "[ 35/12 → 3/1 | s:hh ]", - "[ 3/1 → 97/32 | s:bd ]", - "[ 99/32 → 25/8 | s:bd ]", - "[ 51/16 → 103/32 | s:bd ]", + "[ 3/1 → 37/12 | s:hh ]", + "[ 37/12 → 19/6 | s:hh ]", + "[ 19/6 → 13/4 | s:hh ]", "[ 13/4 → 10/3 | s:hh ]", "[ 10/3 → 41/12 | s:hh ]", "[ 41/12 → 7/2 | s:hh ]", - "[ 7/2 → 43/12 | s:hh ]", - "[ 43/12 → 11/3 | s:hh ]", - "[ 11/3 → 15/4 | s:hh ]", + "[ 7/2 → 113/32 | s:bd ]", + "[ 115/32 → 29/8 | s:bd ]", + "[ 59/16 → 119/32 | s:bd ]", "[ 15/4 → 23/6 | s:hh ]", "[ 23/6 → 47/12 | s:hh ]", "[ 47/12 → 4/1 | s:hh ]", From e90aa5202d5f4f0ee5ad8c161cd02907512f2bfa Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 16 Nov 2025 15:28:10 -0600 Subject: [PATCH 056/194] Export withseed --- packages/core/signal.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 9f3a264f..e50ba22d 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -365,13 +365,13 @@ export const scramble = register('scramble', (n, pat) => { * @param {Pattern} pat Pattern to update * @returns Pattern */ -function withSeed(func, pat) { +export const withSeed = (func, pat) => { return new Pattern((state) => { let { randSeed, ...controls } = state.controls; randSeed = func(randSeed); return pat.query(state.setControls({ ...controls, randSeed })); }, pat._steps); -} +}; /** * Change the seed for random signals. Normally, random signals depend on time, From 48407c309cca95b87d541e3a420659ff2f04abdd Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 17 Nov 2025 23:10:10 +0100 Subject: [PATCH 057/194] add basic dough repl --- packages/codemirror/codemirror.mjs | 6 +- website/src/components/Dough/Dough.astro | 17 +++ website/src/components/Dough/dough-mirror.mjs | 139 ++++++++++++++++++ website/src/components/Dough/dough-repl.mjs | 105 +++++++++++++ website/src/pages/dough/index.astro | 15 ++ 5 files changed, 279 insertions(+), 3 deletions(-) create mode 100644 website/src/components/Dough/Dough.astro create mode 100644 website/src/components/Dough/dough-mirror.mjs create mode 100644 website/src/components/Dough/dough-repl.mjs create mode 100644 website/src/pages/dough/index.astro diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 69fce4b5..ad27fd30 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -27,7 +27,7 @@ import { updateWidgets, widgetPlugin } from './widget.mjs'; export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands'; -const extensions = { +export const extensions = { isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []), isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []), isBracketClosingEnabled: (on) => (on ? closeBrackets() : []), @@ -48,7 +48,7 @@ const extensions = { ] : [], }; -const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); +export const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); export const defaultSettings = { keybindings: 'codemirror', @@ -411,7 +411,7 @@ export class StrudelMirror { } } -function parseBooleans(value) { +export function parseBooleans(value) { return { true: true, false: false }[value] ?? value; } diff --git a/website/src/components/Dough/Dough.astro b/website/src/components/Dough/Dough.astro new file mode 100644 index 00000000..91b50e07 --- /dev/null +++ b/website/src/components/Dough/Dough.astro @@ -0,0 +1,17 @@ +--- +import '../../repl/Repl.css'; +--- + +
+ + + diff --git a/website/src/components/Dough/dough-mirror.mjs b/website/src/components/Dough/dough-mirror.mjs new file mode 100644 index 00000000..f20fd2b4 --- /dev/null +++ b/website/src/components/Dough/dough-mirror.mjs @@ -0,0 +1,139 @@ +import { + initEditor, + codemirrorSettings, + flash, + compartments, + extensions, + parseBooleans, + activateTheme, + updateMiniLocations, + highlightMiniLocations, +} from '@strudel/codemirror'; +import { evalScope } from '@strudel/core'; +import { Framer } from '@strudel/draw'; +import { persistentAtom } from '@nanostores/persistent'; +import { DoughRepl } from './dough-repl.mjs'; + +const initialCode = '$: note("c a f e")'; +export const code = persistentAtom('vanilla-repl-code', initialCode, { + encode: JSON.stringify, + decode: JSON.parse, +}); + +export class DoughMirror { + constructor(options) { + const { root, initialCode = code.get(), bgFill = true } = options; + this.root = root; + this.code = initialCode; + this.repl = new DoughRepl(); + this.prebaked = this.prebake(); + + // init codemirror + this.editor = initEditor({ + root, + initialCode: this.code, + onChange: (v) => { + if (v.docChanged) { + this.code = v.state.doc.toString(); + code.set(this.code); + } + }, + onEvaluate: this.evaluate.bind(this), + onStop: () => this.stop(), + mondo: false, + }); + const settings = codemirrorSettings.get(); + this.setFontSize(settings.fontSize); + this.setFontFamily(settings.fontFamily); + + // init event highlighting + this.framer = new Framer( + () => { + const frameHaps = this.repl.processHaps(); + highlightMiniLocations(this.editor, time, frameHaps); + }, + (err) => console.log('Framer error', err), + ); + } + prebake() { + const modulesLoading = evalScope(import('@strudel/core'), import('@strudel/tonal'), import('@strudel/mini')); + return Promise.all([modulesLoading, this.repl.prebake()]); + } + async evaluate() { + this.framer.start(); + this.flash(); + await this.prebaked; + const { miniLocations } = await this.repl.evaluate(this.code); + updateMiniLocations(this.editor, miniLocations); + } + stop() { + this.repl.stop(); + this.framer.stop(); + highlightMiniLocations(this.editor, 0, []); + } + // added synonym (compared to StrudelMirror) + settings(settings = {}) { + this.updateSettings(settings); + } + + // the rest is copy pasted from StrudelMirror: + + updateSettings(settings = {}) { + settings.fontSize && this.setFontSize(settings.fontSize); + settings.fontFamily && this.setFontFamily(settings.fontFamily); + for (let key in extensions) { + if (key in settings) { + this.reconfigureExtension(key, settings[key]); + } + } + const updated = { ...codemirrorSettings.get(), ...settings }; + // console.log(updated); + codemirrorSettings.set(updated); + } + reconfigureExtension(key, value) { + if (!extensions[key]) { + console.warn(`extension ${key} is not known`); + return; + } + value = parseBooleans(value); + const newValue = extensions[key](value, this); + this.editor.dispatch({ + effects: compartments[key].reconfigure(newValue), + }); + if (key === 'theme') { + activateTheme(value); + } + } + flash(ms) { + flash(this.editor, ms); + } + setFontSize(size) { + this.root.style.fontSize = size + 'px'; + } + setFontFamily(family) { + this.root.style.fontFamily = family; + const scroller = this.root.querySelector('.cm-scroller'); + if (scroller) { + scroller.style.fontFamily = family; + } + } + setCode(code, offset = 0) { + const changes = { + from: 0, + to: this.editor.state.doc.length + offset, + insert: code, + }; + this.editor.dispatch({ changes }); + } + getCursorLocation() { + return this.editor.state.selection.main.head; + } + setCursorLocation(col) { + return this.editor.dispatch({ selection: { anchor: col } }); + } + appendCode(code) { + const cursor = this.getCursorLocation(); + this.setCode(this.code + code); + this.setCursorLocation(cursor); + } +} diff --git a/website/src/components/Dough/dough-repl.mjs b/website/src/components/Dough/dough-repl.mjs new file mode 100644 index 00000000..64364708 --- /dev/null +++ b/website/src/components/Dough/dough-repl.mjs @@ -0,0 +1,105 @@ +// import { Dough, doughsamples } from 'dough-synth'; +import { Dough, doughsamples } from 'https://unpkg.com/dough-synth@0.1.9/dough.js'; +import { Pattern, noteToMidi, evaluate } from '@strudel/core'; +// import doughUrl from 'dough-synth?url'; +import { transpiler } from '@strudel/transpiler'; +//const doughBaseUrl = doughUrl.split('/').slice(0, -1).join('/') + '/'; +const doughBaseUrl = 'https://unpkg.com/dough-synth@0.1.9/'; + +export class DoughRepl { + pattern; + latency = 0.1; + cps = 0.5; + origin; + t0; + lasttime; + strudel; + q = []; + constructor() { + this.ready = this.init(); + } + async init() { + // init dough immediately, so that it can attach the document click event to initAudio immediately + this.dough = new Dough({ + base: doughBaseUrl, + //base: "../", // local dev + onTick: ({ t0, t1 }) => { + if (!this.pattern) { + return; + } + this.origin ??= t0; + this.t0 = t0; + this.lasttime = performance.now(); + const a = (t0 - this.origin) * this.cps; + const b = (t1 - this.origin) * this.cps; + + const haps = this.pattern.queryArc(a, b).filter((hap) => hap.hasOnset()); + if (!haps.length) { + return; + } + haps.forEach((hap) => { + const time = hap.whole.begin.valueOf() / this.cps + this.origin + this.latency; + const duration = hap.duration.valueOf() / this.cps; + const event = { + dough: 'play', + ...hap.value, + time, + duration, + }; + if (event.note && typeof event.note === 'string') { + event.note = noteToMidi(event.note); + } + //console.log("event", JSON.stringify(Object.entries(event))); + this.dough.evaluate(event); + this.q.push({ event, hap }); + }); + }, + }); + // miniAllStrings(); + const setcps = (cps) => (this.cps = cps); + const setcpm = (cpm) => setcps(cpm / 60); + const replScope = { setcps, setcpm }; + Object.assign(globalThis, replScope); + } + async evaluate(code) { + await this.ready; + let patterns = []; + Pattern.prototype.p = function (id) { + if (!id.startsWith('_')) { + patterns.push(this); + } + }; + + let { meta } = await evaluate(code, transpiler, { addReturn: false, wrapAsync: true, emitWidgets: false }); + const { miniLocations } = meta; + + this.pattern = stack(...patterns); + return { miniLocations, pattern: this.pattern }; + } + stop() { + this.pattern = undefined; + this.origin = undefined; + } + prebake() { + return Promise.all([doughsamples('github:eddyflux/crate')]); + } + // tbd: move this to dough-synth + get time() { + return this.t0 + (performance.now() - this.lasttime) / 1000 + this.latency; + } + processHaps() { + const currentHaps = []; + const time = this.time; + this.q = this.q.filter(({ event, hap }) => { + const end = event.time + event.duration; + const isActive = time >= event.time && time <= end; + if (isActive) { + currentHaps.push(hap); + } + return end > time; // delete old events + // we do NOT return !isActive, because a frame might miss an event, which would cause a leak + }); + // console.log(this.q.length); // to check for leaks + return currentHaps; + } +} diff --git a/website/src/pages/dough/index.astro b/website/src/pages/dough/index.astro new file mode 100644 index 00000000..9909fc6d --- /dev/null +++ b/website/src/pages/dough/index.astro @@ -0,0 +1,15 @@ +--- +import HeadCommon from '../../components/HeadCommon.astro'; +import Dough from '../../components/Dough/Dough.astro'; +--- + + + + + Strudel Dough REPL + + + + + + From 3b1c75d3de236d942f0191d54c237c9ed950547e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 17 Nov 2025 23:18:31 +0100 Subject: [PATCH 058/194] fix: linting errors --- website/src/components/Dough/dough-mirror.mjs | 2 +- website/src/components/Dough/dough-repl.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/components/Dough/dough-mirror.mjs b/website/src/components/Dough/dough-mirror.mjs index f20fd2b4..5cdfeb3f 100644 --- a/website/src/components/Dough/dough-mirror.mjs +++ b/website/src/components/Dough/dough-mirror.mjs @@ -48,7 +48,7 @@ export class DoughMirror { // init event highlighting this.framer = new Framer( - () => { + (time) => { const frameHaps = this.repl.processHaps(); highlightMiniLocations(this.editor, time, frameHaps); }, diff --git a/website/src/components/Dough/dough-repl.mjs b/website/src/components/Dough/dough-repl.mjs index 64364708..ef28617e 100644 --- a/website/src/components/Dough/dough-repl.mjs +++ b/website/src/components/Dough/dough-repl.mjs @@ -1,6 +1,6 @@ // import { Dough, doughsamples } from 'dough-synth'; import { Dough, doughsamples } from 'https://unpkg.com/dough-synth@0.1.9/dough.js'; -import { Pattern, noteToMidi, evaluate } from '@strudel/core'; +import { Pattern, noteToMidi, evaluate, stack } from '@strudel/core'; // import doughUrl from 'dough-synth?url'; import { transpiler } from '@strudel/transpiler'; //const doughBaseUrl = doughUrl.split('/').slice(0, -1).join('/') + '/'; From 0bb89d1a9f26b6bcecae3bf4694731bc47965437 Mon Sep 17 00:00:00 2001 From: scrappy_fiddler Date: Wed, 19 Nov 2025 20:09:14 +0100 Subject: [PATCH 059/194] New page FAQ in "More" --- website/src/config.ts | 1 + website/src/pages/learn/faq.mdx | 403 ++++++++++++++++++++++++++++++++ 2 files changed, 404 insertions(+) create mode 100644 website/src/pages/learn/faq.mdx diff --git a/website/src/config.ts b/website/src/config.ts index f335b272..eb39d295 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -76,6 +76,7 @@ export const SIDEBAR: Sidebar = { { text: 'MIDI & OSC', link: 'learn/input-output' }, ], More: [ + { text: 'FAQ', link: 'learn/faq' }, { text: 'Recipes', link: 'recipes/recipes' }, { text: 'Mini-Notation', link: 'learn/mini-notation' }, { text: 'Visual Feedback', link: 'learn/visual-feedback' }, diff --git a/website/src/pages/learn/faq.mdx b/website/src/pages/learn/faq.mdx new file mode 100644 index 00000000..30f2986b --- /dev/null +++ b/website/src/pages/learn/faq.mdx @@ -0,0 +1,403 @@ +--- +title: Frequently Asked Questions +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; + +# Frequently Asked Questions + +This page contains frequently asked questions. Usually, the topic is explained in more detail in a section which is linked in the answer. This page's aim is to give an overview over topics which are important to the users of strudel. + +## Is Strudel/Tidal free? + +Yes - there is no charge, this is a collective open source project, and the music you make with it is your own. + +However there are some caveats - the source code must stay free, i.e. you cannot distribute strudel or tidal as part of projects with incompatible licenses - see the [license]() for details. The contributed examples and tracks are also separately licensed, and must not e.g. be used to train AI models without permission. + +## How to record or export audio? + +There are multiple ways to record the audio -- and video -- output of Strudel: + - capture the raw stereo signal coming out of your web browser. + - use the alternative SuperDirt audio engine. Read [this page](/learn/input-output/#oscsuperdirtstrudeldirt) to know more about it. + - capture the audio/video stream using a capture tool such as [OBS](). + - don't record anything and code it again in front of your friends. + +You will need an external audio editor/DAW such as Reaper/Audacity/Ardour, etc. Strudel itself does not have functionality for exporting stems / individual tracks to an audio or midi file. Strudel is not a digital audio workstation and does not operate following the same principles shared by most traditional audio softwares. + +## Can I use strudel with my IDE? + +Yes you can. There are experimental modes, made by community members, for several IDEs such as: + - VS Code: [Strudel VS](): an experimental mode for Microsoft VSCode. A revived version of [TidalStrudel](), which is defunct. + - nvim: [strudel.nvim](https://github.com/gruvw/strudel.nvim) + +## How can I record samples? + +You can use your own samples with Strudel. There are multiple ways to load your sample collection. Some methods are good for quick experimentation, some others are good to share your audio collection with other musicians: + + - Import folders [from the interface](/learn/samples/#from-disk-via-import-sounds-folder) + - Host your sound library e.g. on github and [load them from an URL](/learn/samples/#loading-custom-samples) + +## Can I use Strudel with AI/LLM tools? + +You are free to do what you like with Strudel, within the terms of the free/open source AGPLv3 license. However as a community we are interested in exploring human creativity. AI is *way* over-hyped right now, including by people with very shady motives. Many in the community are very wary of people training models on their tunes that they've poured their love into. So please keep discussion and questions around AI and LLMs to channels dedicated to the topic and be fully respectful of other people's work. + +Furthermore, tools like ChatGPT generally give wrong answers. Please don't ask the community to fix those answers for you, as generally they will be timewasting nonsense. + +Human questions only! + +## How to run offline? + +Strudel works offline just fine! There are multiple techniques to run it yourself, see [this explanation](learn/pwa/#using-strudel-offline). + +## How to change tempo? How do I translate BPM to cpm? + +If you have your tempo in beats per minute and use 4 beats per cycle (e.g. if your track is in 4/4ths) then you can do `setcpm(BPM/4)` where BPM is your beats per minute. + +If you have a different number of beats per bar or are using more or less beats per cycle (e.g. If you want to put only half a bar or two bars into one cycle), adjust accordingly. + +## Where can I see all the functions? + +If you pop open the sidetab of strudel.cc (small white < on the right hand side), there is a tab "reference" which lists all the functions of strudel. + +## Where can I see all the samples and synths? + +If you pop open the sidetab of strudel.cc (small white < on the right hand side), there is a tab "sounds" which lists all the drum machines, samples and synths currently loaded. + +## How do I use this exactly like a DAW? + +Short answer: you don't. + +Long answer: you can use Strudel to work along your creative work in a DAW. There are many ways to do so. + +If you want to emulate the functionality of a DAW in a live coding language, you'll have to identify the operations executed by the DAW (sequencing, repeating, applying filters and envelopes) and write code that is equivalent to these operations. You might then find that the typical DAW workflow is not really adapted to live coding (because, despite both being ways of making music on the computer, they are two very different tools) and adapt your way of proceeding to the medium of code. This might mean leaving more place to serendipity and writing code that you don't predict the output of. + +## Why doesn't everyone just use a DAW? + +There is no easy answer to this question. Here are some thoughts: + - Live coding tools such as Strudel are excellent for improvising music and visuals using a computer. DAWs are valuable and robust companions for other activities such as producing, mastering and mixing audio, among other usages. Using a tool does not exclude from using any another tool, just build a toolbox. + - Live coding has been practiced for quite some time as a performative activity. Artists like to show their screens while playing in front of an audience. It is an essential part of what they do, of the way they share their activity with everybody. + - Code is a human language, it is made for other humans to read it. You can read the code and enjoy the music too. It has meaning, value, and there might even be something poetic/important about it! + - Strudel is free and open source, you can inspect the code, reshape it, contribute to it if you can/want. It is not opaque and this matters for many people. There is no black box, no obscure abstractions, no business model or hidden features. We need open tools in the arts! + - Live coders don't shy away from using DAWs. They use them all of the time, especially when it makes their life easier for... live coding! + - Code is an artistic material like any other. There is something valuable in the process of making music through code. More generally speaking, it is nice to tackle creative problems through the use of a programming language: creative thinking, building up your own solutions, DIY approach to music-making, unexpected outcome of algorithms, funny human errors, etc. + - There are pianos and trumpets in your DAW: why do people continue playing the piano or the trumpet? Think of live coding tools as instruments that you activate through the act of programming. + +## Is it more efficient to use Strudel than a DAW? + +Strudel was not build to be a DAW, yet it can still be used to make covers, arrange tracks, or prepare patterns for jamming. When playing concerts or jamming, some livecoders prepare their code, some perform from scratch. + +It might be interesting for you to check out for yourself how strudel can be used to express yourself creatively. Also you are free to combine a language like Strudel with a DAW. + +## How can I interface Strudel with my favorite music software? What can I do with it? + +Strudel can send [MIDI and OSC](/learn/input-output/), which are protocols for communicating musical information. + +Another music software (or hardware!) can then listen to these messages and process them according to its capabilities. + +A simple example would be to send livecoded audio to Ableton on different tracks and then use it to mix them. + +You could also send the MIDI of a sequenced pattern to Musescore and then have it transcribe your livecoded work as a musical score. + +You could also send MIDI to your hardware synths because you think they sound better than the software synths built-in Strudel. + +## How do I use this in my closed source webgame? + +You don't. You would need to re-license your game to AGPLv3 to fulfill the license Strudel is distributed under. + +## Where can I download loads of patterns to train my LLM? + +You cannot, as there is no such place. For details regarding our stance towards AI/LLM, see [above](/learn/faq/#can-i-use-strudel-with-aillm-tools) + +## How to play different patterns simultaneously? + +Using the $ operator, several patterns can be played at once: + + +See also [stack](intro/#combining-patterns) + +## Is it possible to mute a pattern? + +With an additional underscore, a pattern can be muted. + + + +See also [hush](/learn/conditional-modifiers/#hush) + +## How can I arrange in Strudel using `mask`? + +With mini-notation, using the `<>` and `!` operators, you can try something like +``` +.mask("<0!24 1!40>") +``` +It mutes a pattern for 24 cycles and plays it for 40. You would gain 64 cycles total, a multiple of 2/4/8 commonly used in western music. + +If each cycle is a bar, as a starting point, you could write a mask like that for any pattern: +``` +.mask("<0!16 0!16 0!16 0!16 0!16 0!10>") +``` +It mutes it throughout. + +For arranging, you could add the same mask to each part and replace some zeroes with ones in your different masks to make parts play. + +If you use `.mask()` on different patterns mess up your counting, then patterns do not align anymore. +On the other hand, doing that on purpose is one of the things that could be considered a strength of tidalcycles and Strudel. +You can make things quite lively and more organic with a little (controlled) interference, according to your own taste. +And you are free to arrange in cycles like 3, 6 or 9 too. + +To modify everything at once, you could try all and when, for example: + +``` +all(x=>x.when("<0!7 1>", x=>x.lpf(saw.range(200, 2000)))) +``` + +This would lowpass filter sweep everything every 8 cycles. + +## How can I arrange in Strudel using `arrange` or `pick`? + +Take [Pachelbel's Canon in D](https://en.wikipedia.org/wiki/Pachelbel%27s_Canon#Analysis) as an example which has 4 voices (one cello and 3 violins) which have repeating patterns, as seen in the link above. + +The following snipped defines the patterns as constants which can then be used for the different voices. `arrange` takes multiple arguments, which are each a number of cycles and a pattern which is played for the number of cycles, wrapped in `[]` If the pattern is shorter than the number, it is repeated. +") + .color("grey").sound("gm_tremolo_strings:3") + const violin_p1 = note( + "<[f#5 e5 d5 c#5] [b4 a4 b4 c#5]>") + .color("blue") + const violin_p2 = note( + "<[d5 c#5 b4 a4] [ g4 f#4 g4 f#4]>") + .color("green") + const violin_p3 = note( + "<[d4 f#4 a4 g4 f#4 d4 f#4 e4] [d4 b3 d4 a4 g4 b4 a4 g4]>") + .color("purple") + const violin_p4 = note( + "<[f#4 d4 e4 c#5 d5 f#5 a5 a4] [b4 g4 a4 f#4 d4 d5 [d5@3 c#5]@2]>") + .color("red") + + + cello$: arrange( + [2, silence], + [18,cello]) + violin1$: arrange( + [4,silence], + [2,violin_p1], [2,violin_p2], + [2,violin_p3], [2,violin_p4], + [2,violin_p1], [2,violin_p2], + [2,violin_p3], [2,violin_p4] + ).sound("gm_tremolo_strings:0") + violin2$: arrange( + [6,silence], [2,violin_p1], + [2,violin_p2], [2,violin_p3], + [2,violin_p4], [2,violin_p1], + [2,violin_p2], [2,violin_p3] + ).sound("gm_tremolo_strings:1") + violin3$: arrange( + [8,silence], + [2,violin_p1], [2,violin_p2], + [2,violin_p3], [2,violin_p4], + [2,violin_p1], [2,violin_p2] + ).sound("gm_tremolo_strings:2") + + all(x => x.release(.2)) +`} /> + +Alternatively, you can also put the different patterns for the violins into one single array (`const violins = [violin_p1, violin_p2, violin_p3, violin_p4]`) and use a pattern as an index to `pick` the nth element of that array. This replaces the voices defined above. Here you use `0@2` to specifiy that the first item (i.e. with index `0`) is played for `2` cycles. + +`pick` has better highlighting than `arrange`: + +") + .color("grey").sound("gm_tremolo_strings:3") + const violin_p1 = note( + "<[f#5 e5 d5 c#5] [b4 a4 b4 c#5]>") + .color("blue") + const violin_p2 = note( + "<[d5 c#5 b4 a4] [ g4 f#4 g4 f#4]>") + .color("green") + const violin_p3 = note( + "<[d4 f#4 a4 g4 f#4 d4 f#4 e4] [d4 b3 d4 a4 g4 b4 a4 g4]>") + .color("purple") + const violin_p4 = note( + "<[f#4 d4 e4 c#5 d5 f#5 a5 a4] [b4 g4 a4 f#4 d4 d5 [d5@3 c#5]@2]>") + .color("red") + +const violins = [violin_p1, violin_p2, violin_p3, violin_p4] + +cello$: "<~@2 0@18>".pick([cello]) +violin1$: "<~@4 0@2 1@2 2@2 3@2 0@2 1@2 2@2 3@2>".pick(violins) +.sound("gm_tremolo_strings:0") +violin2$: "<~@6 0@2 1@2 2@2 3@2 0@2 1@2 2@2>".pick(violins) +.sound("gm_tremolo_strings:1") +violin3$: "<~@8 0@2 1@2 2@2 3@2 0@2 1@2 >".pick(violins) +.sound("gm_tremolo_strings:2") +all(x => x.release(.2)) +`} /> + +The `pick` method also works with jsons which have named elements, which makes it easier to read, see the [here](/learn/conditional-modifiers/#pick). `pickRestart` restarts the pattern upon picking it which can make a difference if the duration of the pick indexes doesn't line up with the patterns which are picked - which is not the case here. + +Try adding `.punchcard()` after the `release(.2)` for a visualization. + +## I saw Switch Angel using functions which I cannot find in the reference (e.g. `trancegate`). How do I make it work? + +Methods like `trancegate()`, `rlpf()` and `acidenv()` are currently not pattern methods which come natively with strudel. + +It's part of a script for strudel which was written by Switch Angel and published [here](https://github.com/switchangel/strudel-scripts/blob/main/allscripts.js) + +Note that this script defines functions and registers methods which `trancegate()` will depend on so just pasting the `trancegate()` method from that script alone will not suffice, but you will also need the `fill` method. + +If you copy the full script into your strudel, be mindful of lines which try to load local samples - these should be deleted or prefixed with `//` as a comment. + +If you paste these functions into your strudel session, hit "Update" and delete the source of the functions, the functions will still be availabe until you do a browser refresh. + +## Is there difference between `n` and `note`? + +They are not aliases of each other, in contrast to `s` and `sound`. + +The method `note` is used to reference a certain note (either as its name, such as `c` or `b2` or the midi number `69`, for example `note("c3 e3 g3")`). + +On the other hand, `n` is a way to reference the nth index of something. This something can be a scale (eg `n("0 2 4").scale("C:major")`) , but it can also be a particular note in a chord (see https://strudel.cc/recipes/recipes/#arpeggios for an example) . + +The method `n` can also be used for something completely unrelated to notes like the nth sample from a sample map `s("hh*8").bank("RolandTR909").n("0 1 2 3")`. Note that `pick` does *not* use `n`. + +".pickRestart([ + n("0 1 2 0"), + n("2 3 4 ~"), + n("[4 5] [4 3] 2 0"), + n("0 -3 0 ~") + ]).scale("C:major").s("piano")`} /> + + +## Is there a cheat sheet for all symbols? + +Yes! + +``` +' marks start and end of strings, is different from " +" marks start and end of single line patterns in mini notation, is different from ' +` marks start and end of patterns with line breaks in mini notation, is different from ' +[] used for patterns, each item in it has the same length +<> used for patterns, alternates each item each cycle +{} historically used for polyrhythmic patterns. {a b c}%4 is the same as *4. +@3 elongates the item by a factor of 3 (other numbers work too) +@ after an item: elongates the item once (multiple @ work too c @ @ is the same as c@3) +_ after an item: also elongates an item once (multiple _ work too c _ _ is the same as c@3) +. this divides equal parts of a pattern and is called a foot. Can be used instead of [] like this: "1 6 7 8 . 2 . 3 . 4" is the same as "[1 6 7 8] 2 3 4" +- silence +~ also silence +x not silence (for the use in struct, same as 1) +s increase a note of a scale by one semitone, i.e. sharp +b decrease a note of a scale by one semitone, i.e. flat +# used in mondo notation +# also used for names of chords, like F# +*3 play the sample or pattern at thrice the speed, fast(3) +!3 play the sample or pattern three times +/2 play the sample or pattern at half speed, slow(2) +? play the pattern sometimes +| once per cycle, choose randomly a pattern of those separated by i.e. chooseCycles() +, play all items separated by it at the same time, i.e. stack() +: is used to separate multiple parameters, such as adsr(".1:.1:.5:.2") +$: at the start of a line, defines a member of the stack. is the only stack name that should occur multiple names +_ before a stack name: mutes the stack, i.e. hush(), for example _$: s("bd") +``` + +## Are there more FAQ items? + +These pages have been taken from [this pad](https://doc.patternclub.org/_CgofWouTciXXHexUP9AVg?both). Some of the items there have not been brushed up and brought here. + +These include the following items: 9, 11, 12 and 19 + +# Coding Syntax (old Content from code) + +Let's take a step back and understand how the syntax in Strudel works. + +Take a look at this simple example: + + + +- We have a word `note` which is followed by some brackets `()` with some words/letters/numbers inside, surrounded by quotes `"c a f e"` +- Then we have a dot `.` followed by another similar piece of code `s("piano")`. +- We can also see these texts are _highlighted_ using colours: word `note` is purple, the brackets `()` are grey, and the content inside the `""` are green. (The colors could be different if you've changed the default theme) + +What happens if we try to 'break' this pattern in different ways? + + + + + + + +Ok, none of these seem to work... + + + +This one does work, but now we only hear the first note... + +So what is going on here? + +# Functions, arguments and chaining + +So far, we've seen the following syntax: + +``` +xxx("foo").yyy("bar") +``` + +Generally, `xxx` and `yyy` are called [_functions_](), while `foo` and `bar` are called function [_arguments_ or _parameters_](). +So far, we've used the functions to declare which aspect of the sound we want to control, and their arguments for the actual data. +The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is preceded with a dot (`.`). + +Generally, the idea with chaining is that code such as `a("this").b("that").c("other")` allows `a`, `b` and `c` functions to happen in a specified order, without needing to write them as three separate lines of code. +You can think of this as being similar to chaining audio effects together using guitar pedals or digital audio effects. + +Strudel makes heavy use of chained functions. Here is a more sophisticated example: + + + +## Write your own chained function + +You can write your own chained function using `register`. Here's the above chain but registered as a reusable, chained function. + + pat + .s("sawtooth") + .cutoff(500) + //.delay(0.5) + .room(0.5) + ) +note("a3 c#4 e4 a4").effectChain()`} +/> + +Try adding `.rev()` after `effectChain()` to hear further effects added. + +# Comments + +The `//` in the example above is a line comment, resulting in the `delay` function being ignored. +It is a handy way to quickly turn code on and off. +Try uncommenting this line by deleting `//` and refreshing the pattern. +You can also use the keyboard shortcut `cmd-/` to toggle comments on and off. + +You might noticed that some comments in the REPL samples include some words starting with a "@", like `@by` or `@license`. +Those are just a convention to define some information about the music. We will talk about it in the [Music metadata](/learn/metadata) section. + +# Strings + +Ok, so what about the content inside the quotes (e.g. `"c a f e"`)? +In JavaScript, as in most programming languages, this content is referred to as being a [_string_](). +A string is simply a sequence of individual characters. +In TidalCycles, double quoted strings are used to write _patterns_ using the mini-notation, and you may hear the phrase _pattern string_ from time to time. +If you want to create a regular string and not a pattern, you can use single quotes, e.g. `'C minor'` will not be parsed as Mini Notation. + +The good news is, that this covers most of the JavaScript syntax needed for Strudel! + +
From 371e6c7d6d4f1fdd780c6a07302049ae069c1a08 Mon Sep 17 00:00:00 2001 From: scrappy_fiddler Date: Wed, 19 Nov 2025 20:17:19 +0100 Subject: [PATCH 060/194] tidy up FAQ --- website/src/pages/learn/faq.mdx | 93 --------------------------------- 1 file changed, 93 deletions(-) diff --git a/website/src/pages/learn/faq.mdx b/website/src/pages/learn/faq.mdx index 30f2986b..dbce4c70 100644 --- a/website/src/pages/learn/faq.mdx +++ b/website/src/pages/learn/faq.mdx @@ -308,96 +308,3 @@ _ before a stack name: mutes the stack, i.e. hush(), for example _$: s("bd") These pages have been taken from [this pad](https://doc.patternclub.org/_CgofWouTciXXHexUP9AVg?both). Some of the items there have not been brushed up and brought here. These include the following items: 9, 11, 12 and 19 - -# Coding Syntax (old Content from code) - -Let's take a step back and understand how the syntax in Strudel works. - -Take a look at this simple example: - - - -- We have a word `note` which is followed by some brackets `()` with some words/letters/numbers inside, surrounded by quotes `"c a f e"` -- Then we have a dot `.` followed by another similar piece of code `s("piano")`. -- We can also see these texts are _highlighted_ using colours: word `note` is purple, the brackets `()` are grey, and the content inside the `""` are green. (The colors could be different if you've changed the default theme) - -What happens if we try to 'break' this pattern in different ways? - - - - - - - -Ok, none of these seem to work... - - - -This one does work, but now we only hear the first note... - -So what is going on here? - -# Functions, arguments and chaining - -So far, we've seen the following syntax: - -``` -xxx("foo").yyy("bar") -``` - -Generally, `xxx` and `yyy` are called [_functions_](), while `foo` and `bar` are called function [_arguments_ or _parameters_](). -So far, we've used the functions to declare which aspect of the sound we want to control, and their arguments for the actual data. -The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is preceded with a dot (`.`). - -Generally, the idea with chaining is that code such as `a("this").b("that").c("other")` allows `a`, `b` and `c` functions to happen in a specified order, without needing to write them as three separate lines of code. -You can think of this as being similar to chaining audio effects together using guitar pedals or digital audio effects. - -Strudel makes heavy use of chained functions. Here is a more sophisticated example: - - - -## Write your own chained function - -You can write your own chained function using `register`. Here's the above chain but registered as a reusable, chained function. - - pat - .s("sawtooth") - .cutoff(500) - //.delay(0.5) - .room(0.5) - ) -note("a3 c#4 e4 a4").effectChain()`} -/> - -Try adding `.rev()` after `effectChain()` to hear further effects added. - -# Comments - -The `//` in the example above is a line comment, resulting in the `delay` function being ignored. -It is a handy way to quickly turn code on and off. -Try uncommenting this line by deleting `//` and refreshing the pattern. -You can also use the keyboard shortcut `cmd-/` to toggle comments on and off. - -You might noticed that some comments in the REPL samples include some words starting with a "@", like `@by` or `@license`. -Those are just a convention to define some information about the music. We will talk about it in the [Music metadata](/learn/metadata) section. - -# Strings - -Ok, so what about the content inside the quotes (e.g. `"c a f e"`)? -In JavaScript, as in most programming languages, this content is referred to as being a [_string_](). -A string is simply a sequence of individual characters. -In TidalCycles, double quoted strings are used to write _patterns_ using the mini-notation, and you may hear the phrase _pattern string_ from time to time. -If you want to create a regular string and not a pattern, you can use single quotes, e.g. `'C minor'` will not be parsed as Mini Notation. - -The good news is, that this covers most of the JavaScript syntax needed for Strudel! - -
From 69c5df67f5cb01ca372afc3d42f3cd23f2985d62 Mon Sep 17 00:00:00 2001 From: scrappy_fiddler Date: Wed, 19 Nov 2025 21:40:48 +0100 Subject: [PATCH 061/194] FAQ is now prettier --- website/src/pages/learn/faq.mdx | 134 +++++++++++++++++++------------- 1 file changed, 78 insertions(+), 56 deletions(-) diff --git a/website/src/pages/learn/faq.mdx b/website/src/pages/learn/faq.mdx index dbce4c70..923bb5f6 100644 --- a/website/src/pages/learn/faq.mdx +++ b/website/src/pages/learn/faq.mdx @@ -12,16 +12,20 @@ This page contains frequently asked questions. Usually, the topic is explained i ## Is Strudel/Tidal free? -Yes - there is no charge, this is a collective open source project, and the music you make with it is your own. +Yes - there is no charge, this is a collective open source project, and the music you make with it is your own. -However there are some caveats - the source code must stay free, i.e. you cannot distribute strudel or tidal as part of projects with incompatible licenses - see the [license]() for details. The contributed examples and tracks are also separately licensed, and must not e.g. be used to train AI models without permission. +However there are some caveats - the source code must stay free, i.e. you cannot distribute strudel or tidal as part of projects with incompatible licenses - see the [license](https://www.gnu.org/licenses/agpl-3.0.en.html) for details. The contributed examples and tracks are also separately licensed, and must not e.g. be used to train AI models without permission. ## How to record or export audio? There are multiple ways to record the audio -- and video -- output of Strudel: + - capture the raw stereo signal coming out of your web browser. + - use the alternative SuperDirt audio engine. Read [this page](/learn/input-output/#oscsuperdirtstrudeldirt) to know more about it. + - capture the audio/video stream using a capture tool such as [OBS](). + - don't record anything and code it again in front of your friends. You will need an external audio editor/DAW such as Reaper/Audacity/Ardour, etc. Strudel itself does not have functionality for exporting stems / individual tracks to an audio or midi file. Strudel is not a digital audio workstation and does not operate following the same principles shared by most traditional audio softwares. @@ -29,9 +33,10 @@ You will need an external audio editor/DAW such as Reaper/Audacity/Ardour, etc. ## Can I use strudel with my IDE? Yes you can. There are experimental modes, made by community members, for several IDEs such as: - - VS Code: [Strudel VS](): an experimental mode for Microsoft VSCode. A revived version of [TidalStrudel](), which is defunct. - - nvim: [strudel.nvim](https://github.com/gruvw/strudel.nvim) - + +- VS Code: [Strudel VS](https://marketplace.visualstudio.com/items?itemName=cmillsdev.strudelvs): an experimental mode for Microsoft VSCode. A revived version of [TidalStrudel](https://marketplace.visualstudio.com/items?itemName=roipoussiere.tidal-strudel), which is defunct. +- nvim: [strudel.nvim](https://github.com/gruvw/strudel.nvim) + ## How can I record samples? You can use your own samples with Strudel. There are multiple ways to load your sample collection. Some methods are good for quick experimentation, some others are good to share your audio collection with other musicians: @@ -41,10 +46,10 @@ You can use your own samples with Strudel. There are multiple ways to load your ## Can I use Strudel with AI/LLM tools? -You are free to do what you like with Strudel, within the terms of the free/open source AGPLv3 license. However as a community we are interested in exploring human creativity. AI is *way* over-hyped right now, including by people with very shady motives. Many in the community are very wary of people training models on their tunes that they've poured their love into. So please keep discussion and questions around AI and LLMs to channels dedicated to the topic and be fully respectful of other people's work. +You are free to do what you like with Strudel, within the terms of the free/open source AGPLv3 license. However as a community we are interested in exploring human creativity. AI is _way_ over-hyped right now, including by people with very shady motives. Many in the community are very wary of people training models on their tunes that they've poured their love into. So please keep discussion and questions around AI and LLMs to channels dedicated to the topic and be fully respectful of other people's work. Furthermore, tools like ChatGPT generally give wrong answers. Please don't ask the community to fix those answers for you, as generally they will be timewasting nonsense. - + Human questions only! ## How to run offline? @@ -53,7 +58,7 @@ Strudel works offline just fine! There are multiple techniques to run it yoursel ## How to change tempo? How do I translate BPM to cpm? -If you have your tempo in beats per minute and use 4 beats per cycle (e.g. if your track is in 4/4ths) then you can do `setcpm(BPM/4)` where BPM is your beats per minute. +If you have your tempo in beats per minute and use 4 beats per cycle (e.g. if your track is in 4/4ths) then you can do `setcpm(BPM/4)` where BPM is your beats per minute. If you have a different number of beats per bar or are using more or less beats per cycle (e.g. If you want to put only half a bar or two bars into one cycle), adjust accordingly. @@ -68,21 +73,24 @@ If you pop open the sidetab of strudel.cc (small white < on the right hand side) ## How do I use this exactly like a DAW? Short answer: you don't. - + Long answer: you can use Strudel to work along your creative work in a DAW. There are many ways to do so. - + If you want to emulate the functionality of a DAW in a live coding language, you'll have to identify the operations executed by the DAW (sequencing, repeating, applying filters and envelopes) and write code that is equivalent to these operations. You might then find that the typical DAW workflow is not really adapted to live coding (because, despite both being ways of making music on the computer, they are two very different tools) and adapt your way of proceeding to the medium of code. This might mean leaving more place to serendipity and writing code that you don't predict the output of. ## Why doesn't everyone just use a DAW? -There is no easy answer to this question. Here are some thoughts: - - Live coding tools such as Strudel are excellent for improvising music and visuals using a computer. DAWs are valuable and robust companions for other activities such as producing, mastering and mixing audio, among other usages. Using a tool does not exclude from using any another tool, just build a toolbox. - - Live coding has been practiced for quite some time as a performative activity. Artists like to show their screens while playing in front of an audience. It is an essential part of what they do, of the way they share their activity with everybody. - - Code is a human language, it is made for other humans to read it. You can read the code and enjoy the music too. It has meaning, value, and there might even be something poetic/important about it! - - Strudel is free and open source, you can inspect the code, reshape it, contribute to it if you can/want. It is not opaque and this matters for many people. There is no black box, no obscure abstractions, no business model or hidden features. We need open tools in the arts! - - Live coders don't shy away from using DAWs. They use them all of the time, especially when it makes their life easier for... live coding! - - Code is an artistic material like any other. There is something valuable in the process of making music through code. More generally speaking, it is nice to tackle creative problems through the use of a programming language: creative thinking, building up your own solutions, DIY approach to music-making, unexpected outcome of algorithms, funny human errors, etc. - - There are pianos and trumpets in your DAW: why do people continue playing the piano or the trumpet? Think of live coding tools as instruments that you activate through the act of programming. +There is no easy answer to this question. Here are some thoughts: + +- Live coding tools such as Strudel are excellent for improvising music and visuals using a computer. DAWs are valuable and robust companions for other activities such as producing, mastering and mixing audio, among other usages. Using a tool does not exclude from using any another tool, just build a toolbox. + +- Live coding has been practiced for quite some time as a performative activity. Artists like to show their screens while playing in front of an audience. It is an essential part of what they do, of the way they share their activity with everybody. + +- Code is a human language, it is made for other humans to read it. You can read the code and enjoy the music too. It has meaning, value, and there might even be something poetic/important about it! - Strudel is free and open source, you can inspect the code, reshape it, contribute to it if you can/want. It is not opaque and this matters for many people. There is no black box, no obscure abstractions, no business model or hidden features. We need open tools in the arts! - Live coders don't shy away from using DAWs. They use them all of the time, especially when it makes their life easier for... live coding! + +- Code is an artistic material like any other. There is something valuable in the process of making music through code. More generally speaking, it is nice to tackle creative problems through the use of a programming language: creative thinking, building up your own solutions, DIY approach to music-making, unexpected outcome of algorithms, funny human errors, etc. + +- There are pianos and trumpets in your DAW: why do people continue playing the piano or the trumpet? Think of live coding tools as instruments that you activate through the act of programming. ## Is it more efficient to use Strudel than a DAW? @@ -96,11 +104,11 @@ Strudel can send [MIDI and OSC](/learn/input-output/), which are protocols for c Another music software (or hardware!) can then listen to these messages and process them according to its capabilities. -A simple example would be to send livecoded audio to Ableton on different tracks and then use it to mix them. +A simple example would be to send livecoded audio to Ableton on different tracks and then use it to mix them. You could also send the MIDI of a sequenced pattern to Musescore and then have it transcribe your livecoded work as a musical score. -You could also send MIDI to your hardware synths because you think they sound better than the software synths built-in Strudel. +You could also send MIDI to your hardware synths because you think they sound better than the software synths built-in Strudel. ## How do I use this in my closed source webgame? @@ -113,8 +121,12 @@ You cannot, as there is no such place. For details regarding our stance towards ## How to play different patterns simultaneously? Using the $ operator, several patterns can be played at once: - + + See also [stack](intro/#combining-patterns) @@ -122,29 +134,36 @@ See also [stack](intro/#combining-patterns) With an additional underscore, a pattern can be muted. - + See also [hush](/learn/conditional-modifiers/#hush) ## How can I arrange in Strudel using `mask`? -With mini-notation, using the `<>` and `!` operators, you can try something like +With mini-notation, using the `<>` and `!` operators, you can try something like + ``` .mask("<0!24 1!40>") ``` + It mutes a pattern for 24 cycles and plays it for 40. You would gain 64 cycles total, a multiple of 2/4/8 commonly used in western music. If each cycle is a bar, as a starting point, you could write a mask like that for any pattern: + ``` -.mask("<0!16 0!16 0!16 0!16 0!16 0!10>") +.mask("<0!16 0!16 0!16 0!16 0!16 0!10>") ``` + It mutes it throughout. For arranging, you could add the same mask to each part and replace some zeroes with ones in your different masks to make parts play. If you use `.mask()` on different patterns mess up your counting, then patterns do not align anymore. -On the other hand, doing that on purpose is one of the things that could be considered a strength of tidalcycles and Strudel. +On the other hand, doing that on purpose is one of the things that could be considered a strength of tidalcycles and Strudel. You can make things quite lively and more organic with a little (controlled) interference, according to your own taste. And you are free to arrange in cycles like 3, 6 or 9 too. @@ -159,8 +178,9 @@ This would lowpass filter sweep everything every 8 cycles. ## How can I arrange in Strudel using `arrange` or `pick`? Take [Pachelbel's Canon in D](https://en.wikipedia.org/wiki/Pachelbel%27s_Canon#Analysis) as an example which has 4 voices (one cello and 3 violins) which have repeating patterns, as seen in the link above. - + The following snipped defines the patterns as constants which can then be used for the different voices. `arrange` takes multiple arguments, which are each a number of cycles and a pattern which is played for the number of cycles, wrapped in `[]` If the pattern is shorter than the number, it is repeated. + ") .color("grey").sound("gm_tremolo_strings:3") @@ -177,34 +197,34 @@ The following snipped defines the patterns as constants which can then be used f "<[f#4 d4 e4 c#5 d5 f#5 a5 a4] [b4 g4 a4 f#4 d4 d5 [d5@3 c#5]@2]>") .color("red") - - cello$: arrange( +cello$: arrange( [2, silence], [18,cello]) violin1$: arrange( - [4,silence], - [2,violin_p1], [2,violin_p2], - [2,violin_p3], [2,violin_p4], - [2,violin_p1], [2,violin_p2], - [2,violin_p3], [2,violin_p4] - ).sound("gm_tremolo_strings:0") - violin2$: arrange( +[4,silence], +[2,violin_p1], [2,violin_p2], +[2,violin_p3], [2,violin_p4], +[2,violin_p1], [2,violin_p2], +[2,violin_p3], [2,violin_p4] +).sound("gm_tremolo_strings:0") +violin2$: arrange( [6,silence], [2,violin_p1], [2,violin_p2], [2,violin_p3], [2,violin_p4], [2,violin_p1], [2,violin_p2], [2,violin_p3] ).sound("gm_tremolo_strings:1") violin3$: arrange( - [8,silence], - [2,violin_p1], [2,violin_p2], - [2,violin_p3], [2,violin_p4], - [2,violin_p1], [2,violin_p2] - ).sound("gm_tremolo_strings:2") +[8,silence], +[2,violin_p1], [2,violin_p2], +[2,violin_p3], [2,violin_p4], +[2,violin_p1], [2,violin_p2] +).sound("gm_tremolo_strings:2") all(x => x.release(.2)) + `} /> -Alternatively, you can also put the different patterns for the violins into one single array (`const violins = [violin_p1, violin_p2, violin_p3, violin_p4]`) and use a pattern as an index to `pick` the nth element of that array. This replaces the voices defined above. Here you use `0@2` to specifiy that the first item (i.e. with index `0`) is played for `2` cycles. +Alternatively, you can also put the different patterns for the violins into one single array (`const violins = [violin_p1, violin_p2, violin_p3, violin_p4]`) and use a pattern as an index to `pick` the nth element of that array. This replaces the voices defined above. Here you use `0@2` to specifiy that the first item (i.e. with index `0`) is played for `2` cycles. `pick` has better highlighting than `arrange`: @@ -231,18 +251,18 @@ violin1$: "<~@4 0@2 1@2 2@2 3@2 0@2 1@2 2@2 3@2>".pick(violins) .sound("gm_tremolo_strings:0") violin2$: "<~@6 0@2 1@2 2@2 3@2 0@2 1@2 2@2>".pick(violins) .sound("gm_tremolo_strings:1") -violin3$: "<~@8 0@2 1@2 2@2 3@2 0@2 1@2 >".pick(violins) +violin3$: "<~@8 0@2 1@2 2@2 3@2 0@2 1@2 >".pick(violins) .sound("gm_tremolo_strings:2") all(x => x.release(.2)) -`} /> - +`} /> + The `pick` method also works with jsons which have named elements, which makes it easier to read, see the [here](/learn/conditional-modifiers/#pick). `pickRestart` restarts the pattern upon picking it which can make a difference if the duration of the pick indexes doesn't line up with the patterns which are picked - which is not the case here. Try adding `.punchcard()` after the `release(.2)` for a visualization. ## I saw Switch Angel using functions which I cannot find in the reference (e.g. `trancegate`). How do I make it work? -Methods like `trancegate()`, `rlpf()` and `acidenv()` are currently not pattern methods which come natively with strudel. +Methods like `trancegate()`, `rlpf()` and `acidenv()` are currently not pattern methods which come natively with strudel. It's part of a script for strudel which was written by Switch Angel and published [here](https://github.com/switchangel/strudel-scripts/blob/main/allscripts.js) @@ -255,20 +275,22 @@ If you paste these functions into your strudel session, hit "Update" and delete ## Is there difference between `n` and `note`? They are not aliases of each other, in contrast to `s` and `sound`. - -The method `note` is used to reference a certain note (either as its name, such as `c` or `b2` or the midi number `69`, for example `note("c3 e3 g3")`). - -On the other hand, `n` is a way to reference the nth index of something. This something can be a scale (eg `n("0 2 4").scale("C:major")`) , but it can also be a particular note in a chord (see https://strudel.cc/recipes/recipes/#arpeggios for an example) . -The method `n` can also be used for something completely unrelated to notes like the nth sample from a sample map `s("hh*8").bank("RolandTR909").n("0 1 2 3")`. Note that `pick` does *not* use `n`. +The method `note` is used to reference a certain note (either as its name, such as `c` or `b2` or the midi number `69`, for example `note("c3 e3 g3")`). -".pickRestart([ +On the other hand, `n` is a way to reference the nth index of something. This something can be a scale (eg `n("0 2 4").scale("C:major")`) , but it can also be a particular note in a chord (see https://strudel.cc/recipes/recipes/#arpeggios for an example) . + +The method `n` can also be used for something completely unrelated to notes like the nth sample from a sample map `s("hh*8").bank("RolandTR909").n("0 1 2 3")`. Note that `pick` does _not_ use `n`. + +".pickRestart([ n("0 1 2 0"), n("2 3 4 ~"), n("[4 5] [4 3] 2 0"), n("0 -3 0 ~") - ]).scale("C:major").s("piano")`} /> - + ]).scale("C:major").s("piano")`} +/>{' '} ## Is there a cheat sheet for all symbols? @@ -305,6 +327,6 @@ _ before a stack name: mutes the stack, i.e. hush(), for example _$: s("bd") ## Are there more FAQ items? -These pages have been taken from [this pad](https://doc.patternclub.org/_CgofWouTciXXHexUP9AVg?both). Some of the items there have not been brushed up and brought here. +These pages have been taken from [this pad](https://doc.patternclub.org/_CgofWouTciXXHexUP9AVg?both). Some of the items there have not been brushed up and brought here. These include the following items: 9, 11, 12 and 19 From 29b6729246fa45eab19e8ee0964cb4a50f002eae Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 19 Nov 2025 15:03:11 -0600 Subject: [PATCH 062/194] First pass at improving controls --- packages/core/controls.mjs | 108 +++++++++++++++++++------------------ packages/core/pattern.mjs | 61 +++++++++++++++++++++ 2 files changed, 116 insertions(+), 53 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 0555bd10..307c15f4 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1740,29 +1740,6 @@ export const { panorient } = registerControl('panorient'); // ['pitch2'], // ['pitch3'], // ['portamento'], -/** - * Selects which LFO number to use for modulation. Multiple LFOs - * can be applied using the ':' mininotation. There are an arbitrary number - * of LFOs available -- the number is only used to share LFOs across targets - * if desired (and to conserve processing power) - * - * @name lfoNum - * @param {number | Pattern} lfoNum Index of the LFO. - * setup: note("F2").sound("supersaw") - * .lpf(100) - * .lfoDepth(1000) - * .lfoRate(0.25) - * .lfoSynced(1) - * .lfoTarget("lpf") - * .lfoParam("frequency") - * .lfoNum(2) - * - * reuse: note("F3").sound("square").lpf(50) - * .lfoTarget("lpf") - * .lfoParam("frequency") - * .lfoNum(2) // uses the same LFO - */ -export const { lfoNum } = registerControl('lfoNum'); /** * Sets the target destination for the LFO modulation. Names are typically related @@ -1770,7 +1747,8 @@ export const { lfoNum } = registerControl('lfoNum'); * and if it fails, the console will print the available options. * * @name lfoTarget - * @param {string | Pattern} lfoTarget Target identifier for modulation. + * @synonyms lfot, lfotarget + * @param {string | Pattern} target Target identifier for modulation. * note("F2").sound("supersaw") * .lpf(100) * .lfoDepth(3000) @@ -1779,7 +1757,7 @@ export const { lfoNum } = registerControl('lfoNum'); * .lfoTarget("lpf") * .lfoParam("frequency") */ -export const { lfoTarget } = registerControl('lfoTarget'); +export const { lfoTarget, lfot, lfotarget } = registerControl('lfoTarget', 'lfot', 'lfotarget'); /** * Chooses which parameter the LFO will modulate on the target. Parameter values @@ -1787,7 +1765,8 @@ export const { lfoTarget } = registerControl('lfoTarget'); * and if it fails, the console will print the available options. * * @name lfoParam - * @param {string | Pattern} lfoParam Parameter name + * @synonyms lfop, lfoparam + * @param {string | Pattern} param Parameter name * note("F2").sound("supersaw") * .lpf(100) * .lfoDepth(3000) @@ -1796,13 +1775,14 @@ export const { lfoTarget } = registerControl('lfoTarget'); * .lfoTarget("lpf") * .lfoParam("frequency") */ -export const { lfoParam } = registerControl('lfoParam'); +export const { lfoParam, lfop, lfoparam } = registerControl('lfoParam', 'lfop', 'lfoparam'); /** * Controls the speed of the LFO. * * @name lfoRate - * @param {number | Pattern} lfoRate Frequency or tempo-relative value. + * @synonyms lfor, lforate + * @param {number | Pattern} rate Frequency or tempo-relative value. * note("F2").sound("supersaw") * .lpf(100) * .lfoTarget("lpf") @@ -1810,13 +1790,14 @@ export const { lfoParam } = registerControl('lfoParam'); * .lfoDepth(3000) * .lfoRate(0.25) */ -export const { lfoRate } = registerControl('lfoRate'); +export const { lfoRate, lfor, lforate } = registerControl('lfoRate', 'lfor', 'lforate'); /** * Sets the modulation depth of the LFO. * * @name lfoDepth - * @param {number | Pattern} lfoDepth Modulation depth amount. + * @synonyms lfod, lfodepth + * @param {number | Pattern} depth Modulation depth amount. * note("F2").sound("supersaw") * .lpf(100) * .lfoTarget("lpf") @@ -1824,7 +1805,7 @@ export const { lfoRate } = registerControl('lfoRate'); * .lfoSynced(1) * .lfoDepth(5000) */ -export const { lfoDepth } = registerControl('lfoDepth'); +export const { lfoDepth, lfod, lfodepth } = registerControl('lfoDepth', 'lfod', 'lfodepth'); /** * Applies a DC offset to the LFO signal. Normally the LFO varies from @@ -1832,7 +1813,8 @@ export const { lfoDepth } = registerControl('lfoDepth'); * a bipolar LFO. * * @name lfoDCOffset - * @param {number | Pattern} lfoDCOffset Offset amount. + * @synonyms lfodc, lfodcoffset + * @param {number | Pattern} offset Offset amount. * note("F2").sound("supersaw") * .lpf(2000) * .lfoTarget("lpf") @@ -1842,7 +1824,7 @@ export const { lfoDepth } = registerControl('lfoDepth'); * .lfoSynced(1) * .lfoDCOffset(-0.5) */ -export const { lfoDCOffset } = registerControl('lfoDCOffset'); +export const { lfoDCOffset, lfodc, lfodcoffset } = registerControl('lfoDCOffset', 'lfodc', 'lfodcoffset'); /** * Selects the waveform shape of the LFO. Current options are @@ -1850,7 +1832,8 @@ export const { lfoDCOffset } = registerControl('lfoDCOffset'); * respectively). * * @name lfoShape - * @param {number | Pattern} lfoShape Waveform type identifier. + * @synonyms lfosh, lfoshape + * @param {number | Pattern} shape Waveform type identifier. * note("F2").sound("supersaw") * .lpf(100) * .lfoTarget("lpf") @@ -1860,13 +1843,14 @@ export const { lfoDCOffset } = registerControl('lfoDCOffset'); * .lfoSynced(1) * .lfoShape(3) */ -export const { lfoShape } = registerControl('lfoShape'); +export const { lfoShape, lfosh, lfoshape } = registerControl('lfoShape', 'lfosh', 'lfoshape'); /** * Skews the LFO waveform. * * @name lfoSkew - * @param {number | Pattern} lfoSkew Skew amount (between 0 and 1). + * @synonyms lfosk, lfoskew + * @param {number | Pattern} skew Skew amount (between 0 and 1). * note("F2").sound("supersaw") * .lpf(100) * .lfoTarget("lpf") @@ -1876,13 +1860,14 @@ export const { lfoShape } = registerControl('lfoShape'); * .lfoSynced(1) * .lfoSkew(0.75) */ -export const { lfoSkew } = registerControl('lfoSkew'); +export const { lfoSkew, lfosk, lfoskew } = registerControl('lfoSkew', 'lfosk', 'lfoskew'); /** * Adjusts the (exponential) curvature of the LFO waveform. * * @name lfoCurve - * @param {number | Pattern} lfoCurve Curve shaping amount. + * @synonyms lfoc, lfocurve + * @param {number | Pattern} curve Curve shaping amount. * note("F2").sound("supersaw") * .lpf(100) * .lfoTarget("lpf") @@ -1892,22 +1877,22 @@ export const { lfoSkew } = registerControl('lfoSkew'); * .lfoSynced(1) * .lfoCurve(0.95) */ -export const { lfoCurve } = registerControl('lfoCurve'); +export const { lfoCurve, lfoc, lfocurve } = registerControl('lfoCurve', 'lfoc', 'lfocurve'); /** - * Determines whether the LFO is tempo-synced. + * Sets the tempo-synced rate of the LFO * - * @name lfoSynced - * @param {number | Pattern} lfoSynced Boolean flag (0 or 1). + * @name lfoSync + * @synonyms lfos, lfosync + * @param {number | Pattern} rate Rate to be multiplied by cycles per second * note("F2").sound("supersaw") * .lpf(100) * .lfoTarget("lpf") * .lfoParam("frequency") * .lfoDepth(1000) - * .lfoShape(3).lfoRate(2) - * .lfoSynced(1) + * .lfoShape(3).lfoSync(2) */ -export const { lfoSynced } = registerControl('lfoSynced'); +export const { lfoSync, lfos, lfosync } = registerControl('lfoSync', 'lfos', 'lfosync'); /** * Sets the target destination for the envelope modulation. Names are typically related @@ -1915,6 +1900,7 @@ export const { lfoSynced } = registerControl('lfoSynced'); * and if it fails, the console will print the available options. * * @name envTarget + * @synonyms envt, envtarget * @param {number | Pattern} envTarget Target identifier for modulation. * n(irand(12).seg(8)).scale("F#3:minor").room(1) * .lpf(100) @@ -1922,7 +1908,7 @@ export const { lfoSynced } = registerControl('lfoSynced'); * .envTarget("source:lpf") * .envParam("detune:frequency") */ -export const { envTarget } = registerControl('envTarget'); +export const { envTarget, envt, envtarget } = registerControl('envTarget', 'envt', 'envtarget'); /** * Chooses which parameter the LFO will modulate on the target. Parameter values @@ -1930,6 +1916,7 @@ export const { envTarget } = registerControl('envTarget'); * and if it fails, the console will print the available options. * * @name envParam + * @synonyms envp, envparam * @param {number | Pattern} envParam Parameter index or identifier. * n(irand(12).seg(8)).scale("F#3:minor").room(1) * .lpf(100) @@ -1937,12 +1924,13 @@ export const { envTarget } = registerControl('envTarget'); * .envTarget("source:lpf") * .envParam("detune:frequency") */ -export const { envParam } = registerControl('envParam'); +export const { envParam, envp, envparam } = registerControl('envParam', 'envp', 'envparam'); /** * Controls the attack time of the envelope. * * @name envAttack + * @synonyms envatt, envattack * @param {number | Pattern} envAttack Duration of attack phase. * n(irand(12).seg(8)).scale("F#3:minor").room(1) * .lpf(500) @@ -1951,12 +1939,13 @@ export const { envParam } = registerControl('envParam'); * .envParam("detune:frequency") * .envAttack(0.5) */ -export const { envAttack } = registerControl('envAttack'); +export const { envAttack, envatt, envattack } = registerControl('envAttack', 'envatt', 'envattack'); /** * Controls the decay time of the envelope. * * @name envDecay + * @synonyms envdec, envdecay * @param {number | Pattern} envDecay Duration of decay phase. * n(irand(12).seg(8)).scale("F#3:minor").room(1) * .lpf(100) @@ -1965,12 +1954,13 @@ export const { envAttack } = registerControl('envAttack'); * .envParam("detune:frequency") * .envDecay("0.03:0.15").envCurve("exp:exp") */ -export const { envDecay } = registerControl('envDecay'); +export const { envDecay, envdec, envdecay } = registerControl('envDecay', 'envdec', 'envdecay'); /** * Sets the sustain level of the envelope. * * @name envSustain + * @synonyms envs, envsustain * @param {number | Pattern} envSustain Sustain amplitude level. * n(irand(12).seg(8)).scale("F#3:minor").room(1) * .lpf(100) @@ -1980,12 +1970,13 @@ export const { envDecay } = registerControl('envDecay'); * .envDecay("0.03:0.15").envCurve("exp:exp") * .envSustain(0.2) */ -export const { envSustain } = registerControl('envSustain'); +export const { envSustain, envs, envsustain } = registerControl('envSustain', 'envs', 'envsustain'); /** * Controls the release time of the envelope. * * @name envRelease + * @synonyms envr, envrelease * @param {number | Pattern} envRelease Duration of release phase. * @example * n(irand(12).seg(8)).scale("F#3:minor").room(1) @@ -1997,12 +1988,13 @@ export const { envSustain } = registerControl('envSustain'); * .envSustain(0.5) * .envRelease(3) */ -export const { envRelease } = registerControl('envRelease'); +export const { envRelease, envr, envrelease } = registerControl('envRelease', 'envr', 'envrelease'); /** * Selects the style of envelope: `exp` or `lin` (exponential or linear). * * @name envCurve + * @synonyms envc, envcurve * @param {string | Pattern} envCurve Envelope curve style. * @example * n(irand(12).seg(8)).scale("F#3:minor").room(1) @@ -2013,12 +2005,13 @@ export const { envRelease } = registerControl('envRelease'); * .envDecay("0.3:0.15") * .envCurve("lin:exp") */ -export const { envCurve } = registerControl('envCurve'); +export const { envCurve, envc, envcurve } = registerControl('envCurve', 'envc', 'envcurve'); /** * Sets the modulation depth of the envelope. * * @name envDepth + * @synonyms envd, envdepth * @param {number | Pattern} envDepth Modulation depth amount. * @example * n(irand(12).seg(8)).scale("F#3:minor").room(1) @@ -2027,7 +2020,7 @@ export const { envCurve } = registerControl('envCurve'); * .envParam("detune:frequency") * .envDepth("4800:400") */ -export const { envDepth } = registerControl('envDepth'); +export const { envDepth, envd, envdepth } = registerControl('envDepth', 'envd', 'envdepth'); // TODO: slide param for certain synths export const { slide } = registerControl('slide'); @@ -2400,6 +2393,15 @@ export const { clip, legato } = registerControl('clip', 'legato'); */ export const { duration, dur } = registerControl('duration', 'dur'); +/** + * Sets the ID of the pattern for later reference + * + * @name id + * @param {number | Pattern} id ID of the pattern + * + */ +export const { id } = registerControl('id'); + // ZZFX export const { zrand } = registerControl('zrand'); export const { curve } = registerControl('curve'); diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index f5b0c676..38f9aeb3 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3624,3 +3624,64 @@ for (const name of distAlgoNames) { return this.distort(argsPat); }; } + +/** + * Selects which LFO number to use for modulation. Multiple LFOs + * can be applied using the ':' mininotation. There are an arbitrary number + * of LFOs available -- the number is only used to share LFOs across targets + * if desired (and to conserve processing power) + * + * @name lfoNum + * @param {number | Pattern} lfoNum Index of the LFO. + * setup: note("F2").sound("supersaw") + * .lpf(100) + * .lfoDepth(1000) + * .lfoRate(0.25) + * .lfoSynced(1) + * .lfoTarget("lpf") + * .lfoParam("frequency") + * .lfoNum(2) + * + * reuse: note("F3").sound("square").lpf(50) + * .lfoTarget("lpf") + * .lfoParam("frequency") + * .lfoNum(2) // uses the same LFO + */ + +/** + * Sets the target destination for the envelope modulation. Names are typically related + * to existing controls ("source", "lpf", "vibrato", etc.). You can try a value + * and if it fails, the console will print the available options. + * + * @name envTarget + * @param {number | Pattern} envTarget Target identifier for modulation. + * n(irand(12).seg(8)).scale("F#3:minor").room(1) + * .lpf(100) + * .envDepth("4800:400") + * .envTarget("source:lpf") + * .envParam("detune:frequency") + */ + + +/** + * Establishes a signal chain. Can be called in sequence like pat.chain(...).chain(...) and so forth + * and/or in a single .chain(..., ..., etc) call. The arguments to `chain` are _patterns_ which each act like + * a self-contained pattern and follow the normal [signal chain](https://strudel.cc/learn/effects/). + * + * If multiple sound generators are present within the chain, they will be mixed in at the location where + * they are declared. + * + * @name chain + * @memberof Pattern + * @param {Pattern | Pattern[]} patterns Patterns to combine into a single chain + * @returns Pattern + */ +Pattern.prototype.chain = function (...pats) { + pats = pats.map(reify); + return this.withValue((v) => (vEff) => { + const currChain = v.chain ?? []; + return { ...v, chain: currChain.concat(vEff) }; + }).appLeft(parray(pats)); +}; + +export const chain = (pats) => pure({}).chain(pats); \ No newline at end of file From 4504d1afd0f893f696600493c23dff122c06c6ca Mon Sep 17 00:00:00 2001 From: scrappy_fiddler Date: Wed, 19 Nov 2025 22:14:23 +0100 Subject: [PATCH 063/194] remove trailing spaces in FAQ --- website/src/pages/learn/faq.mdx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/website/src/pages/learn/faq.mdx b/website/src/pages/learn/faq.mdx index 923bb5f6..2e4cb5c4 100644 --- a/website/src/pages/learn/faq.mdx +++ b/website/src/pages/learn/faq.mdx @@ -42,6 +42,7 @@ Yes you can. There are experimental modes, made by community members, for severa You can use your own samples with Strudel. There are multiple ways to load your sample collection. Some methods are good for quick experimentation, some others are good to share your audio collection with other musicians: - Import folders [from the interface](/learn/samples/#from-disk-via-import-sounds-folder) + - - Host your sound library e.g. on github and [load them from an URL](/learn/samples/#loading-custom-samples) ## Can I use Strudel with AI/LLM tools? @@ -80,15 +81,15 @@ If you want to emulate the functionality of a DAW in a live coding language, you ## Why doesn't everyone just use a DAW? -There is no easy answer to this question. Here are some thoughts: +There is no easy answer to this question. Here are some thoughts: -- Live coding tools such as Strudel are excellent for improvising music and visuals using a computer. DAWs are valuable and robust companions for other activities such as producing, mastering and mixing audio, among other usages. Using a tool does not exclude from using any another tool, just build a toolbox. +- Live coding tools such as Strudel are excellent for improvising music and visuals using a computer. DAWs are valuable and robust companions for other activities such as producing, mastering and mixing audio, among other usages. Using a tool does not exclude from using any another tool, just build a toolbox. -- Live coding has been practiced for quite some time as a performative activity. Artists like to show their screens while playing in front of an audience. It is an essential part of what they do, of the way they share their activity with everybody. +- Live coding has been practiced for quite some time as a performative activity. Artists like to show their screens while playing in front of an audience. It is an essential part of what they do, of the way they share their activity with everybody. -- Code is a human language, it is made for other humans to read it. You can read the code and enjoy the music too. It has meaning, value, and there might even be something poetic/important about it! - Strudel is free and open source, you can inspect the code, reshape it, contribute to it if you can/want. It is not opaque and this matters for many people. There is no black box, no obscure abstractions, no business model or hidden features. We need open tools in the arts! - Live coders don't shy away from using DAWs. They use them all of the time, especially when it makes their life easier for... live coding! +- Code is a human language, it is made for other humans to read it. You can read the code and enjoy the music too. It has meaning, value, and there might even be something poetic/important about it! - Strudel is free and open source, you can inspect the code, reshape it, contribute to it if you can/want. It is not opaque and this matters for many people. There is no black box, no obscure abstractions, no business model or hidden features. We need open tools in the arts! - Live coders don't shy away from using DAWs. They use them all of the time, especially when it makes their life easier for... live coding! -- Code is an artistic material like any other. There is something valuable in the process of making music through code. More generally speaking, it is nice to tackle creative problems through the use of a programming language: creative thinking, building up your own solutions, DIY approach to music-making, unexpected outcome of algorithms, funny human errors, etc. +- Code is an artistic material like any other. There is something valuable in the process of making music through code. More generally speaking, it is nice to tackle creative problems through the use of a programming language: creative thinking, building up your own solutions, DIY approach to music-making, unexpected outcome of algorithms, funny human errors, etc. - There are pianos and trumpets in your DAW: why do people continue playing the piano or the trumpet? Think of live coding tools as instruments that you activate through the act of programming. From b73f62405820993ad0be28291066023279580fad Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 19 Nov 2025 15:41:29 -0600 Subject: [PATCH 064/194] Trying out alias approach --- packages/core/controls.mjs | 54 ++++++++++++++--------- packages/core/pattern.mjs | 90 ++++++++++++++++++++------------------ 2 files changed, 82 insertions(+), 62 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 6be21414..732679d6 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2097,7 +2097,8 @@ export const { envAttack, envatt, envattack } = registerControl('envAttack', 'en * .envDepth("4800:400") * .envTarget("source:lpf") * .envParam("detune:frequency") - * .envDecay("0.03:0.15").envCurve("exp:exp") + * .envDecay("0.03:0.15") + * .envDCurve(-0.4) */ export const { envDecay, envdec, envdecay } = registerControl('envDecay', 'envdec', 'envdecay'); @@ -2112,7 +2113,7 @@ export const { envDecay, envdec, envdecay } = registerControl('envDecay', 'envde * .envDepth("4800:400") * .envTarget("source:lpf") * .envParam("detune:frequency") - * .envDecay("0.03:0.15").envCurve("exp:exp") + * .envACurve(0.6).envDCurve(-0.2) * .envSustain(0.2) */ export const { envSustain, envs, envsustain } = registerControl('envSustain', 'envs', 'envsustain'); @@ -2129,29 +2130,12 @@ export const { envSustain, envs, envsustain } = registerControl('envSustain', 'e * .envDepth("4800:400") * .envTarget("source:lpf") * .envParam("detune:frequency") - * .envDecay("0.03:0.15").envCurve("exp:exp") + * .envDecay("0.03:0.15").envRCurve(0.5) * .envSustain(0.5) * .envRelease(3) */ export const { envRelease, envr, envrelease } = registerControl('envRelease', 'envr', 'envrelease'); -/** - * Selects the style of envelope: `exp` or `lin` (exponential or linear). - * - * @name envCurve - * @synonyms envc, envcurve - * @param {string | Pattern} envCurve Envelope curve style. - * @example - * n(irand(12).seg(8)).scale("F#3:minor").room(1) - * .lpf(100).release(2) - * .envTarget("source:lpf") - * .envParam("detune:frequency") - * .envDepth("500:4000") - * .envDecay("0.3:0.15") - * .envCurve("lin:exp") - */ -export const { envCurve, envc, envcurve } = registerControl('envCurve', 'envc', 'envcurve'); - /** * Sets the modulation depth of the envelope. * @@ -2167,6 +2151,36 @@ export const { envCurve, envc, envcurve } = registerControl('envCurve', 'envc', */ export const { envDepth, envd, envdepth } = registerControl('envDepth', 'envd', 'envdepth'); +/** + * Adjusts the curvature of the attack portion of the envelope. Positive values are snappy, + * negative values are slow + * + * @name envACurve + * @synonyms envac, envacurve + * @param {number | Pattern} envACurve Curvature amount (between -1 and 1). + */ +export const { envACurve, envac, envacurve } = registerControl('envACurve', 'envac', 'envacurve'); + +/** + * Adjusts the curvature of the decay portion of the envelope. Positive values are snappy, + * negative values are slow + * + * @name envDCurve + * @synonyms envdc, envdcurve + * @param {number | Pattern} envDCurve Curvature amount (between -1 and 1). + */ +export const { envDCurve, envdc, envdcurve } = registerControl('envDCurve', 'envdc', 'envdcurve'); + +/** + * Adjusts the curvature of the release portion of the envelope. Positive values are snappy, + * negative values are slow + * + * @name envRCurve + * @synonyms envrc, envrcurve + * @param {number | Pattern} envRCurve Curvature amount (between -1 and 1). + */ +export const { envRCurve, envrc, envrcurve } = registerControl('envRCurve', 'envrc', 'envrcurve'); + // TODO: slide param for certain synths export const { slide } = registerControl('slide'); // TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index ea9d0f34..4ba24b20 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3688,42 +3688,43 @@ export const phases = (list) => { return _ensureListPattern(list).as('phases'); }; -/** - * Selects which LFO number to use for modulation. Multiple LFOs - * can be applied using the ':' mininotation. There are an arbitrary number - * of LFOs available -- the number is only used to share LFOs across targets - * if desired (and to conserve processing power) - * - * @name lfoNum - * @param {number | Pattern} lfoNum Index of the LFO. - * setup: note("F2").sound("supersaw") - * .lpf(100) - * .lfoDepth(1000) - * .lfoRate(0.25) - * .lfoSynced(1) - * .lfoTarget("lpf") - * .lfoParam("frequency") - * .lfoNum(2) - * - * reuse: note("F3").sound("square").lpf(50) - * .lfoTarget("lpf") - * .lfoParam("frequency") - * .lfoNum(2) // uses the same LFO - */ +const configAliases = new Map(); +const addConfigAlias = (funcName, canonical, ...aliases) => { + const lowerFunc = String(funcName).toLowerCase(); + const aliasMap = configAliases.get(lowerFunc) ?? new Map(); + const allKeys = new Set([canonical, ...aliases]); + for (const alias of allKeys) { + aliasMap.set(String(alias).toLowerCase(), canonical); + } + configAliases.set(lowerFunc, aliasMap); +}; -/** - * Sets the target destination for the envelope modulation. Names are typically related - * to existing controls ("source", "lpf", "vibrato", etc.). You can try a value - * and if it fails, the console will print the available options. - * - * @name envTarget - * @param {number | Pattern} envTarget Target identifier for modulation. - * n(irand(12).seg(8)).scale("F#3:minor").room(1) - * .lpf(100) - * .envDepth("4800:400") - * .envTarget("source:lpf") - * .envParam("detune:frequency") - */ +const resolveConfigKey = (funcName, key) => { + const aliasMap = configAliases.get(String(funcName).toLowerCase()); + if (!aliasMap) return key; + const normalized = String(key).toLowerCase(); + return aliasMap.get(normalized) ?? key; +}; + +addConfigAlias('lfo', 'lfoTarget', 'lfot', 'lfotarget', 'target', 't'); +addConfigAlias('lfo', 'lfoParam', 'lfop', 'lfoparam', 'param', 'parameter', 'p'); +addConfigAlias('lfo', 'lfoRate', 'lfor', 'lforate', 'rate', 'r'); +addConfigAlias('lfo', 'lfoDepth', 'lfod', 'lfodepth', 'depth', 'd'); +addConfigAlias('lfo', 'lfoDCOffset', 'lfodc', 'lfodcoffset', 'dcoffset', 'offset', 'dc'); +addConfigAlias('lfo', 'lfoShape', 'lfosh', 'lfoshape', 'shape', 'sh'); +addConfigAlias('lfo', 'lfoSkew', 'lfosk', 'lfoskew', 'skew', 'sk'); +addConfigAlias('lfo', 'lfoCurve', 'lfoc', 'lfocurve', 'curve', 'c'); +addConfigAlias('lfo', 'lfoSync', 'lfos', 'lfosync', 'sync', 'synced', 's'); +addConfigAlias('env', 'envTarget', 'envt', 'envtarget'); +addConfigAlias('env', 'envParam', 'envp', 'envparam'); +addConfigAlias('env', 'envAttack', 'envatt', 'envattack'); +addConfigAlias('env', 'envDecay', 'envdec', 'envdecay'); +addConfigAlias('env', 'envSustain', 'envs', 'envsustain'); +addConfigAlias('env', 'envRelease', 'envr', 'envrelease'); +addConfigAlias('env', 'envDepth', 'envd', 'envdepth'); +addConfigAlias('env', 'envACurve', 'envac', 'envacurve'); +addConfigAlias('env', 'envDCurve', 'envdc', 'envdcurve'); +addConfigAlias('env', 'envRCurve', 'envrc', 'envrcurve'); /** * Establishes a signal chain. Can be called in sequence like pat.chain(...).chain(...) and so forth @@ -3738,12 +3739,17 @@ export const phases = (list) => { * @param {Pattern | Pattern[]} patterns Patterns to combine into a single chain * @returns Pattern */ -Pattern.prototype.chain = function (...pats) { - pats = pats.map(reify); - return this.withValue((v) => (vEff) => { - const currChain = v.chain ?? []; - return { ...v, chain: currChain.concat(vEff) }; - }).appLeft(parray(pats)); +Pattern.prototype.lfo = function (config) { + if (config == null || typeof config !== 'object') { + return this; + } + let output = this; + for (const [rawKey, value] of Object.entries(config)) { + const key = resolveConfigKey('lfo', rawKey); + const pat = reify(value); + output = output.set(pat.as(key)); + } + return output; }; -export const chain = (pats) => pure({}).chain(pats); +export const lfo = (config) => pure({}).lfo(config); From 7ccdefe1c69605563ff16768ea06fcb7eaffc8f8 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 19 Nov 2025 16:33:08 -0600 Subject: [PATCH 065/194] Huge overhaul for config part 1 --- packages/core/controls.mjs | 295 ------------------------------------- packages/core/pattern.mjs | 140 +++++++++++++----- 2 files changed, 105 insertions(+), 330 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 732679d6..bcab1d1b 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1886,301 +1886,6 @@ export const { panorient } = registerControl('panorient'); // ['pitch3'], // ['portamento'], -/** - * Sets the target destination for the LFO modulation. Names are typically related - * to existing controls ("source", "lpf", "vibrato", etc.). You can try a value - * and if it fails, the console will print the available options. - * - * @name lfoTarget - * @synonyms lfot, lfotarget - * @param {string | Pattern} target Target identifier for modulation. - * note("F2").sound("supersaw") - * .lpf(100) - * .lfoDepth(3000) - * .lfoRate(0.25) - * .lfoSynced(1) - * .lfoTarget("lpf") - * .lfoParam("frequency") - */ -export const { lfoTarget, lfot, lfotarget } = registerControl('lfoTarget', 'lfot', 'lfotarget'); - -/** - * Chooses which parameter the LFO will modulate on the target. Parameter values - * are things like "frequency", "Q", "detune", etc. You can try a value - * and if it fails, the console will print the available options. - * - * @name lfoParam - * @synonyms lfop, lfoparam - * @param {string | Pattern} param Parameter name - * note("F2").sound("supersaw") - * .lpf(100) - * .lfoDepth(3000) - * .lfoRate(0.25) - * .lfoSynced(1) - * .lfoTarget("lpf") - * .lfoParam("frequency") - */ -export const { lfoParam, lfop, lfoparam } = registerControl('lfoParam', 'lfop', 'lfoparam'); - -/** - * Controls the speed of the LFO. - * - * @name lfoRate - * @synonyms lfor, lforate - * @param {number | Pattern} rate Frequency or tempo-relative value. - * note("F2").sound("supersaw") - * .lpf(100) - * .lfoTarget("lpf") - * .lfoParam("frequency") - * .lfoDepth(3000) - * .lfoRate(0.25) - */ -export const { lfoRate, lfor, lforate } = registerControl('lfoRate', 'lfor', 'lforate'); - -/** - * Sets the modulation depth of the LFO. - * - * @name lfoDepth - * @synonyms lfod, lfodepth - * @param {number | Pattern} depth Modulation depth amount. - * note("F2").sound("supersaw") - * .lpf(100) - * .lfoTarget("lpf") - * .lfoParam("frequency") - * .lfoSynced(1) - * .lfoDepth(5000) - */ -export const { lfoDepth, lfod, lfodepth } = registerControl('lfoDepth', 'lfod', 'lfodepth'); - -/** - * Applies a DC offset to the LFO signal. Normally the LFO varies from - * 0 to 1 (i.e. unipolar). By using an offset of -0.5, one can achieve - * a bipolar LFO. - * - * @name lfoDCOffset - * @synonyms lfodc, lfodcoffset - * @param {number | Pattern} offset Offset amount. - * note("F2").sound("supersaw") - * .lpf(2000) - * .lfoTarget("lpf") - * .lfoParam("frequency") - * .lfoDepth(1000) - * .lfoRate(4) - * .lfoSynced(1) - * .lfoDCOffset(-0.5) - */ -export const { lfoDCOffset, lfodc, lfodcoffset } = registerControl('lfoDCOffset', 'lfodc', 'lfodcoffset'); - -/** - * Selects the waveform shape of the LFO. Current options are - * triangle, square, sine, saw, ramp (corresponding to 0 through 4, - * respectively). - * - * @name lfoShape - * @synonyms lfosh, lfoshape - * @param {number | Pattern} shape Waveform type identifier. - * note("F2").sound("supersaw") - * .lpf(100) - * .lfoTarget("lpf") - * .lfoParam("frequency") - * .lfoDepth(1000) - * .lfoRate(4) - * .lfoSynced(1) - * .lfoShape(3) - */ -export const { lfoShape, lfosh, lfoshape } = registerControl('lfoShape', 'lfosh', 'lfoshape'); - -/** - * Skews the LFO waveform. - * - * @name lfoSkew - * @synonyms lfosk, lfoskew - * @param {number | Pattern} skew Skew amount (between 0 and 1). - * note("F2").sound("supersaw") - * .lpf(100) - * .lfoTarget("lpf") - * .lfoParam("frequency") - * .lfoDepth(1000) - * .lfoRate(4) - * .lfoSynced(1) - * .lfoSkew(0.75) - */ -export const { lfoSkew, lfosk, lfoskew } = registerControl('lfoSkew', 'lfosk', 'lfoskew'); - -/** - * Adjusts the (exponential) curvature of the LFO waveform. - * - * @name lfoCurve - * @synonyms lfoc, lfocurve - * @param {number | Pattern} curve Curve shaping amount. - * note("F2").sound("supersaw") - * .lpf(100) - * .lfoTarget("lpf") - * .lfoParam("frequency") - * .lfoDepth(1000) - * .lfoShape(3).lfoRate(4) - * .lfoSynced(1) - * .lfoCurve(0.95) - */ -export const { lfoCurve, lfoc, lfocurve } = registerControl('lfoCurve', 'lfoc', 'lfocurve'); - -/** - * Sets the tempo-synced rate of the LFO - * - * @name lfoSync - * @synonyms lfos, lfosync - * @param {number | Pattern} rate Rate to be multiplied by cycles per second - * note("F2").sound("supersaw") - * .lpf(100) - * .lfoTarget("lpf") - * .lfoParam("frequency") - * .lfoDepth(1000) - * .lfoShape(3).lfoSync(2) - */ -export const { lfoSync, lfos, lfosync } = registerControl('lfoSync', 'lfos', 'lfosync'); - -/** - * Sets the target destination for the envelope modulation. Names are typically related - * to existing controls ("source", "lpf", "vibrato", etc.). You can try a value - * and if it fails, the console will print the available options. - * - * @name envTarget - * @synonyms envt, envtarget - * @param {number | Pattern} envTarget Target identifier for modulation. - * n(irand(12).seg(8)).scale("F#3:minor").room(1) - * .lpf(100) - * .envDepth("4800:400") - * .envTarget("source:lpf") - * .envParam("detune:frequency") - */ -export const { envTarget, envt, envtarget } = registerControl('envTarget', 'envt', 'envtarget'); - -/** - * Chooses which parameter the LFO will modulate on the target. Parameter values - * are things like "frequency", "Q", "detune", etc. You can try a value - * and if it fails, the console will print the available options. - * - * @name envParam - * @synonyms envp, envparam - * @param {number | Pattern} envParam Parameter index or identifier. - * n(irand(12).seg(8)).scale("F#3:minor").room(1) - * .lpf(100) - * .envDepth("4800:400") - * .envTarget("source:lpf") - * .envParam("detune:frequency") - */ -export const { envParam, envp, envparam } = registerControl('envParam', 'envp', 'envparam'); - -/** - * Controls the attack time of the envelope. - * - * @name envAttack - * @synonyms envatt, envattack - * @param {number | Pattern} envAttack Duration of attack phase. - * n(irand(12).seg(8)).scale("F#3:minor").room(1) - * .lpf(500) - * .envDepth("4800:400") - * .envTarget("source:lpf") - * .envParam("detune:frequency") - * .envAttack(0.5) - */ -export const { envAttack, envatt, envattack } = registerControl('envAttack', 'envatt', 'envattack'); - -/** - * Controls the decay time of the envelope. - * - * @name envDecay - * @synonyms envdec, envdecay - * @param {number | Pattern} envDecay Duration of decay phase. - * n(irand(12).seg(8)).scale("F#3:minor").room(1) - * .lpf(100) - * .envDepth("4800:400") - * .envTarget("source:lpf") - * .envParam("detune:frequency") - * .envDecay("0.03:0.15") - * .envDCurve(-0.4) - */ -export const { envDecay, envdec, envdecay } = registerControl('envDecay', 'envdec', 'envdecay'); - -/** - * Sets the sustain level of the envelope. - * - * @name envSustain - * @synonyms envs, envsustain - * @param {number | Pattern} envSustain Sustain amplitude level. - * n(irand(12).seg(8)).scale("F#3:minor").room(1) - * .lpf(100) - * .envDepth("4800:400") - * .envTarget("source:lpf") - * .envParam("detune:frequency") - * .envACurve(0.6).envDCurve(-0.2) - * .envSustain(0.2) - */ -export const { envSustain, envs, envsustain } = registerControl('envSustain', 'envs', 'envsustain'); - -/** - * Controls the release time of the envelope. - * - * @name envRelease - * @synonyms envr, envrelease - * @param {number | Pattern} envRelease Duration of release phase. - * @example - * n(irand(12).seg(8)).scale("F#3:minor").room(1) - * .lpf(100).release(3) - * .envDepth("4800:400") - * .envTarget("source:lpf") - * .envParam("detune:frequency") - * .envDecay("0.03:0.15").envRCurve(0.5) - * .envSustain(0.5) - * .envRelease(3) - */ -export const { envRelease, envr, envrelease } = registerControl('envRelease', 'envr', 'envrelease'); - -/** - * Sets the modulation depth of the envelope. - * - * @name envDepth - * @synonyms envd, envdepth - * @param {number | Pattern} envDepth Modulation depth amount. - * @example - * n(irand(12).seg(8)).scale("F#3:minor").room(1) - * .lpf(100) - * .envTarget("source:lpf") - * .envParam("detune:frequency") - * .envDepth("4800:400") - */ -export const { envDepth, envd, envdepth } = registerControl('envDepth', 'envd', 'envdepth'); - -/** - * Adjusts the curvature of the attack portion of the envelope. Positive values are snappy, - * negative values are slow - * - * @name envACurve - * @synonyms envac, envacurve - * @param {number | Pattern} envACurve Curvature amount (between -1 and 1). - */ -export const { envACurve, envac, envacurve } = registerControl('envACurve', 'envac', 'envacurve'); - -/** - * Adjusts the curvature of the decay portion of the envelope. Positive values are snappy, - * negative values are slow - * - * @name envDCurve - * @synonyms envdc, envdcurve - * @param {number | Pattern} envDCurve Curvature amount (between -1 and 1). - */ -export const { envDCurve, envdc, envdcurve } = registerControl('envDCurve', 'envdc', 'envdcurve'); - -/** - * Adjusts the curvature of the release portion of the envelope. Positive values are snappy, - * negative values are slow - * - * @name envRCurve - * @synonyms envrc, envrcurve - * @param {number | Pattern} envRCurve Curvature amount (between -1 and 1). - */ -export const { envRCurve, envrc, envrcurve } = registerControl('envRCurve', 'envrc', 'envrcurve'); - // TODO: slide param for certain synths export const { slide } = registerControl('slide'); // TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 4ba24b20..1eefa41e 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3706,50 +3706,120 @@ const resolveConfigKey = (funcName, key) => { return aliasMap.get(normalized) ?? key; }; -addConfigAlias('lfo', 'lfoTarget', 'lfot', 'lfotarget', 'target', 't'); -addConfigAlias('lfo', 'lfoParam', 'lfop', 'lfoparam', 'param', 'parameter', 'p'); -addConfigAlias('lfo', 'lfoRate', 'lfor', 'lforate', 'rate', 'r'); -addConfigAlias('lfo', 'lfoDepth', 'lfod', 'lfodepth', 'depth', 'd'); -addConfigAlias('lfo', 'lfoDCOffset', 'lfodc', 'lfodcoffset', 'dcoffset', 'offset', 'dc'); -addConfigAlias('lfo', 'lfoShape', 'lfosh', 'lfoshape', 'shape', 'sh'); -addConfigAlias('lfo', 'lfoSkew', 'lfosk', 'lfoskew', 'skew', 'sk'); -addConfigAlias('lfo', 'lfoCurve', 'lfoc', 'lfocurve', 'curve', 'c'); -addConfigAlias('lfo', 'lfoSync', 'lfos', 'lfosync', 'sync', 'synced', 's'); -addConfigAlias('env', 'envTarget', 'envt', 'envtarget'); -addConfigAlias('env', 'envParam', 'envp', 'envparam'); -addConfigAlias('env', 'envAttack', 'envatt', 'envattack'); -addConfigAlias('env', 'envDecay', 'envdec', 'envdecay'); -addConfigAlias('env', 'envSustain', 'envs', 'envsustain'); -addConfigAlias('env', 'envRelease', 'envr', 'envrelease'); -addConfigAlias('env', 'envDepth', 'envd', 'envdepth'); -addConfigAlias('env', 'envACurve', 'envac', 'envacurve'); -addConfigAlias('env', 'envDCurve', 'envdc', 'envdcurve'); -addConfigAlias('env', 'envRCurve', 'envrc', 'envrcurve'); +addConfigAlias('lfo', 'target', 't'); +addConfigAlias('lfo', 'param', 'p'); +addConfigAlias('lfo', 'rate', 'r'); +addConfigAlias('lfo', 'depth', 'dep', 'dp', 'd'); +addConfigAlias('lfo', 'dc'); +addConfigAlias('lfo', 'shape', 'sh'); +addConfigAlias('lfo', 'skew', 'sk'); +addConfigAlias('lfo', 'curve', 'c'); +addConfigAlias('lfo', 'sync', 's'); +addConfigAlias('env', 'target', 't'); +addConfigAlias('env', 'attack', 'att', 'a'); +addConfigAlias('env', 'decay', 'dec', 'd'); +addConfigAlias('env', 'sustain', 'sus', 's'); +addConfigAlias('env', 'release', 'rel', 'r'); +addConfigAlias('env', 'depth', 'dep', 'dp'); +addConfigAlias('env', 'acurve', 'ac'); +addConfigAlias('env', 'dcurve', 'dc'); +addConfigAlias('env', 'rcurve', 'rc'); +addConfigAlias('send', 'id'); +addConfigAlias('send', 'target', 't'); +addConfigAlias('send', 'depth', 'dep', 'dp', 'd'); +addConfigAlias('send', 'dc'); +addConfigAlias('send', 'offset', 'off', 'o'); -/** - * Establishes a signal chain. Can be called in sequence like pat.chain(...).chain(...) and so forth - * and/or in a single .chain(..., ..., etc) call. The arguments to `chain` are _patterns_ which each act like - * a self-contained pattern and follow the normal [signal chain](https://strudel.cc/learn/effects/). - * - * If multiple sound generators are present within the chain, they will be mixed in at the location where - * they are declared. - * - * @name chain - * @memberof Pattern - * @param {Pattern | Pattern[]} patterns Patterns to combine into a single chain - * @returns Pattern - */ -Pattern.prototype.lfo = function (config) { +Pattern.prototype.mod = function (type, config, idx) { if (config == null || typeof config !== 'object') { return this; } + if (!['lfo', 'send', 'env'].includes(type)) { + logger(`[core] Modulation type ${type} not found. Please use one of 'lfo', 'env', 'send'`); + return this; + } let output = this; for (const [rawKey, value] of Object.entries(config)) { - const key = resolveConfigKey('lfo', rawKey); + const key = resolveConfigKey(type, rawKey); const pat = reify(value); - output = output.set(pat.as(key)); + output = output + .fmap((v) => (c) => { + v[type] ??= []; + const t = v[type]; + idx ??= t.length; + t[idx] ??= {}; + t[idx][key] = c; + return v; + }) + .appLeft(pat); } return output; }; +/** + * Configures an LFO. Can be called in sequence like pat.lfo(...).lfo(...) to set up multiple LFOs + * + * + * @name lfo + * @memberof Pattern + * @param {Object} config LFO configuration. + * @param {string | Pattern} [config.target] Node (and parameter if specified like `lpf.frequency`) to modulate. Aliases: target, t + * @param {number | Pattern} [config.rate] Modulation rate. Aliases: rate, r + * @param {number | Pattern} [config.depth] Modulation depth. Aliases: dep, dp, d + * @param {number | Pattern} [config.dc] DC offset / bias for the waveform + * @param {number | Pattern} [config.shape] Waveform shape index. Aliases: sh + * @param {number | Pattern} [config.skew] Waveform skew amount. Aliases: sk + * @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: c + * @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s + * @param {number | null} idx Index of the LFO slot to overwrite. Omit to append a new LFO + * @returns Pattern + */ +Pattern.prototype.lfo = function (config, idx) { + return this.mod('lfo', config, idx); +}; export const lfo = (config) => pure({}).lfo(config); + +/** + * Configures an envelope. Can be called in sequence like pat.env(...).env(...) to set up multiple envelopes + * + * + * @name env + * @memberof Pattern + * @param {Object} config Envelope configuration. + * @param {string | Pattern} [config.target] Node (and parameter if specified like `lpf.frequency`) to modulate. Aliases: target, t + * @param {number | Pattern} [config.depth] Modulation depth. Aliases: dep, dp + * @param {number | Pattern} [config.attack] Time to reach depth. Aliases: att, a + * @param {number | Pattern} [config.decay] Time to reach sustain. Aliases: dec, d + * @param {number | Pattern} [config.sustain] Sustain depth. Aliases: sus, s + * @param {number | Pattern} [config.release] Time to return to nominal value. Aliases: rel, r + * @param {number | Pattern} [config.acurve] Snappiness of attack curve (-1 = relaxed, 1 = snappy). Aliases: ac + * @param {number | Pattern} [config.dcurve] Snappiness of decay curve (-1 = relaxed, 1 = snappy). Aliases: dc + * @param {number | Pattern} [config.rcurve] Snappiness of release curve (-1 = relaxed, 1 = snappy). Aliases: rc + * @param {number | null} idx Index of the envelope slot to overwrite. Omit to append a new envelope + * @returns Pattern + */ +Pattern.prototype.env = function (config, idx) { + return this.mod('env', config, idx); +}; +export const env = (config) => pure({}).env(config); + +/** + * Sends the output of this pattern to a parameter on another pattern. + * Can be called in sequence like pat.send(...).send(...) to send to multiple parameters + * + * + * @name send + * @memberof Pattern + * @param {Object} config Send configuration. + * @param {string | Pattern} [config.id] Pattern id to modulate + * @param {string | Pattern} [config.target] Node (and parameter if specified like `lpf.frequency`) to modulate. Aliases: target, t + * @param {number | Pattern} [config.depth] Modulation depth. Aliases:dep, dp, d + * @param {number | Pattern} [config.dc] DC offset prior to application + * @param {number | Pattern} [config.offset] Offset to apply to the parameter. Aliases: off, o + * @param {number | null} idx Index of the send slot to overwrite. Omit to append a new send + * @returns Pattern + */ +Pattern.prototype.send = function (config, idx) { + return this.mod('send', config, idx); +}; +export const send = (config) => pure({}).send(config); From 4ec68fcd1de98ba0cd1d0fb22de84ecddfb15d1a Mon Sep 17 00:00:00 2001 From: scrappy_fiddler Date: Thu, 20 Nov 2025 00:07:44 +0100 Subject: [PATCH 066/194] a little prettier --- website/src/pages/learn/faq.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/learn/faq.mdx b/website/src/pages/learn/faq.mdx index 2e4cb5c4..b16eb8cd 100644 --- a/website/src/pages/learn/faq.mdx +++ b/website/src/pages/learn/faq.mdx @@ -42,7 +42,7 @@ Yes you can. There are experimental modes, made by community members, for severa You can use your own samples with Strudel. There are multiple ways to load your sample collection. Some methods are good for quick experimentation, some others are good to share your audio collection with other musicians: - Import folders [from the interface](/learn/samples/#from-disk-via-import-sounds-folder) - - + - - Host your sound library e.g. on github and [load them from an URL](/learn/samples/#loading-custom-samples) ## Can I use Strudel with AI/LLM tools? From 442f4879738b7f7cb3f3f1baef8a0a5fac7cfb19 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 19 Nov 2025 17:10:05 -0600 Subject: [PATCH 067/194] Another major cleanup 90% of first pass --- packages/superdough/superdough.mjs | 257 +++++++++++------------------ packages/superdough/worklets.mjs | 4 +- 2 files changed, 100 insertions(+), 161 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 52137c51..4cd71376 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -376,8 +376,7 @@ export function resetGlobalEffects() { controller?.reset(); analysers = {}; analysersData = {}; - lfos = {}; - nodes = {}; + idToNodes = {}; } function _getNodeParam(node, name) { @@ -409,66 +408,21 @@ function _getNodeParams(node) { return Array.from(params); } -/** - * Split parameters (which might be arrays -- implying multiple-parameter modulation -- or a single number) into independent - * objects which only account for single-parameter modulation - * - * @param {Object} params - Dictionary of modulation parameters. - * @returns {Object[]} - Array of parameter objects, one per parameter modulation - */ -function _splitParams(params) { - const num = ['num', 'target', 'param'] // names used to indicate individual parameter modulations - .map((k) => [params[k] ?? 0].flat().length) - .reduce((a, v) => Math.max(a, v), 1); - - const individualParams = []; - for (let i = 0; i < num; i++) { - const paramsI = {}; - for (const k in params) { - const flatV = [params[k]].flat(); - if (flatV.length !== num && flatV.length !== 1) { - errorLogger( - new Error( - `Could not set up modulations. We derived ${num} items, but ${k}: ${JSON.stringify(flatV)} has length ${flatV.length} (needs 1 or ${num}).`, - ), - 'superdough', - ); - return []; - } - paramsI[k] = flatV[i] ?? flatV[0]; - } - individualParams.push(paramsI); - } - return individualParams; -} - -/** - * Given a node name and the name of a parameter on that node, attempt to retrieve - * all nodes corresponding to the name and their associated parameters - * - * Note that we say nodes, plural, because some nodes have multiple sub-nodes, like a - * 24db filter which is two filters in series - * - * @param {string} targetName - Name of the node to modulate parameters on (e.g. `lpf`, `source`, etc.) - * @param {string} paramName - Name of the parameter to modulate on that node - * @returns {AudioParam[]} - Array of audio parameter objects for modulation - * - */ -function _getTargetParams(targetName, paramName) { - const targetNodes = nodes[targetName]; +function _getTargetParams(nodes, target) { + const targetNodes = nodes[target]; if (!targetNodes) { const keys = Object.keys(nodes); errorLogger( new Error( - `Could not connect to target '${targetName}' — it does not exist. Available targets: ${keys.join(', ')}`, + `Could not connect to target '${target}' — it does not exist. Available targets: ${keys.join(', ')}`, ), 'superdough', ); return []; } - const audioParams = []; targetNodes.forEach((targetNode) => { + const paramName = guessParamName(blah); const targetParam = _getNodeParam(targetNode, paramName); if (!targetParam) { const available = _getNodeParams(targetNode); @@ -485,64 +439,57 @@ function _getTargetParams(targetName, paramName) { return audioParams; } -function _setWorkletParamsAtTime(audioParams, params, time) { - for (const [name, value] of params) { - if (value == null) continue; - const p = audioParams.get(name); - if (p.cancelAndHoldAtTime) p.cancelAndHoldAtTime(time); - else p.cancelScheduledValues(time); - p.setValueAtTime(value, time); - } -} - -let lfos = {}; -function _connectLFO(params, nodeTracker) { +function connectLFO(idx, params, nodeTracker) { const { - frequency = 1, - synced = 0, - cps = 0.5, - num = 1, // default to LFO 1 + rate = 1, + sync, + cps, target, - param, - begin, ...filteredParams } = params; - filteredParams['frequency'] = synced ? frequency / cps : frequency; - let lfoNode = lfos[num]; - if (lfoNode == null) { - const ac = getAudioContext(); - lfoNode = getLfo(ac, filteredParams); - lfos[num] = lfoNode; - nodeTracker[`lfo${num}`] = [lfoNode]; - } - const targets = _getTargetParams(target, param); - targets.forEach((target) => lfoNode.connect(target)); - _setWorkletParamsAtTime(lfoNode.parameters, Object.entries(filteredParams), begin); - return lfoNode; + filteredParams['frequency'] = sync !== undefined ? sync / cps : rate; + const ac = getAudioContext(); + lfoNode = getLfo(ac, filteredParams); + nodeTracker[`lfo${idx}`] = [lfoNode]; + _getTargetParams(target).forEach(lfoNode.connect); } -function _connectEnvelope(params) { - const { target, param, envDepth, begin, end, attack, decay, sustain, release, curve, ...filteredParams } = params; - const targets = _getTargetParams(target, param); - const [att, dec, sus, rel] = getADSRValues([attack, decay, sustain, release], curve, [0.005, 0.14, 0, 0.1]); - targets.forEach((targetParam) => { - const currentValue = targetParam.value; - const min = currentValue; - const max = currentValue + envDepth; - getParamADSR(targetParam, att, dec, sus, rel, min, max, begin, end, curve); - }); +function connectEnvelope(idx, params, nodeTracker) { + const { target, ...filteredParams } = params; + const ac = getAudioContext(); + envNode = getEnvelope(ac, filteredParams); + nodeTracker[`env${idx}`] = [envNode]; + _getTargetParams(nodeTracker, target).forEach(envNode.connect); } -function connectModulators(params, modulatorType, nodeTracker) { - // We break down params specifying multiple modulators into a set of parameters for - // a single one - const individualParams = _splitParams(params); - if (modulatorType === 'lfo') { - individualParams.map((p) => _connectLFO(p, nodeTracker)); - } else if (modulatorType === 'envelope') { - individualParams.forEach(_connectEnvelope); - } - return []; +function connectSendModulator(params, signal, nodeTracker, pendingConnections) { + const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 }); + dc.start(t); + const offset = new ConstantSourceNode(ac, { offset: params.offset ?? 0 }); + offset.start(t); + const raw = dc.connect(gainNode(1)); + const modulator = post + .connect(raw) + .connect(gainNode((params.depth ?? 1) / 0.3)) + .connect(gainNode(1)); + offset.connect(modulator); + webAudioTimeout( + ac, + () => { + _getTargetParams(nodeTracker, params.target).forEach(signal.connect);; + }, + 0, + params.begin, + ); + webAudioTimeout( + ac, + () => { + signal.disconnect(); + delete pendingConnections[params.id][chainID]; + }, + 0, + params.end + 0.05, + ); } let activeSoundSources = new Map(); @@ -552,9 +499,10 @@ function mapChannelNumbers(channels) { return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); } -let nodes = {}; - +let idToNodes = {}; +let pendingConnections = {}; export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { + const nodes = {}; // new: t is always expected to be the absolute target onset time const ac = getAudioContext(); const audioController = getSuperdoughAudioController(); @@ -644,24 +592,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) compressorKnee, compressorAttack, compressorRelease, - lfoNum, - lfoTarget, - lfoParam, - lfoRate, - lfoDepth, - lfoDCOffset, - lfoShape, - lfoSkew, - lfoCurve, - lfoSynced, - envTarget, - envParam, - envAttack, - envDecay, - envSustain, - envRelease, - envCurve, - envDepth, } = value; delaytime = delaytime ?? cycleToSeconds(delaysync, cps); @@ -991,45 +921,56 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); audioNodes = audioNodes.concat(chain); - // finally, now that `nodes` is populated, set up LFOs and envelopes - if (lfoTarget !== undefined && lfoParam !== undefined) { - connectModulators( - { - num: lfoNum ?? 1, - target: lfoTarget, - param: lfoParam, - frequency: lfoRate, - depth: lfoDepth, - dcoffset: lfoDCOffset ?? 0, // override default value of 0.5 - shape: lfoShape, - skew: lfoSkew, - curve: lfoCurve, - persistent: 1, - begin: t, - synced: lfoSynced, - cps: cps, - }, - 'lfo', - nodes, - ); + // finally, now that `nodes` is populated, set up modulators + if (value.lfo) { + for (const [params, idx] of Object.entries(value.lfo)) { + connectLFO( + idx, + { + ...params, + cps, + begin: t, + end: endWithRelease, + }, + 'lfo', + nodes, + ); + } } - if (envTarget !== undefined && envParam !== undefined) { - connectModulators( - { - target: envTarget, - param: envParam, - envDepth, - attack: envAttack, - decay: envDecay, - sustain: envSustain, - release: envRelease, - curve: envCurve, - begin: t, - end: endWithRelease, - }, - 'envelope', - ); + if (value.env) { + for (const [params, idx] of Object.entries(value.env)) { + connectEnvelope( + idx, + { + ...params, + begin: t, + end: endWithRelease, + }, + 'envelope', + nodes, + ); + } } + if (value.id) { + idToNodes[value.id] = new WeakRef(nodes); + } + if (value.send) { + for (const p of value.env) { + const modNodes = idToNodes[p.id]; + if (!modNodes) { + logger( + `[superdough] Could not connect to pattern ${p.id} -- make sure a pattern with this name exists. Available targets: ${Object.keys(idToNodes).join(', ')}`, + ); + } else { + connectSendModulator(params, post); + pendingConnections[p.id] ??= {}; + pendingConnections[p.id][chainID] = [modulator, p.target, p.param, endWithRelease]; + } + }); + } + + if (applySends) { + }; export const superdoughTrigger = (t, hap, ct, cps) => { diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 46605556..a098929b 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -123,7 +123,6 @@ class LFOProcessor extends AudioWorkletProcessor { { name: 'dcoffset', defaultValue: 0 }, { name: 'min', defaultValue: 0 }, { name: 'max', defaultValue: 1 }, - { name: 'persistent', defaultValue: 0, min: 0, max: 1 }, // whether to ignore end ]; } @@ -142,8 +141,7 @@ class LFOProcessor extends AudioWorkletProcessor { process(_inputs, outputs, parameters) { const begin = parameters['begin'][0]; const end = parameters['end'][0]; - const persistent = parameters['persistent'][0]; - if (persistent < 0.5 && currentTime >= end) { + if (currentTime >= end) { return false; } if (currentTime <= begin) { From f8796d039ff90d4408068cff633cc2933617fbf9 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 19 Nov 2025 17:18:38 -0600 Subject: [PATCH 068/194] Add param guesses --- packages/superdough/superdough.mjs | 36 +++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 4cd71376..df1090a8 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -408,7 +408,38 @@ function _getNodeParams(node) { return Array.from(params); } +const targetToParamGuess = { + source: 'detune', + lpf: 'frequency', + bpf: 'frequency', + hpf: 'frequency', + distort: 'distort', + gain: 'gain', + vowel: 'frequency', + coarse: 'coarse', + crush: 'crush', + shape: 'shape', + compressor: 'threshold', + pan: 'pan', + phaser: 'rate', + post: 'gain', + delay: 'time', + room: 'size', + djf: 'value', + lfo: 'frequency', + env: 'depth', + send: 'depth', +} + function _getTargetParams(nodes, target) { + let param; + if (target.includes('.')) { + const split = target.split('.'); + target = split[0]; + param = split[1]; + } else { + param = targetToParamGuess[target]; + } const targetNodes = nodes[target]; if (!targetNodes) { const keys = Object.keys(nodes); @@ -422,13 +453,12 @@ function _getTargetParams(nodes, target) { } const audioParams = []; targetNodes.forEach((targetNode) => { - const paramName = guessParamName(blah); - const targetParam = _getNodeParam(targetNode, paramName); + const targetParam = _getNodeParam(targetNode, param); if (!targetParam) { const available = _getNodeParams(targetNode); errorLogger( new Error( - `Could not connect to parameter '${paramName}' on '${targetName}'. Available parameters: ${available.join(', ')}`, + `Could not connect to parameter '${param}' on '${targetName}'. Available parameters: ${available.join(', ')}`, ), 'superdough', ); From 0dbca05de09a07a967318f07595829edffc1a74d Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 19 Nov 2025 17:24:33 -0600 Subject: [PATCH 069/194] Ready for testing --- packages/superdough/superdough.mjs | 50 ++++++++++++++++++------------ 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index df1090a8..1262a926 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -423,13 +423,13 @@ const targetToParamGuess = { pan: 'pan', phaser: 'rate', post: 'gain', - delay: 'time', + delay: 'delayTime', room: 'size', djf: 'value', lfo: 'frequency', env: 'depth', send: 'depth', -} +}; function _getTargetParams(nodes, target) { let param; @@ -438,15 +438,14 @@ function _getTargetParams(nodes, target) { target = split[0]; param = split[1]; } else { - param = targetToParamGuess[target]; + const targetWithoutIndex = target.replace(/(\d+)$/, ''); + param = targetToParamGuess[targetWithoutIndex]; } const targetNodes = nodes[target]; if (!targetNodes) { const keys = Object.keys(nodes); errorLogger( - new Error( - `Could not connect to target '${target}' — it does not exist. Available targets: ${keys.join(', ')}`, - ), + new Error(`Could not connect to target '${target}' — it does not exist. Available targets: ${keys.join(', ')}`), 'superdough', ); return []; @@ -470,13 +469,7 @@ function _getTargetParams(nodes, target) { } function connectLFO(idx, params, nodeTracker) { - const { - rate = 1, - sync, - cps, - target, - ...filteredParams - } = params; + const { rate = 1, sync, cps, target, ...filteredParams } = params; filteredParams['frequency'] = sync !== undefined ? sync / cps : rate; const ac = getAudioContext(); lfoNode = getLfo(ac, filteredParams); @@ -506,7 +499,7 @@ function connectSendModulator(params, signal, nodeTracker, pendingConnections) { webAudioTimeout( ac, () => { - _getTargetParams(nodeTracker, params.target).forEach(signal.connect);; + _getTargetParams(nodeTracker, params.target).forEach(signal.connect); }, 0, params.begin, @@ -994,13 +987,32 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } else { connectSendModulator(params, post); pendingConnections[p.id] ??= {}; - pendingConnections[p.id][chainID] = [modulator, p.target, p.param, endWithRelease]; + pendingConnections[p.id][chainID] = new WeakRef([modulator, p.target, endWithRelease]); } - }); + } + } + if (value.id in pendingConnections) { + for (const data of Object.values(pendingConnections[id])) { + const [modulator, target, end] = data.deref(); + const targets = _getTargetParams(nodes, target); + webAudioTimeout( + ac, + () => { + targets.forEach((target) => modulator.connect(target)); + }, + 0, + t, + ); + webAudioTimeout( + ac, + () => { + modulator.disconnect(); + }, + 0, + end, + ); + } } - - if (applySends) { - }; export const superdoughTrigger = (t, hap, ct, cps) => { From 19b710c401831f2fd67673bc725061753138ab7a Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 19 Nov 2025 19:44:02 -0600 Subject: [PATCH 070/194] Mostly working version --- packages/core/controls.mjs | 1 - packages/core/pattern.mjs | 12 +++--- packages/soundfonts/fontloader.mjs | 2 +- packages/superdough/helpers.mjs | 20 +++------ packages/superdough/sampler.mjs | 2 +- packages/superdough/superdough.mjs | 68 +++++++++++++----------------- packages/superdough/synth.mjs | 14 +++--- packages/superdough/worklets.mjs | 6 +-- packages/superdough/zzfx.mjs | 1 + 9 files changed, 56 insertions(+), 70 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index bcab1d1b..eab90442 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2320,7 +2320,6 @@ export const { curve } = registerControl('curve'); export const { deltaSlide } = registerControl('deltaSlide'); export const { pitchJump } = registerControl('pitchJump'); export const { pitchJumpTime } = registerControl('pitchJumpTime'); -export const { lfo, repeatTime } = registerControl('lfo', 'repeatTime'); // noise on the frequency or as bubo calls it "frequency fog" :) export const { znoise } = registerControl('znoise'); export const { zmod } = registerControl('zmod'); diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 1eefa41e..a15ee42e 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3710,7 +3710,7 @@ addConfigAlias('lfo', 'target', 't'); addConfigAlias('lfo', 'param', 'p'); addConfigAlias('lfo', 'rate', 'r'); addConfigAlias('lfo', 'depth', 'dep', 'dp', 'd'); -addConfigAlias('lfo', 'dc'); +addConfigAlias('lfo', 'dcoffset', 'dc'); addConfigAlias('lfo', 'shape', 'sh'); addConfigAlias('lfo', 'skew', 'sk'); addConfigAlias('lfo', 'curve', 'c'); @@ -3730,7 +3730,7 @@ addConfigAlias('send', 'depth', 'dep', 'dp', 'd'); addConfigAlias('send', 'dc'); addConfigAlias('send', 'offset', 'off', 'o'); -Pattern.prototype.mod = function (type, config, idx) { +Pattern.prototype.modulate = function (type, config, idx) { if (config == null || typeof config !== 'object') { return this; } @@ -3766,7 +3766,7 @@ Pattern.prototype.mod = function (type, config, idx) { * @param {string | Pattern} [config.target] Node (and parameter if specified like `lpf.frequency`) to modulate. Aliases: target, t * @param {number | Pattern} [config.rate] Modulation rate. Aliases: rate, r * @param {number | Pattern} [config.depth] Modulation depth. Aliases: dep, dp, d - * @param {number | Pattern} [config.dc] DC offset / bias for the waveform + * @param {number | Pattern} [config.dcoffset] DC offset / bias for the waveform. Aliases: dc * @param {number | Pattern} [config.shape] Waveform shape index. Aliases: sh * @param {number | Pattern} [config.skew] Waveform skew amount. Aliases: sk * @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: c @@ -3775,7 +3775,7 @@ Pattern.prototype.mod = function (type, config, idx) { * @returns Pattern */ Pattern.prototype.lfo = function (config, idx) { - return this.mod('lfo', config, idx); + return this.modulate('lfo', config, idx); }; export const lfo = (config) => pure({}).lfo(config); @@ -3799,7 +3799,7 @@ export const lfo = (config) => pure({}).lfo(config); * @returns Pattern */ Pattern.prototype.env = function (config, idx) { - return this.mod('env', config, idx); + return this.modulate('env', config, idx); }; export const env = (config) => pure({}).env(config); @@ -3820,6 +3820,6 @@ export const env = (config) => pure({}).env(config); * @returns Pattern */ Pattern.prototype.send = function (config, idx) { - return this.mod('send', config, idx); + return this.modulate('send', config, idx); }; export const send = (config) => pure({}).send(config); diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index 427f57e3..9b0525ec 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -176,7 +176,7 @@ export function registerSoundfonts() { node.disconnect(); onended(); }; - return { node, stop }; + return { node, stop, source: bufferSource }; }, { type: 'soundfont', prebake: true, fonts }, ); diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 47161ed6..00ea3883 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -105,22 +105,16 @@ function getModulationShapeInput(val) { return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0; } -export function getLfo(audioContext, begin, end, properties = {}) { - const { shape = 0, ...props } = properties; - const { dcoffset = -0.5, depth = 1 } = properties; +export function getEnvelope(audioContext, properties = {}) { + return getWorklet(audioContext, 'envelope-processor', properties); +} + +export function getLfo(audioContext, properties = {}) { + // Extract some params we need for deriving other params + const { begin, shape = 0, ...props } = properties; const lfoprops = { - frequency: 1, - depth, - skew: 0.5, - phaseoffset: 0, time: begin, - begin, - end, shape: getModulationShapeInput(shape), - dcoffset, - min: dcoffset * depth, - max: dcoffset * depth + depth, - curve: 1, ...props, }; diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 84bac358..a966252d 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -331,7 +331,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { const stop = (endTime) => { bufferSource.stop(endTime); }; - const handle = { node: out, bufferSource, stop }; + const handle = { node: out, source: bufferSource, stop }; // cut groups if (cut !== undefined) { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 1262a926..b2017a9d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -16,13 +16,14 @@ import { getADSRValues, getCompressor, getDistortion, + getEnvelope, getLfo, getParamADSR, getWorklet, webAudioTimeout, } from './helpers.mjs'; import { map } from 'nanostores'; -import { logger } from './logger.mjs'; +import { errorLogger, logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; import { getAudioContext } from './audioContext.mjs'; import { SuperdoughAudioController } from './superdoughoutput.mjs'; @@ -292,18 +293,6 @@ function getSuperdoughAudioController() { return controller; } -export function getLfo(audioContext, properties = {}) { - // Extract some params we need for deriving other params - const { begin, shape = 0, ...props } = properties; - const lfoprops = { - time: begin, - shape: getModulationShapeInput(shape), - ...props, - }; - - return getWorklet(audioContext, 'lfo-processor', lfoprops); -} - export function connectToDestination(input, channels) { const controller = getSuperdoughAudioController(); controller.output.connectToDestination(input, channels); @@ -445,7 +434,7 @@ function _getTargetParams(nodes, target) { if (!targetNodes) { const keys = Object.keys(nodes); errorLogger( - new Error(`Could not connect to target '${target}' — it does not exist. Available targets: ${keys.join(', ')}`), + `Could not connect to target '${target}' — it does not exist. Available targets: ${keys.join(', ')}`, 'superdough', ); return []; @@ -456,9 +445,7 @@ function _getTargetParams(nodes, target) { if (!targetParam) { const available = _getNodeParams(targetNode); errorLogger( - new Error( - `Could not connect to parameter '${param}' on '${targetName}'. Available parameters: ${available.join(', ')}`, - ), + `Could not connect to parameter '${param}' on '${target}'. Available parameters: ${available.join(', ')}`, 'superdough', ); return; @@ -472,26 +459,27 @@ function connectLFO(idx, params, nodeTracker) { const { rate = 1, sync, cps, target, ...filteredParams } = params; filteredParams['frequency'] = sync !== undefined ? sync / cps : rate; const ac = getAudioContext(); - lfoNode = getLfo(ac, filteredParams); + const lfoNode = getLfo(ac, filteredParams); nodeTracker[`lfo${idx}`] = [lfoNode]; - _getTargetParams(target).forEach(lfoNode.connect); + _getTargetParams(nodeTracker, target).forEach((t) => lfoNode.connect(t)); } function connectEnvelope(idx, params, nodeTracker) { const { target, ...filteredParams } = params; const ac = getAudioContext(); - envNode = getEnvelope(ac, filteredParams); + const envNode = getEnvelope(ac, filteredParams); nodeTracker[`env${idx}`] = [envNode]; - _getTargetParams(nodeTracker, target).forEach(envNode.connect); + _getTargetParams(nodeTracker, target).forEach((t) => envNode.connect(t)); } -function connectSendModulator(params, signal, nodeTracker, pendingConnections) { +function connectSendModulator(params, signal, nodeTracker, chainID) { + const ac = getAudioContext(); const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 }); - dc.start(t); + dc.start(params.begin); const offset = new ConstantSourceNode(ac, { offset: params.offset ?? 0 }); - offset.start(t); + offset.start(params.begin); const raw = dc.connect(gainNode(1)); - const modulator = post + const modulator = signal .connect(raw) .connect(gainNode((params.depth ?? 1) / 0.3)) .connect(gainNode(1)); @@ -499,7 +487,7 @@ function connectSendModulator(params, signal, nodeTracker, pendingConnections) { webAudioTimeout( ac, () => { - _getTargetParams(nodeTracker, params.target).forEach(signal.connect); + _getTargetParams(nodeTracker, params.target).forEach((t) => modulator.connect(t)); }, 0, params.begin, @@ -507,12 +495,13 @@ function connectSendModulator(params, signal, nodeTracker, pendingConnections) { webAudioTimeout( ac, () => { - signal.disconnect(); + modulator.disconnect(); delete pendingConnections[params.id][chainID]; }, 0, params.end + 0.05, ); + return modulator; } let activeSoundSources = new Map(); @@ -679,7 +668,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) if (soundHandle) { sourceNode = soundHandle.node; activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC - nodes['source'] = [soundHandle.oscillator]; + nodes['source'] = [soundHandle.source]; } } else { throw new Error(`sound ${s} not found! Is it loaded?`); @@ -761,7 +750,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) hpParams.type = 'highpass'; const hp1 = filt(hpParams); nodes['hpf'] = [hp1]; - chain.push(hp()); + chain.push(hp1); if (ftype === '24db') { const hp2 = filt(hpParams); nodes['hpf'].push(hp1); @@ -791,7 +780,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) const bpParams = pickAndRename(value, bpMap); bpParams.type = 'bandpass'; const bp1 = filt(bpParams); - chain.push(bp()); + chain.push(bp1); if (ftype === '24db') { const bp2 = filt(bpParams); nodes['bpf'].push(bp2); @@ -946,7 +935,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // finally, now that `nodes` is populated, set up modulators if (value.lfo) { - for (const [params, idx] of Object.entries(value.lfo)) { + for (const [idx, params] of Object.entries(value.lfo)) { connectLFO( idx, { @@ -955,13 +944,12 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) begin: t, end: endWithRelease, }, - 'lfo', nodes, ); } } if (value.env) { - for (const [params, idx] of Object.entries(value.env)) { + for (const [idx, params] of Object.entries(value.env)) { connectEnvelope( idx, { @@ -969,7 +957,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) begin: t, end: endWithRelease, }, - 'envelope', nodes, ); } @@ -978,22 +965,27 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) idToNodes[value.id] = new WeakRef(nodes); } if (value.send) { - for (const p of value.env) { + for (const p of value.send) { const modNodes = idToNodes[p.id]; if (!modNodes) { logger( `[superdough] Could not connect to pattern ${p.id} -- make sure a pattern with this name exists. Available targets: ${Object.keys(idToNodes).join(', ')}`, ); } else { - connectSendModulator(params, post); + const modulator = connectSendModulator({ ...p, begin: t, end: endWithRelease }, post, nodes, chainID); pendingConnections[p.id] ??= {}; pendingConnections[p.id][chainID] = new WeakRef([modulator, p.target, endWithRelease]); } } } if (value.id in pendingConnections) { - for (const data of Object.values(pendingConnections[id])) { - const [modulator, target, end] = data.deref(); + for (const data of Object.values(pendingConnections[value.id])) { + const derefData = data.deref(); + if (!derefData) { + delete pendingConnections[value.id]; + continue; + } + const [modulator, target, end] = derefData; const targets = _getTargetParams(nodes, target); webAudioTimeout( ac, diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index ef57ece3..1b88a1e8 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -71,7 +71,7 @@ export function registerSynthSounds() { stop(envEnd); return { node, - oscillator: o, + source: o, stop: (endTime) => { stop(endTime); }, @@ -141,7 +141,7 @@ export function registerSynthSounds() { return { node, - oscillator: o, + source: o, stop: (endTime) => { o.stop(endTime); }, @@ -208,7 +208,7 @@ export function registerSynthSounds() { return { node: envGain, - oscillator: o, + source: o, stop: (time) => { timeoutNode.stop(time); }, @@ -285,7 +285,7 @@ export function registerSynthSounds() { return { node: envGain, - oscillator: o, + source: o, stop: (time) => { timeoutNode.stop(time); }, @@ -363,7 +363,7 @@ export function registerSynthSounds() { return { node: envGain, - oscillator: o, + source: o, stop: (time) => { timeoutNode.stop(time); }, @@ -409,7 +409,7 @@ export function registerSynthSounds() { stop(envEnd); return { node, - oscillator: o, + source: o, stop: (endTime) => { stop(endTime); }, @@ -503,7 +503,7 @@ export function getOscillator(s, t, value) { return { node: noiseMix?.node || o, - oscillator: o, + source: o, stop: (time) => { fmModulator.stop(time); vibratoOscillator?.stop(time); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index a098929b..cf8d61ba 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -964,7 +964,7 @@ class EnvelopeProcessor extends AudioWorkletProcessor { { name: 'attackCurve', defaultValue: 0, minValue: -1, maxValue: 1 }, { name: 'decayCurve', defaultValue: 0, minValue: -1, maxValue: 1 }, { name: 'releaseCurve', defaultValue: 0, minValue: -1, maxValue: 1 }, - { name: 'peak', defaultValue: 1 }, + { name: 'depth', defaultValue: 1 }, { name: 'retrigger', defaultValue: 1, minValue: 0, maxValue: 1 }, ]; } @@ -1026,7 +1026,7 @@ class EnvelopeProcessor extends AudioWorkletProcessor { const aCurve = pv(params.attackCurve, i); const dCurve = pv(params.decayCurve, i); const rCurve = pv(params.releaseCurve, i); - const peak = pv(params.peak, i); + const depth = pv(params.depth, i); const states = [ { time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle { time: attack, start: this.attackStart, target: 1, curve: aCurve }, @@ -1040,7 +1040,7 @@ class EnvelopeProcessor extends AudioWorkletProcessor { this.state = (this.state + 1) % states.length; time = states[this.state].time; } - out[i] = this.val * peak; + out[i] = this.val * depth; } return true; } diff --git a/packages/superdough/zzfx.mjs b/packages/superdough/zzfx.mjs index 32db0395..c99b8b9c 100644 --- a/packages/superdough/zzfx.mjs +++ b/packages/superdough/zzfx.mjs @@ -74,6 +74,7 @@ export const getZZFX = (value, t) => { source.start(t); return { node: source, + source, }; }; From c3ad0d1789d526e006e0d401ad9319ad6f186def Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 19 Nov 2025 20:01:17 -0600 Subject: [PATCH 071/194] Fix for curve params --- packages/superdough/superdough.mjs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index b2017a9d..2fd74e09 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -465,9 +465,14 @@ function connectLFO(idx, params, nodeTracker) { } function connectEnvelope(idx, params, nodeTracker) { - const { target, ...filteredParams } = params; + const { target, acurve, dcurve, rcurve, ...filteredParams } = params; const ac = getAudioContext(); - const envNode = getEnvelope(ac, filteredParams); + const envNode = getEnvelope(ac, { + ...filteredParams, + attackCurve: acurve, + decayCurve: dcurve, + releaseCurve: rcurve, + }); nodeTracker[`env${idx}`] = [envNode]; _getTargetParams(nodeTracker, target).forEach((t) => envNode.connect(t)); } From 3e118ce0a88caabce0d8cdacf7cac7e25b1eedfd Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 19 Nov 2025 20:11:02 -0600 Subject: [PATCH 072/194] Fully working version --- packages/superdough/superdough.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 2fd74e09..c005cae9 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -971,13 +971,14 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } if (value.send) { for (const p of value.send) { - const modNodes = idToNodes[p.id]; + const modNodes = idToNodes[p.id].deref(); if (!modNodes) { logger( `[superdough] Could not connect to pattern ${p.id} -- make sure a pattern with this name exists. Available targets: ${Object.keys(idToNodes).join(', ')}`, ); + delete idToNodes[p.id]; } else { - const modulator = connectSendModulator({ ...p, begin: t, end: endWithRelease }, post, nodes, chainID); + const modulator = connectSendModulator({ ...p, begin: t, end: endWithRelease }, post, modNodes, chainID); pendingConnections[p.id] ??= {}; pendingConnections[p.id][chainID] = new WeakRef([modulator, p.target, endWithRelease]); } From 2794541dadaccfa828aa0f8c8ce27b29e802c8dd Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 19 Nov 2025 20:52:51 -0600 Subject: [PATCH 073/194] Clean up send nodes and remove offset --- packages/core/pattern.mjs | 2 - packages/superdough/superdough.mjs | 21 ++-- test/__snapshots__/examples.test.mjs.snap | 111 ---------------------- 3 files changed, 11 insertions(+), 123 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index a15ee42e..ecca31af 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3728,7 +3728,6 @@ addConfigAlias('send', 'id'); addConfigAlias('send', 'target', 't'); addConfigAlias('send', 'depth', 'dep', 'dp', 'd'); addConfigAlias('send', 'dc'); -addConfigAlias('send', 'offset', 'off', 'o'); Pattern.prototype.modulate = function (type, config, idx) { if (config == null || typeof config !== 'object') { @@ -3815,7 +3814,6 @@ export const env = (config) => pure({}).env(config); * @param {string | Pattern} [config.target] Node (and parameter if specified like `lpf.frequency`) to modulate. Aliases: target, t * @param {number | Pattern} [config.depth] Modulation depth. Aliases:dep, dp, d * @param {number | Pattern} [config.dc] DC offset prior to application - * @param {number | Pattern} [config.offset] Offset to apply to the parameter. Aliases: off, o * @param {number | null} idx Index of the send slot to overwrite. Omit to append a new send * @returns Pattern */ diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index c005cae9..0c3e9a71 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -481,14 +481,9 @@ function connectSendModulator(params, signal, nodeTracker, chainID) { const ac = getAudioContext(); const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 }); dc.start(params.begin); - const offset = new ConstantSourceNode(ac, { offset: params.offset ?? 0 }); - offset.start(params.begin); - const raw = dc.connect(gainNode(1)); - const modulator = signal - .connect(raw) - .connect(gainNode((params.depth ?? 1) / 0.3)) - .connect(gainNode(1)); - offset.connect(modulator); + const shifted = dc.connect(gainNode(1)); + signal.connect(shifted); + const modulator = shifted.connect(gainNode((params.depth ?? 1) / 0.3)); webAudioTimeout( ac, () => { @@ -506,7 +501,7 @@ function connectSendModulator(params, signal, nodeTracker, chainID) { 0, params.end + 0.05, ); - return modulator; + return { modulator, nodes: [dc, shifted, modulator] }; } let activeSoundSources = new Map(); @@ -978,7 +973,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) ); delete idToNodes[p.id]; } else { - const modulator = connectSendModulator({ ...p, begin: t, end: endWithRelease }, post, modNodes, chainID); + const { modulator, nodes: nodesToCleanup } = connectSendModulator( + { ...p, begin: t, end: endWithRelease }, + post, + modNodes, + chainID, + ); + audioNodes = audioNodes.concat(nodesToCleanup); pendingConnections[p.id] ??= {}; pendingConnections[p.id][chainID] = new WeakRef([modulator, p.target, endWithRelease]); } diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index d0a0153b..32e61c31 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3656,117 +3656,6 @@ exports[`runs examples > example "end" example index 0 1`] = ` ] `; -exports[`runs examples > example "envCurve" example index 0 1`] = ` -[ - "[ 0/1 → 1/8 | note:F#3 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 1/8 → 1/4 | note:G#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 1/4 → 3/8 | note:C#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 3/8 → 1/2 | note:C#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 1/2 → 5/8 | note:B3 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 5/8 → 3/4 | note:G#3 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 3/4 → 7/8 | note:A3 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 7/8 → 1/1 | note:C#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 1/1 → 9/8 | note:E4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 9/8 → 5/4 | note:G#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 5/4 → 11/8 | note:G#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 11/8 → 3/2 | note:A3 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 3/2 → 13/8 | note:F#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 13/8 → 7/4 | note:D4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 7/4 → 15/8 | note:A3 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 15/8 → 2/1 | note:F#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 2/1 → 17/8 | note:C#5 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 17/8 → 9/4 | note:B4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 9/4 → 19/8 | note:C#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 19/8 → 5/2 | note:B4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 5/2 → 21/8 | note:D4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 21/8 → 11/4 | note:G#3 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 11/4 → 23/8 | note:F#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 23/8 → 3/1 | note:F#3 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 3/1 → 25/8 | note:A3 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 25/8 → 13/4 | note:C#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 13/4 → 27/8 | note:C#5 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 27/8 → 7/2 | note:C#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 7/2 → 29/8 | note:C#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 29/8 → 15/4 | note:C#5 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 15/4 → 31/8 | note:A4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", - "[ 31/8 → 4/1 | note:G#4 room:1 cutoff:100 release:2 envTarget:[source lpf] envParam:[detune frequency] envDepth:[500 4000] envDecay:[0.3 0.15] envCurve:[lin exp] ]", -] -`; - -exports[`runs examples > example "envDepth" example index 0 1`] = ` -[ - "[ 0/1 → 1/8 | note:F#3 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 1/8 → 1/4 | note:G#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 1/4 → 3/8 | note:C#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 3/8 → 1/2 | note:C#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 1/2 → 5/8 | note:B3 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 5/8 → 3/4 | note:G#3 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 3/4 → 7/8 | note:A3 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 7/8 → 1/1 | note:C#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 1/1 → 9/8 | note:E4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 9/8 → 5/4 | note:G#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 5/4 → 11/8 | note:G#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 11/8 → 3/2 | note:A3 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 3/2 → 13/8 | note:F#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 13/8 → 7/4 | note:D4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 7/4 → 15/8 | note:A3 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 15/8 → 2/1 | note:F#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 2/1 → 17/8 | note:C#5 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 17/8 → 9/4 | note:B4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 9/4 → 19/8 | note:C#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 19/8 → 5/2 | note:B4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 5/2 → 21/8 | note:D4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 21/8 → 11/4 | note:G#3 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 11/4 → 23/8 | note:F#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 23/8 → 3/1 | note:F#3 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 3/1 → 25/8 | note:A3 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 25/8 → 13/4 | note:C#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 13/4 → 27/8 | note:C#5 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 27/8 → 7/2 | note:C#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 7/2 → 29/8 | note:C#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 29/8 → 15/4 | note:C#5 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 15/4 → 31/8 | note:A4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", - "[ 31/8 → 4/1 | note:G#4 room:1 cutoff:100 envTarget:[source lpf] envParam:[detune frequency] envDepth:[4800 400] ]", -] -`; - -exports[`runs examples > example "envRelease" example index 0 1`] = ` -[ - "[ 0/1 → 1/8 | note:F#3 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 1/8 → 1/4 | note:G#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 1/4 → 3/8 | note:C#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 3/8 → 1/2 | note:C#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 1/2 → 5/8 | note:B3 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 5/8 → 3/4 | note:G#3 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 3/4 → 7/8 | note:A3 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 7/8 → 1/1 | note:C#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 1/1 → 9/8 | note:E4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 9/8 → 5/4 | note:G#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 5/4 → 11/8 | note:G#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 11/8 → 3/2 | note:A3 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 3/2 → 13/8 | note:F#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 13/8 → 7/4 | note:D4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 7/4 → 15/8 | note:A3 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 15/8 → 2/1 | note:F#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 2/1 → 17/8 | note:C#5 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 17/8 → 9/4 | note:B4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 9/4 → 19/8 | note:C#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 19/8 → 5/2 | note:B4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 5/2 → 21/8 | note:D4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 21/8 → 11/4 | note:G#3 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 11/4 → 23/8 | note:F#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 23/8 → 3/1 | note:F#3 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 3/1 → 25/8 | note:A3 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 25/8 → 13/4 | note:C#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 13/4 → 27/8 | note:C#5 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 27/8 → 7/2 | note:C#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 7/2 → 29/8 | note:C#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 29/8 → 15/4 | note:C#5 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 15/4 → 31/8 | note:A4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", - "[ 31/8 → 4/1 | note:G#4 room:1 cutoff:100 release:3 envDepth:[4800 400] envTarget:[source lpf] envParam:[detune frequency] envDecay:[0.03 0.15] envCurve:[exp exp] envSustain:0.5 envRelease:3 ]", -] -`; - exports[`runs examples > example "euclid" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:c3 ]", From a929b36770ba3bdbcd76fc16f82fc89395b6929a Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 20 Nov 2025 14:06:45 -0600 Subject: [PATCH 074/194] Remove room --- packages/superdough/superdough.mjs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 0c3e9a71..5ebdd230 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -413,7 +413,6 @@ const targetToParamGuess = { phaser: 'rate', post: 'gain', delay: 'delayTime', - room: 'size', djf: 'value', lfo: 'frequency', env: 'depth', @@ -904,8 +903,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } roomIR = await loadBuffer(url, ac, ir, 0); } - const reverbNode = orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin); - nodes['room'] = [reverbNode]; + orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin); const reverbSend = orbitBus.sendReverb(post, room); audioNodes.push(reverbSend); } From 8529516c663ee8c34b8ef108a148c56c1b690fa6 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 25 Nov 2025 13:05:27 -0600 Subject: [PATCH 075/194] Add ability to turn mini parsing off with midi-off decorator --- packages/transpiler/test/transpiler.test.mjs | 14 ++++++++ packages/transpiler/transpiler.mjs | 38 ++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/packages/transpiler/test/transpiler.test.mjs b/packages/transpiler/test/transpiler.test.mjs index 02970cf4..68bec915 100644 --- a/packages/transpiler/test/transpiler.test.mjs +++ b/packages/transpiler/test/transpiler.test.mjs @@ -42,4 +42,18 @@ describe('transpiler', () => { [12, 14], ]); }); + it('allows disabling mini', () => { + const code = `/* mini-off */ + const randPrefix = Math.random() > 0.5 ? "b" : "s"; + const drumPat = \`\${randPrefix}d\`; + // mini-on + s(drumPat).lpf("5000 10000") // make sure mini still runs; + `; + const { output, miniLocations } = transpiler(code, { ...simple, emitMiniLocations: true }); + expect(output).not.toContain("m('b'"); + expect(output).not.toContain("m('s'"); + const cutoffIdx = code.indexOf('5000 10000'); + expect(miniLocations).toHaveLength(2); + expect(miniLocations[0][0]).toEqual(cutoffIdx); + }); }); diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index fea6bad4..b394591c 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -21,12 +21,15 @@ export function registerLanguage(type, config) { export function transpiler(input, options = {}) { const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options; + const comments = []; let ast = parse(input, { ecmaVersion: 2022, allowAwaitOutsideFunction: true, locations: true, + onComment: comments, }); + const miniDisableRanges = findMiniDisableRanges(comments, input.length); let miniLocations = []; const collectMiniLocations = (value, node) => { const minilang = languages.get('minilang'); @@ -66,6 +69,9 @@ export function transpiler(input, options = {}) { return this.replace(tidalWithLocation(raw, offset)); } if (isBackTickString(node, parent)) { + if (isMiniDisabled(node.start, miniDisableRanges)) { + return; + } const { quasis } = node; const { raw } = quasis[0].value; this.skip(); @@ -73,6 +79,9 @@ export function transpiler(input, options = {}) { return this.replace(miniWithLocation(raw, node)); } if (isStringWithDoubleQuotes(node)) { + if (isMiniDisabled(node.start, miniDisableRanges)) { + return; + } const { value } = node; this.skip(); emitMiniLocations && collectMiniLocations(value, node); @@ -327,3 +336,32 @@ function languageWithLocation(name, value, offset) { optional: false, }; } + +function findMiniDisableRanges(comments, codeEnd) { + const ranges = []; + const stack = []; // used to track on/off pairs + for (const comment of comments) { + const value = comment.value.trim(); + if (value.startsWith('mini-off')) { + stack.push(comment.start); + } else if (value.startsWith('mini-on')) { + const start = stack.pop(); + ranges.push([start, comment.end]); + } + } + while (stack.length) { + // If no closing mini-on is found, just turn it off until `codeEnd` + const start = stack.pop(); + ranges.push([start, codeEnd]); + } + return ranges; +} + +function isMiniDisabled(offset, miniDisableRanges) { + for (const [start, end] of miniDisableRanges) { + if (offset >= start && offset < end) { + return true; + } + } + return false; +} From d84db1e663b2d89867700b60788f3ca54ffc4081 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 28 Nov 2025 10:02:57 +0100 Subject: [PATCH 076/194] hotfix: auto deploy to warm.strudel.cc when main changes --- .forgejo/workflows/deploy-warm-beta.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.forgejo/workflows/deploy-warm-beta.yml b/.forgejo/workflows/deploy-warm-beta.yml index 2de6670c..e6636a72 100644 --- a/.forgejo/workflows/deploy-warm-beta.yml +++ b/.forgejo/workflows/deploy-warm-beta.yml @@ -1,6 +1,9 @@ name: Build and Deploy to beta (warm.strudel.cc) -on: [workflow_dispatch] +on: + push: + branches: + - main # Allow one concurrent deployment concurrency: From fc74dcdb1a56e7f2122e87210be53e6a60d1ea74 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 24 Oct 2025 19:21:34 +0300 Subject: [PATCH 077/194] More bug fixes --- packages/webaudio/webaudio.mjs | 43 +++++++++++------------------ website/src/repl/useReplContext.jsx | 10 ++++++- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index c9b5cbda..7c8ff648 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -14,12 +14,8 @@ import { setAudioContext, initAudio, setSuperdoughAudioController, - setMaxPolyphony, - setMultiChannelOrbits, resetGlobalEffects, - errorLogger, - maxPolyphony as superdoughMaxPolyphony, - multiChannelOrbits as superdoughMultiChannelOrbits + errorLogger } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; @@ -51,10 +47,6 @@ export async function renderPatternAudio( multiChannelOrbits, downloadName = undefined, ) { - let realtimeOptions = { - maxPolyphony: superdoughMaxPolyphony, - multiChannelOrbits: superdoughMultiChannelOrbits - } let audioContext = getAudioContext(); await audioContext.close(); audioContext = new OfflineAudioContext(2, ((end - begin) / cps) * sampleRate, sampleRate); @@ -63,28 +55,26 @@ export async function renderPatternAudio( await initAudio({ maxPolyphony, multiChannelOrbits - }); + }) logger('[webaudio] preloading'); let haps = pattern.queryArc(begin, end, { _cps: cps }); - await Promise.all( - haps.map(async (hap) => { - if (hap.hasOnset()) { - try { - await superdough( - hap2value(hap), - hap.whole.begin.valueOf() / cps, - hap.duration / cps, - cps, - hap.whole?.begin.valueOf(), - ); - } catch (err) { - errorLogger(err, 'webaudio'); - } + for (const hap of haps) { + if (hap.hasOnset()) { + try { + await superdough( + hap2value(hap), + (hap.whole.begin.valueOf() - begin) / cps, + hap.duration / cps, + cps, + (hap.whole?.begin.valueOf() - begin) / cps, + ); + } catch (err) { + errorLogger(err, 'webaudio'); } - }), - ); + } + } logger('[webaudio] start rendering'); return audioContext @@ -106,7 +96,6 @@ export async function renderPatternAudio( setAudioContext(null); setSuperdoughAudioController(null); resetGlobalEffects(); - await initAudio(realtimeOptions); }); } diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 15b3fab1..361abc1d 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -15,6 +15,7 @@ import { resetLoadedSounds, initAudioOnFirstClick, resetDefaults, + initAudio, } from '@strudel/webaudio'; import { setVersionDefaultsFrom } from './util.mjs'; import { StrudelMirror, defaultSettings } from '@strudel/codemirror'; @@ -217,7 +218,14 @@ export function useReplContext() { maxPolyphony, multiChannelOrbits, downloadName, - ).finally(() => { + ).finally(async () => { + const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get() + await initAudio({ + latestCode, + maxPolyphony, + audioDeviceName, + multiChannelOrbits + }) editorRef.current.repl.scheduler.stop(); }); }; From 91ebdd5ea62ddeb8d9a346f73d5c3dd2a06aa98b Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 24 Oct 2025 20:55:39 +0300 Subject: [PATCH 078/194] Better progress bar UI --- website/src/repl/components/ExportModal.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx index 4bc8990e..ad1fc582 100644 --- a/website/src/repl/components/ExportModal.jsx +++ b/website/src/repl/components/ExportModal.jsx @@ -199,7 +199,7 @@ export default function ExportModal(Props) { disabled={exporting} onClick={async () => { setExporting(true); - setTimeout(refreshProgress, 1000); + setTimeout(refreshProgress, 2000); await handleExport(startCycle, endCycle, sampleRate, maxPolyphony, multiChannelOrbits, downloadName) .then(() => { const modal = document.getElementById('exportModal'); @@ -212,13 +212,13 @@ export default function ExportModal(Props) { }); }} > - {exporting ? 'Exporting...' : 'Export to WAV'}
+ {exporting ? 'Exporting...' : 'Export to WAV'}
From 8f9967c76dae6d053f8157f2e091d697c385a19c Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 24 Oct 2025 20:55:56 +0300 Subject: [PATCH 079/194] Format --- packages/webaudio/webaudio.mjs | 6 +++--- website/src/repl/components/ExportModal.jsx | 6 ++---- website/src/repl/useReplContext.jsx | 6 +++--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 7c8ff648..9da5a411 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -15,7 +15,7 @@ import { initAudio, setSuperdoughAudioController, resetGlobalEffects, - errorLogger + errorLogger, } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; @@ -54,8 +54,8 @@ export async function renderPatternAudio( setSuperdoughAudioController(new SuperdoughAudioController(audioContext)); await initAudio({ maxPolyphony, - multiChannelOrbits - }) + multiChannelOrbits, + }); logger('[webaudio] preloading'); let haps = pattern.queryArc(begin, end, { _cps: cps }); diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx index ad1fc582..bac8c6b1 100644 --- a/website/src/repl/components/ExportModal.jsx +++ b/website/src/repl/components/ExportModal.jsx @@ -3,9 +3,7 @@ import cx from '@src/cx.mjs'; import NumberInput from './NumberInput'; import { useEffect, useState } from 'react'; import { Textbox } from './textbox/Textbox'; -import { - getAudioContext, -} from '@strudel/webaudio'; +import { getAudioContext } from '@strudel/webaudio'; import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon'; function Checkbox({ label, value, onChange, disabled = false }) { @@ -215,7 +213,7 @@ export default function ExportModal(Props) {
{exporting ? 'Exporting...' : 'Export to WAV'} diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 361abc1d..59d86a4d 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -219,13 +219,13 @@ export function useReplContext() { multiChannelOrbits, downloadName, ).finally(async () => { - const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get() + const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get(); await initAudio({ latestCode, maxPolyphony, audioDeviceName, - multiChannelOrbits - }) + multiChannelOrbits, + }); editorRef.current.repl.scheduler.stop(); }); }; From 7d890156f4c2a4a584c975e57fc8fa3e44d897df Mon Sep 17 00:00:00 2001 From: Nikita Date: Sat, 25 Oct 2025 18:53:24 +0300 Subject: [PATCH 080/194] Fix haps not being sorted by time --- packages/webaudio/webaudio.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 9da5a411..8f18d80d 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -58,7 +58,9 @@ export async function renderPatternAudio( }); logger('[webaudio] preloading'); - let haps = pattern.queryArc(begin, end, { _cps: cps }); + let haps = pattern + .queryArc(begin, end, { _cps: cps }) + .sort((a, b) => a.whole.begin.valueOf() - b.whole.begin.valueOf()); for (const hap of haps) { if (hap.hasOnset()) { From c5250d7794a2600d0aeca36885adf0e93e1d7be7 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sat, 25 Oct 2025 18:59:12 +0300 Subject: [PATCH 081/194] Add info comment about ascending time order --- packages/webaudio/webaudio.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 8f18d80d..36ee9d64 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -58,10 +58,11 @@ export async function renderPatternAudio( }); logger('[webaudio] preloading'); + // Calling superdough(...) in ascending onset time order is important + // for controls that depend on the audio graph state like `cut` let haps = pattern .queryArc(begin, end, { _cps: cps }) .sort((a, b) => a.whole.begin.valueOf() - b.whole.begin.valueOf()); - for (const hap of haps) { if (hap.hasOnset()) { try { From 5bddb6f48d969427d977ad807f3e7c3f0f482703 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 30 Nov 2025 10:09:06 +0300 Subject: [PATCH 082/194] Move export UI to the panel --- website/src/repl/components/ExportModal.jsx | 225 ------------------ website/src/repl/components/Header.jsx | 15 +- .../src/repl/components/panel/ExportTab.jsx | 197 +++++++++++++++ website/src/repl/components/panel/Panel.jsx | 4 + 4 files changed, 203 insertions(+), 238 deletions(-) delete mode 100644 website/src/repl/components/ExportModal.jsx create mode 100644 website/src/repl/components/panel/ExportTab.jsx diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx deleted file mode 100644 index bac8c6b1..00000000 --- a/website/src/repl/components/ExportModal.jsx +++ /dev/null @@ -1,225 +0,0 @@ -import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; -import cx from '@src/cx.mjs'; -import NumberInput from './NumberInput'; -import { useEffect, useState } from 'react'; -import { Textbox } from './textbox/Textbox'; -import { getAudioContext } from '@strudel/webaudio'; -import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon'; - -function Checkbox({ label, value, onChange, disabled = false }) { - return ( - - ); -} - -function FormItem({ label, children, disabled }) { - return ( -
- - {children} -
- ); -} - -export default function ExportModal(Props) { - const { started, isEmbedded, handleExport } = Props; - - const [downloadName, setDownloadName] = useState(''); // TODO: make a form? - const [startCycle, setStartCycle] = useState(0); - const [endCycle, setEndCycle] = useState(1); - const [sampleRate, setSampleRate] = useState(48000); - const [multiChannelOrbits, setMultiChannelOrbits] = useState(true); - const [maxPolyphony, setMaxPolyphony] = useState(1024); - const [exporting, setExporting] = useState(false); - const [progress, setProgress] = useState(0); - const [length, setLength] = useState(1); - - const refreshProgress = () => { - const audioContext = getAudioContext(); - if (audioContext instanceof OfflineAudioContext) { - setProgress(audioContext.currentTime); - setLength(audioContext.length / sampleRate); - setTimeout(refreshProgress, 100); - } - }; - - return ( - <> - - - -
- - { - setDownloadName(e.target.value); - }} - onChange={(v) => { - setDownloadName(v); - }} - disabled={exporting} - placeholder="Leave empty to use current date" - className={cx('placeholder:opacity-50', exporting && 'opacity-50 border-opacity-50')} - value={downloadName ?? ''} - /> - -
- - { - let v = parseInt(e.target.value); - v = isNaN(v) ? 0 : Math.max(0, v); - setStartCycle(v); - }} - onChange={(v) => { - v = parseInt(v); - setStartCycle(v); - }} - type="number" - placeholder="" - disabled={exporting} - className={cx(exporting && 'opacity-50 border-opacity-50')} - value={startCycle ?? ''} - /> - - - { - let v = parseInt(e.target.value); - v = isNaN(v) ? Math.max(startCycle + 1, parseInt(v)) : v; - setEndCycle(v); - }} - onChange={(v) => { - v = parseInt(v); - setEndCycle(v); - }} - type="number" - placeholder="" - disabled={exporting} - className={cx(exporting && 'opacity-50 border-opacity-50')} - value={endCycle ?? ''} - /> - -
-
- - { - let v = parseInt(e.target.value); - v = isNaN(v) ? 1 : Math.max(1, v); - setSampleRate(v); - }} - onChange={(v) => { - v = parseInt(v); - setSampleRate(v); - }} - type="number" - placeholder="" - disabled={exporting} - className={cx(exporting && 'opacity-50 border-opacity-50')} - value={sampleRate ?? ''} - /> - - - { - let v = parseInt(e.target.value); - v = isNaN(v) ? Math.max(1, parseInt(v)) : v; - setMaxPolyphony(v); - }} - onChange={(v) => { - v = Math.max(1, parseInt(v)); - setMaxPolyphony(v); - }} - type="number" - placeholder="" - disabled={exporting} - className={cx(exporting && 'opacity-50 border-opacity-50')} - value={maxPolyphony ?? ''} - /> - -
-
- { - const val = cbEvent.target.checked; - setMultiChannelOrbits(val); - }} - disabled={exporting} - value={multiChannelOrbits} - /> -
- -
-
- - ); -} diff --git a/website/src/repl/components/Header.jsx b/website/src/repl/components/Header.jsx index dd9f684c..ca4e1ecf 100644 --- a/website/src/repl/components/Header.jsx +++ b/website/src/repl/components/Header.jsx @@ -3,23 +3,13 @@ import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon'; import cx from '@src/cx.mjs'; import { useSettings, setIsZen } from '../../settings.mjs'; import '../Repl.css'; -import ExportModal from './ExportModal'; const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; export function Header({ context, embedded = false }) { - const { - started, - pending, - isDirty, - activeCode, - handleTogglePlay, - handleEvaluate, - handleShuffle, - handleShare, - handleExport, - } = context; + const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare } = + context; const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location); const { isZen, isButtonRowHidden, isCSSAnimationDisabled, fontFamily } = useSettings(); @@ -103,7 +93,6 @@ export function Header({ context, embedded = false }) { > {!isEmbedded && update} - {/* !isEmbedded && ( +
+ + + ); +} diff --git a/website/src/repl/components/panel/Panel.jsx b/website/src/repl/components/panel/Panel.jsx index b26c2ede..cfe90ae3 100644 --- a/website/src/repl/components/panel/Panel.jsx +++ b/website/src/repl/components/panel/Panel.jsx @@ -9,6 +9,7 @@ import { useLogger } from '../useLogger'; import { WelcomeTab } from './WelcomeTab'; import { PatternsTab } from './PatternsTab'; import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid'; +import ExportTab from './ExportTab'; const TAURI = typeof window !== 'undefined' && window.__TAURI__; @@ -80,6 +81,7 @@ const tabNames = { patterns: 'patterns', sounds: 'sounds', reference: 'reference', + export: 'export', console: 'console', settings: 'settings', }; @@ -126,6 +128,8 @@ function PanelContent({ context, tab }) { return ; case tabNames.reference: return ; + case tabNames.export: + return ; case tabNames.settings: return ; case tabNames.files: From 24b69ad1bcc6861268f34f4cf01685e8f11bfeca Mon Sep 17 00:00:00 2001 From: jeromew Date: Sun, 30 Nov 2025 14:09:03 +0000 Subject: [PATCH 083/194] [perf] Add audiograph `await debugAudiograph();` tool --- website/src/repl/audiograph.mjs | 618 ++++++++++++++++++++++++++++ website/src/repl/useReplContext.jsx | 2 + 2 files changed, 620 insertions(+) create mode 100644 website/src/repl/audiograph.mjs diff --git a/website/src/repl/audiograph.mjs b/website/src/repl/audiograph.mjs new file mode 100644 index 00000000..d203496a --- /dev/null +++ b/website/src/repl/audiograph.mjs @@ -0,0 +1,618 @@ +/* +audiograph.mjs - show a svg view of the web audio API graph built during a playback +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 . +*/ + +// main entry point is `debugAudiograph` + +import { logger } from '@strudel/core'; +import { getAudioContext, getSuperdoughAudioController, webaudioOutput } from '@strudel/webaudio'; + +let mermaid = null; +let svgPanZoom = null; + +let running = false; +let hap_count = 0; + +let cache = new Map(); +const initCache = JSON.stringify({ + connect: [], + where: [], + disconnectAll: 0, + disconnectOne: 0, + stopCount: 0, + ac: null, + creation: null, +}); + +let toggleOrig; + +function stackTrace() { + var err = new Error(); + const stacktrace = err.stack; + const lines = stacktrace.split('\n'); + let lineIndex = lines.findIndex((line) => line !== 'Error' && !line.includes('audiograph.mjs')); + if (lines[lineIndex].includes('gainNode')) lineIndex++; + if (lines[lineIndex].includes('getWorklet')) lineIndex++; + const line = lines[lineIndex].replace(/\s*at\s/, '').replace('http', '@'); + let match; + match = line.match(/([^@]*)@.*packages(\/[^:]+:\d+:\d+)/); + if (match) { + return match[1].replace(/[^.:/a-zA-Z0-9]/g, '') + '@' + match[2].replace(/[^.:/a-zA-Z0-9]/g, ''); + } + return '@'; +} + +// This captures all AudioNodes lazily +// when an `.audioid` property is called +// no solution was found to hook +// AudioNode's constructor directly +let audioid = 0; +const lazyRegister = (o) => { + Object.defineProperty(o.prototype, 'audioid', { + get: function () { + if (!this._audioid) { + this._audioid = ++audioid; + const s = JSON.parse(initCache); + s.type = this.constructor.name === 'AudiographNode' ? this.constructor._parentClassName : this.constructor.name; + s.ac = this.context?.constructor.name || 'AudioParam'; + s.creation = s.creation || stackTrace(); + cache.set(this._audioid, s); + } + return this._audioid; + }, + enumerable: false, + configurable: true, + }); +}; + +// extend a specific AudioNode's constructor +// necessary when creation is done direclty by +// calling the constructor +// eg: new GainNode(...) +const audioNodeHook = (node) => { + const name = node.prototype.constructor.name; + const PatchedNode = class AudiographNode extends node { + constructor(...args) { + super(...args); + // trigger the lazy register + this._audioid = this.audioid; + } + }; + PatchedNode._parentClassName = name; + window[name] = PatchedNode; +}; + +const drawMessage = async function (message) { + const element = document.querySelector('.strudel-mermaid'); + let gd = ''; + gd += '---\n'; + gd += 'config:\n'; + gd += ' flowchart:\n'; + gd += ' wrappingWidth: 600\n'; + gd += '---\n'; + gd += 'flowchart LR\n'; + gd += 'id[' + message.replaceAll(' ', ' ') + ']\n'; + + let { svg } = await mermaid.render('strudelSvgId', gd); + svg = svg.replace(/max-width:\s[0-9.]*px;/i, 'height: 100%'); + svg = svg.replaceAll('&nbsp;', ' '); + element.innerHTML = svg; +}; + +const drawDiagram = async function () { + const element = document.querySelector('.strudel-mermaid'); + + let code = window.strudelMirror.code; + code = code.replace(/^await debugAudiograph.*\n?/gm, ''); + code = '// date: ' + new Date().toISOString() + '\n\n' + code; + code = '// host: ' + document.location.hostname + '\n' + code; + const codeLines = code.split(/(?:\n|\r\n?)/); + const maxLineLength = codeLines.reduce((memo, line) => Math.max(memo, line.length), 0); + + // https://mermaid.js.org/syntax/flowchart.html + let gd = ''; + gd += '---\n'; + gd += 'config:\n'; + gd += ' flowchart:\n'; + gd += ' wrappingWidth: ' + 14 * maxLineLength + '\n'; + gd += '---\n'; + gd += 'flowchart TB\n'; + gd += '\tsubgraph AG[STRUDEL AUDIOGRAPH]\n'; + + // seed graph builder with all + // unconnected nodes + let lookup = []; + cache.forEach((v, k) => { + if (v.connect.length === 0) lookup.push(k); + }); + const relations = []; + let curRelations; + const zombieCount = 0; + const sourceLoc = (stack) => { + if (stack === '@') return stack; + return stack.replace('@', '\n').replace('/superdough/', '/'); + }; + const label = (s) => { + const source = s.creation ? '\n' + sourceLoc(s.creation) : ''; + let lb = '[' + '**' + s.type + '**' + source + ']'; + if (s.ac === 'OfflineAudioContext') lb = '[' + lb + ']'; + return lb; + }; + const isLeak = (s) => { + return ( + ['AudioDestinationNode', 'AudioParam'].indexOf(s.type) === -1 && + s.disconnectAll === 0 && + s.connect.length > s.disconnectOne + ); + }; + do { + curRelations = relations.length; + lookup.slice().forEach((n) => { + cache.forEach((v, k) => { + if (v.connect.indexOf(n) !== -1) { + if (lookup.indexOf(k) === -1) lookup.push(k); + gd += v.connect + .map((i) => { + if (lookup.indexOf(i) === -1) lookup.push(i); + if (relations.indexOf(k + '-' + i) === -1) { + relations.push(k + '-' + i); + return ( + '\t\tnode' + + k + + label(v) + + ' -- ' + + sourceLoc(v.where[0]) + + ' --> node' + + i + + label(cache.get(i)) + + '\n' + ); + } + }) + .join(''); + } + if (k === n) { + gd += v.connect + .map((i) => { + if (lookup.indexOf(i) === -1) lookup.push(i); + if (relations.indexOf(k + '-' + i) === -1) { + relations.push(k + '-' + i); + return ( + '\t\tnode' + + k + + label(v) + + ' -- ' + + sourceLoc(v.where[0]) + + ' --> node' + + i + + label(cache.get(i)) + + '\n' + ); + } + }) + .join(''); + } + }); + }); + } while (relations.length > curRelations /*&& lookup.length < 100*/); + + const codePlaceholder = 'm'.repeat(maxLineLength); + gd += '\tsubgraph LEGEND\n'; + gd += '\t\tlegend1[in AudioContext]\n'; + gd += '\t\tlegend2[[in OfflineAudioContext]]\n'; + gd += '\t\tlegend3[not disconnected]\n'; + gd += '\t\tlegend4[AudioParam]\n'; + gd += '\t\tlegend5[AudioDestinationNode]\n'; + gd += '\tend\n'; + gd += '\tsubgraph CODE[Strudel Code]\n'; + // we use a codePlaceholder to + // - avoid problems with special chars + // - stop mermaid to split lines on space with multiple tspans + // - force mermaid to prepare a sufficiently sized zone + gd += '\ncode[' + (codePlaceholder + '
').repeat(codeLines.length) + ']\n'; + gd += '\tend\n'; + gd += '\tend\n'; + gd += '\tclassDef audioparam fill:#6f6;\n'; + gd += '\tclassDef destination fill:#99f;\n'; + gd += '\tclassDef suspect fill:#f96,stroke:#f00,stroke-width:2px;\n'; + gd += '\tclass legend3 suspect;\n'; + gd += '\tclass legend4 audioparam;\n'; + gd += '\tclass legend5 destination;\n'; + cache.forEach((v, k) => { + if (isLeak(v)) { + gd += '\tclass node' + k + ' suspect;\n'; + } + if (v.type === 'AudioParam') { + gd += '\tclass node' + k + ' audioparam;\n'; + } + if (v.type === 'AudioDestinationNode') { + gd += '\tclass node' + k + ' destination;\n'; + } + }); + let { svg } = await mermaid.render('strudelSvgId', gd); + + // put real code in code zone + let idx = 0; + const escapeHtml = (unsafe) => { + return unsafe + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + }; + svg = svg.replaceAll(codePlaceholder, () => escapeHtml(codeLines[idx++])); + + // improve sizing on web page + svg = svg.replace(/max-width:\s[0-9.]*px;/i, 'height: 100%'); + element.innerHTML = svg; + + // align the code lines + let svgText = document.querySelector('[id^=flowchart-code] text'); + svgText.setAttributeNS(null, 'style', 'text-anchor: start;'); + const svgElement = document.querySelector('svg'); + const svgLabel = document.querySelector('svg [id^=flowchart-code] .label'); + const transformList = svgLabel.transform.baseVal; + const svgTransform = svgElement.createSVGTransform(); + const tspans = Array.from(document.querySelectorAll('[id^=flowchart-code] tspan.text-inner-tspan')); + let tspansMaxLength = tspans.reduce((memo, tspan) => Math.max(memo, tspan.getComputedTextLength()), 0); + svgTransform.setTranslate(-tspansMaxLength / 2, 0); + transformList.appendItem(svgTransform); + + let doPan = false; + let eventsHandler; + let panZoom; + let mousepos; + + eventsHandler = { + haltEventListeners: ['mousedown', 'mousemove', 'mouseup'], + mouseDownHandler: function (ev) { + if (event.target.className == '[object SVGAnimatedString]') { + doPan = true; + mousepos = { + x: ev.clientX, + y: ev.clientY, + }; + } + }, + mouseMoveHandler: function (ev) { + if (doPan) { + panZoom.panBy({ + x: ev.clientX - mousepos.x, + y: ev.clientY - mousepos.y, + }); + mousepos = { + x: ev.clientX, + y: ev.clientY, + }; + window.getSelection().removeAllRanges(); + } + }, + mouseUpHandler: function (ev) { + doPan = false; + }, + init: function (options) { + options.svgElement.addEventListener('mousedown', this.mouseDownHandler, false); + options.svgElement.addEventListener('mousemove', this.mouseMoveHandler, false); + options.svgElement.addEventListener('mouseup', this.mouseUpHandler, false); + }, + destroy: function (options) { + options.svgElement.removeEventListener('mousedown', this.mouseDownHandler, false); + options.svgElement.removeEventListener('mousemove', this.mouseMoveHandler, false); + options.svgElement.removeEventListener('mouseup', this.mouseUpHandler, false); + }, + }; + panZoom = svgPanZoom('#strudelSvgId', { + zoomEnabled: true, + controlIconsEnabled: true, + fit: 1, + center: 1, + zoomScaleSensitivity: 0.4, + customEventsHandler: eventsHandler, + }); +}; + +const svgExport = async () => { + const a = document.createElement('a'); + document.body.appendChild(a); + a.style = 'display: none'; + + const selector = '.strudel-mermaid'; + const bbox = document.querySelector('svg g').getBBox(); + let transform, style; + // clean pan-zoom viewport + const pzViewport = document.querySelector('.svg-pan-zoom_viewport'); + if (pzViewport) { + transform = pzViewport.transform; + style = pzViewport.style; + pzViewport.setAttribute('transform', ''); + pzViewport.style = ''; + } + + const spzMin = await fetch('https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.2/dist/svg-pan-zoom.min.js').then((res) => + res.text(), + ); + + const scriptContent = ''; + // prepare svg + const content = document + .querySelector(selector) + .innerHTML.replaceAll('
', '
') + // remove useless tags + .replace(/ { + if (k <= audioid) cache.delete(k); + }); +}; + +const postProcessing = async function () { + hap_count = 0; + await drawDiagram(); + + resetAudioOutput(audioid); +}; + +const defaultOptions = { + StopAfterHapCount: 10, + hapsBatch: 0, + maxEdges: 10000, + maxTextSize: 200000, + audioAPIBreathingRoomSec: 5, +}; + +// `StopAfterHapCount` : +// The player will auto-stop after hap count have +// been played. when StopAfterHapCount = 0, it will +// continue playing until 'stop' is clicked +// `audioAPIBreathingRoomSec` : +// how much time should we wait after 'stop' to let +// the audioAPI finish its tail of ondended calls +// `hapsBatch` : +// the AudioGraph will be displayed every hapsBatch haps +// when hapsBatch = 0, AudioGraph will only be displayed +// after and auto-stop or after 'stop' is clicked +// In hapsBatch mode you will probably see a trailing of +// non disconnected notes on the graph because the audio +// API may have some lag disconnecting them +// cf also audioAPIBreathingRoomSec +// `maxEdges` +// This is a mermaid.js config that forces a hard limit +// on the maximum number of Edges of a graph +// needs a reload to be taken into account +// `maxTextSize` +// This is a mermaid.js config that forces a hard limit +// on the maximum text size of a graph definition +// needs a reload to be taken into account + +export const debugAudiograph = async (argOptions = {}) => { + const options = Object.assign({}, defaultOptions, argOptions); + const { StopAfterHapCount, hapsBatch, maxEdges, maxTextSize, audioAPIBreathingRoomSec } = options; + const sm = window.strudelMirror; + const code = sm.code; + if (!code.match(/await\s+debugAudiograph/)) { + throw new Error('you need to call `await debugAudiograph()` for audiograph to work'); + } + const emptyOptions = /await\s+debugAudiograph\(\)/.exec(code); + if (emptyOptions) { + const cutCode = emptyOptions.index + emptyOptions[0].length - 1; + const codeOptions = JSON.stringify({ StopAfterHapCount: StopAfterHapCount }).replaceAll('"', ''); + sm.setCode(code.slice(0, cutCode) + codeOptions + code.slice(cutCode)); + } + + if (window.audiograph === undefined) { + const ag = (window.audiograph = {}); + + toggleOrig = sm.toggle; + + //////////////////////////////////////// + // step 1: web audio api instrumentation + //////////////////////////////////////// + + // path AudioNode & AudioParam + // to give them lazy ids + // this captures both `ac.createGain` + // and `new GainNode(..)` patterns + lazyRegister(AudioNode); + lazyRegister(AudioParam); + + const audioNodes = [ + AudioBufferSourceNode, + AudioWorkletNode, + AnalyserNode, + BiquadFilterNode, + ChannelMergerNode, + ChannelSplitterNode, + ConstantSourceNode, + ConvolverNode, + DelayNode, + DynamicsCompressorNode, + GainNode, + IIRFilterNode, + OscillatorNode, + PannerNode, + StereoPannerNode, + WaveShaperNode, + ]; + audioNodes.map((n) => audioNodeHook(n)); + + // patch BaseAudioContext factory methods + // to capture the source reference + Object.getOwnPropertyNames(BaseAudioContext.prototype) + .filter((n) => n.startsWith('create') && ['createBuffer'].indexOf(n) === -1) + .map((name) => { + const orig = BaseAudioContext.prototype[name]; + BaseAudioContext.prototype[name] = function (...args) { + const result = orig.call(this, ...args); + const s = cache.get(result.audioid); + s.creation = stackTrace(); + return result; + }; + }); + + const connectOrig = AudioNode.prototype.connect; + AudioNode.prototype.connect = function (destination, ...args) { + const result = connectOrig.call(this, destination, ...args); + const s = cache.get(this.audioid); + s.connect.push(destination.audioid); + s.where.push(stackTrace()); + return result; + }; + + const disconnectOrig = AudioNode.prototype.disconnect; + AudioNode.prototype.disconnect = function (destination, ...args) { + const result = disconnectOrig.call(this, destination, ...args); + const s = cache.get(this.audioid); + if (s.connect.length) { + if (destination) { + s.disconnectOne++; + } else { + s.disconnectAll++; + } + } else { + logger('WEIRD: node ' + this.audioid + 'called disconnect before any call to connect !'); + logger(new Error().stack); + console.log(cache); + } + return result; + }; + + // call reset 2 times to handle reload + 'play' + // the first reset's disconnect adds audioid tags on previous outputs + // that were not tagged (wrong cutoff) + resetAudioOutput(audioid); + // the second reset has the correct audioid cutoff + resetAudioOutput(audioid); + + //////////////////////////////////////// + // step 2: Load external modules + //////////////////////////////////////// + + const { default: mermaidModule } = await import( + 'https://cdn.jsdelivr.net/npm/mermaid@11.12.1/dist/mermaid.esm.mjs' + ); + mermaid = mermaidModule; + + mermaid.initialize({ + startOnLoad: false, + themeCSS: '.flowchart { height: 100%; }', + maxEdges: maxEdges, + maxTextSize: maxTextSize, + htmlLabels: false, + flowchart: { + htmlLabels: false, + }, + }); + + const { default: svgPanZoomModule } = await import('https://esm.sh/svg-pan-zoom'); + svgPanZoom = svgPanZoomModule; + + ////////////////////////////////////////// + // step 3: UI modifications + ////////////////////////////////////////// + + // add audiograph panel + if (!document.querySelector('.strudel-mermaid')) { + const mermaidDiv = document.createElement('div'); + mermaidDiv.className = 'strudel-mermaid'; + mermaidDiv.style = 'min-height: 600px; width: 60%'; + const referenceNode = document.querySelector('#code'); + referenceNode.parentNode.insertBefore(mermaidDiv, referenceNode.nextSibling); + } + + // add svg export button + if (!document.querySelector('button[title=svg]')) { + const exportButton = document.createElement('button'); + exportButton.innerHTML = 'ExportDiagram'; + exportButton.title = 'svg'; + exportButton.onclick = svgExport; + const updateButton = document.querySelector('button[title=update]'); + updateButton.parentNode.insertBefore(exportButton, updateButton); + } + } + + if (!running) { + running = true; + } + + if (hapsBatch === 0 || hap_count < hapsBatch) { + let msg = ''; + msg += 'Recording activity...'; + msg += '\npress stop to build diagram'; + if (StopAfterHapCount) { + msg += '\nwill stop automatically in ' + Math.max(StopAfterHapCount - hap_count, 0) + ' haps'; + } + await drawMessage(msg); + } + + sm.toggle = async () => { + running = false; + sm.toggle = toggleOrig; + // schedule `toggle` on the js main loop + // to avoid interfering with any on-flight onTick + // not doing this can lead to a phase > 0 which will + // break the next start + setTimeout(sm.toggle.bind(sm), 0); + await drawMessage('please wait ' + audioAPIBreathingRoomSec + ' seconds\n' + 'the audio API is finishing its work'); + setTimeout(postProcessing, audioAPIBreathingRoomSec * 1000); + }; + + /*global all*/ + all((pat) => + pat.onTrigger(async (hap, duration, cps, t) => { + hap_count++; + const key = Object.entries(hap.value) + .map((param) => param.join('/')) + .join('/'); + + // if we reached StopAfterHapCount, click 'stop' + if (StopAfterHapCount && hap_count > StopAfterHapCount) { + if (running) { + await sm.toggle(); + } + // stop sending haps to superdough(...) + return; + } + + await webaudioOutput(hap, t, hap.duration / cps, cps, t); + + if (hapsBatch && hap_count % hapsBatch === 0) drawDiagram(); + }), + ); +}; diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 8dcb6b75..7ef86c08 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -36,6 +36,7 @@ import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs'; import './Repl.css'; import { setInterval, clearInterval } from 'worker-timers'; import { getMetadata } from '../metadata_parser'; +import { debugAudiograph } from './audiograph'; const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get(); let modulesLoading, presets, drawContext, clearCanvas, audioReady; @@ -129,6 +130,7 @@ export function useReplContext() { bgFill: false, }); window.strudelMirror = editor; + window.debugAudiograph = debugAudiograph; // init settings initCode().then(async (decoded) => { From 05ed16e15860c5ac704a9c14b88048998942c3e9 Mon Sep 17 00:00:00 2001 From: jeromew Date: Sun, 30 Nov 2025 17:50:56 +0000 Subject: [PATCH 084/194] Add `stop-leak` detection. Stoppable nodes that are not stopped --- website/src/repl/audiograph.mjs | 42 +++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/website/src/repl/audiograph.mjs b/website/src/repl/audiograph.mjs index d203496a..52ac311a 100644 --- a/website/src/repl/audiograph.mjs +++ b/website/src/repl/audiograph.mjs @@ -21,6 +21,7 @@ const initCache = JSON.stringify({ where: [], disconnectAll: 0, disconnectOne: 0, + hasStop: false, stopCount: 0, ac: null, creation: null, @@ -56,6 +57,7 @@ const lazyRegister = (o) => { this._audioid = ++audioid; const s = JSON.parse(initCache); s.type = this.constructor.name === 'AudiographNode' ? this.constructor._parentClassName : this.constructor.name; + s.hasStop = window[s.type].prototype instanceof AudioScheduledSourceNode; s.ac = this.context?.constructor.name || 'AudioParam'; s.creation = s.creation || stackTrace(); cache.set(this._audioid, s); @@ -103,7 +105,6 @@ const drawMessage = async function (message) { const drawDiagram = async function () { const element = document.querySelector('.strudel-mermaid'); - let code = window.strudelMirror.code; code = code.replace(/^await debugAudiograph.*\n?/gm, ''); code = '// date: ' + new Date().toISOString() + '\n\n' + code; @@ -140,13 +141,16 @@ const drawDiagram = async function () { if (s.ac === 'OfflineAudioContext') lb = '[' + lb + ']'; return lb; }; - const isLeak = (s) => { + const isConnectLeak = (s) => { return ( ['AudioDestinationNode', 'AudioParam'].indexOf(s.type) === -1 && s.disconnectAll === 0 && s.connect.length > s.disconnectOne ); }; + const isStopLeak = (s) => { + return s.hasStop && s.stopCount === 0; + }; do { curRelations = relations.length; lookup.slice().forEach((n) => { @@ -197,6 +201,13 @@ const drawDiagram = async function () { }); }); } while (relations.length > curRelations /*&& lookup.length < 100*/); + // add orphan nodes + const inRelation = '-' + relations.join('-') + '-'; + cache.forEach((v, k) => { + if (!inRelation.includes('-' + k + '-')) { + gd += '\t\tnode' + k + label(v) + '\n'; + } + }); const codePlaceholder = 'm'.repeat(maxLineLength); gd += '\tsubgraph LEGEND\n'; @@ -205,6 +216,7 @@ const drawDiagram = async function () { gd += '\t\tlegend3[not disconnected]\n'; gd += '\t\tlegend4[AudioParam]\n'; gd += '\t\tlegend5[AudioDestinationNode]\n'; + gd += '\t\tlegend6[not stopped]\n'; gd += '\tend\n'; gd += '\tsubgraph CODE[Strudel Code]\n'; // we use a codePlaceholder to @@ -216,13 +228,17 @@ const drawDiagram = async function () { gd += '\tend\n'; gd += '\tclassDef audioparam fill:#6f6;\n'; gd += '\tclassDef destination fill:#99f;\n'; - gd += '\tclassDef suspect fill:#f96,stroke:#f00,stroke-width:2px;\n'; - gd += '\tclass legend3 suspect;\n'; + gd += '\tclassDef connectleak fill:#f96,stroke:#f00,stroke-width:2px;\n'; + gd += '\tclassDef stopleak fill:#f55,stroke:#f00,stroke-width:2px;\n'; + gd += '\tclass legend3 connectleak;\n'; gd += '\tclass legend4 audioparam;\n'; gd += '\tclass legend5 destination;\n'; + gd += '\tclass legend6 stopleak;\n'; cache.forEach((v, k) => { - if (isLeak(v)) { - gd += '\tclass node' + k + ' suspect;\n'; + if (isConnectLeak(v)) { + gd += '\tclass node' + k + ' connectleak;\n'; + } else if (isStopLeak(v)) { + gd += '\tclass node' + k + ' stopleak;\n'; } if (v.type === 'AudioParam') { gd += '\tclass node' + k + ' audioparam;\n'; @@ -470,7 +486,19 @@ export const debugAudiograph = async (argOptions = {}) => { StereoPannerNode, WaveShaperNode, ]; - audioNodes.map((n) => audioNodeHook(n)); + audioNodes.map((n) => { + if (n.prototype instanceof AudioScheduledSourceNode) { + const stopOrig = n.prototype.stop; + n.prototype.stop = function (...args) { + // stop called + const result = stopOrig.call(this, ...args); + const s = cache.get(this.audioid); + s.stopCount++; + return result; + }; + } + audioNodeHook(n); + }); // patch BaseAudioContext factory methods // to capture the source reference From 39bff8900f695b80e4c5a666a62a33a186e58ff0 Mon Sep 17 00:00:00 2001 From: scrappy_fiddler Date: Mon, 1 Dec 2025 18:22:26 +0100 Subject: [PATCH 085/194] website/src/pages/learn/faq.mdx aktualisiert updated the section on switch angel's prebake. Now refers to the readme for instructions. --- website/src/pages/learn/faq.mdx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/website/src/pages/learn/faq.mdx b/website/src/pages/learn/faq.mdx index b16eb8cd..c2239133 100644 --- a/website/src/pages/learn/faq.mdx +++ b/website/src/pages/learn/faq.mdx @@ -265,13 +265,9 @@ Try adding `.punchcard()` after the `release(.2)` for a visualization. Methods like `trancegate()`, `rlpf()` and `acidenv()` are currently not pattern methods which come natively with strudel. -It's part of a script for strudel which was written by Switch Angel and published [here](https://github.com/switchangel/strudel-scripts/blob/main/allscripts.js) +They are part of a script/prebake for strudel which was written by Switch Angel and published [here](https://github.com/switchangel/strudel-scripts) -Note that this script defines functions and registers methods which `trancegate()` will depend on so just pasting the `trancegate()` method from that script alone will not suffice, but you will also need the `fill` method. - -If you copy the full script into your strudel, be mindful of lines which try to load local samples - these should be deleted or prefixed with `//` as a comment. - -If you paste these functions into your strudel session, hit "Update" and delete the source of the functions, the functions will still be availabe until you do a browser refresh. +You can find the instructions how to use that script in the readme.md there. ## Is there difference between `n` and `note`? From 71d1fe99e499d07f5d6a4d84bea5ea2e69b25908 Mon Sep 17 00:00:00 2001 From: jeromew Date: Tue, 2 Dec 2025 13:51:16 +0000 Subject: [PATCH 086/194] Fix special case for FeedbackDelayNode --- website/src/repl/audiograph.mjs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/website/src/repl/audiograph.mjs b/website/src/repl/audiograph.mjs index 52ac311a..bdd37971 100644 --- a/website/src/repl/audiograph.mjs +++ b/website/src/repl/audiograph.mjs @@ -5,7 +5,6 @@ This program is free software: you can redistribute it and/or modify it under th */ // main entry point is `debugAudiograph` - import { logger } from '@strudel/core'; import { getAudioContext, getSuperdoughAudioController, webaudioOutput } from '@strudel/webaudio'; @@ -57,7 +56,12 @@ const lazyRegister = (o) => { this._audioid = ++audioid; const s = JSON.parse(initCache); s.type = this.constructor.name === 'AudiographNode' ? this.constructor._parentClassName : this.constructor.name; - s.hasStop = window[s.type].prototype instanceof AudioScheduledSourceNode; + // special case for FeedbackDelayNode + // it is a subclass of DelayNode created in superdough/feedbackdelay.mjs + // it is not an AudioScheduledSourceNode anyway + if (s.type !== 'FeedbackDelayNode') { + s.hasStop = window[s.type].prototype instanceof AudioScheduledSourceNode; + } s.ac = this.context?.constructor.name || 'AudioParam'; s.creation = s.creation || stackTrace(); cache.set(this._audioid, s); From 2db30a710b9d35047c48991cdc25d36f9a70ffd9 Mon Sep 17 00:00:00 2001 From: jeromew Date: Tue, 2 Dec 2025 13:54:27 +0000 Subject: [PATCH 087/194] Fix codeformat --- website/src/repl/audiograph.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/src/repl/audiograph.mjs b/website/src/repl/audiograph.mjs index bdd37971..2ec75bef 100644 --- a/website/src/repl/audiograph.mjs +++ b/website/src/repl/audiograph.mjs @@ -56,10 +56,10 @@ const lazyRegister = (o) => { this._audioid = ++audioid; const s = JSON.parse(initCache); s.type = this.constructor.name === 'AudiographNode' ? this.constructor._parentClassName : this.constructor.name; - // special case for FeedbackDelayNode - // it is a subclass of DelayNode created in superdough/feedbackdelay.mjs - // it is not an AudioScheduledSourceNode anyway - if (s.type !== 'FeedbackDelayNode') { + // special case for FeedbackDelayNode + // it is a subclass of DelayNode created in superdough/feedbackdelay.mjs + // it is not an AudioScheduledSourceNode anyway + if (s.type !== 'FeedbackDelayNode') { s.hasStop = window[s.type].prototype instanceof AudioScheduledSourceNode; } s.ac = this.context?.constructor.name || 'AudioParam'; From 25c20296a1a8fba2824d8df01bcc121a25205127 Mon Sep 17 00:00:00 2001 From: jeromew Date: Tue, 2 Dec 2025 13:58:09 +0000 Subject: [PATCH 088/194] Tone down log message when a pure orphan node is found --- website/src/repl/audiograph.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/repl/audiograph.mjs b/website/src/repl/audiograph.mjs index 2ec75bef..fd599db7 100644 --- a/website/src/repl/audiograph.mjs +++ b/website/src/repl/audiograph.mjs @@ -539,7 +539,7 @@ export const debugAudiograph = async (argOptions = {}) => { } } else { logger('WEIRD: node ' + this.audioid + 'called disconnect before any call to connect !'); - logger(new Error().stack); + //logger(new Error().stack); console.log(cache); } return result; From d7e240e2dcce012256fc9c8bad509818a3d80828 Mon Sep 17 00:00:00 2001 From: jeromew Date: Thu, 4 Dec 2025 12:56:54 +0000 Subject: [PATCH 089/194] Add PeriodicWave & VowelNode support --- website/src/repl/audiograph.mjs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/website/src/repl/audiograph.mjs b/website/src/repl/audiograph.mjs index fd599db7..2876388a 100644 --- a/website/src/repl/audiograph.mjs +++ b/website/src/repl/audiograph.mjs @@ -56,10 +56,10 @@ const lazyRegister = (o) => { this._audioid = ++audioid; const s = JSON.parse(initCache); s.type = this.constructor.name === 'AudiographNode' ? this.constructor._parentClassName : this.constructor.name; - // special case for FeedbackDelayNode - // it is a subclass of DelayNode created in superdough/feedbackdelay.mjs - // it is not an AudioScheduledSourceNode anyway - if (s.type !== 'FeedbackDelayNode') { + // special case for subclassed AudioNodes + // they are implemented in superdough but hard to get a reference on here + // they are not AudioScheduledSourceNodes anyway + if (['FeedbackDelayNode', 'VowelNode'].indexOf(s.type) === -1) { s.hasStop = window[s.type].prototype instanceof AudioScheduledSourceNode; } s.ac = this.context?.constructor.name || 'AudioParam'; @@ -471,6 +471,7 @@ export const debugAudiograph = async (argOptions = {}) => { // and `new GainNode(..)` patterns lazyRegister(AudioNode); lazyRegister(AudioParam); + lazyRegister(PeriodicWave); const audioNodes = [ AudioBufferSourceNode, From 600ab0a83e4edd4a8650ad44546de15448e82276 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 4 Dec 2025 12:17:07 -0600 Subject: [PATCH 090/194] WIP with compressors, filters, heavy worklets --- packages/superdough/audioContext.mjs | 9 +++ packages/superdough/nodePools.mjs | 70 ++++++++++++++++++++++++ packages/superdough/superdough.mjs | 7 ++- packages/superdough/superdoughoutput.mjs | 9 +++ packages/superdough/synth.mjs | 38 +++++++------ packages/superdough/wavetable.mjs | 46 +++++++++------- packages/superdough/worklets.mjs | 68 ++++++++++------------- 7 files changed, 166 insertions(+), 81 deletions(-) create mode 100644 packages/superdough/nodePools.mjs diff --git a/packages/superdough/audioContext.mjs b/packages/superdough/audioContext.mjs index 71e01d57..3523f6df 100644 --- a/packages/superdough/audioContext.mjs +++ b/packages/superdough/audioContext.mjs @@ -1,3 +1,12 @@ +/* +audioContext.mjs - Audio Context manager + +Sets up a common and accessible audio context for all superdough operations + +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 . +*/ + let audioContext; export const setDefaultAudioContext = () => { diff --git a/packages/superdough/nodePools.mjs b/packages/superdough/nodePools.mjs new file mode 100644 index 00000000..ae59a2ce --- /dev/null +++ b/packages/superdough/nodePools.mjs @@ -0,0 +1,70 @@ +/* +nodePools.mjs - Helper functions related to pooling and re-using audio nodes + +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 . +*/ + +const nodePools = new Map(); +const POOL_KEY = Symbol('nodePoolKey'); +const IS_WORKLET_DEAD = Symbol('nodePoolIsWorkletDead'); +const MAX_POOL_SIZE = 64; + +export const isPoolable = (node) => !!node[POOL_KEY]; + +const getParams = (node) => { + const params = new Set(); + node.parameters?.forEach((param) => params.add(param)); + const visited = new Set(); // prioritize deepest definition + let proto = node; + // Move up the prototype chain + while (proto !== Object.prototype) { + for (const key of Object.getOwnPropertyNames(proto)) { + if (visited.has(key)) continue; + visited.add(key); + const value = node[key]; + if (value instanceof AudioParam) { + params.add(value); + } + } + proto = Object.getPrototypeOf(proto); + } + return params; +}; + +export const releaseNodeToPool = (node) => { + node.disconnect(); + if (node instanceof AudioScheduledSourceNode) { + // not reusable + return; + } + // Fallback to a type-based key if the node was not created via getNodeFromPool + const key = node[POOL_KEY]; + if (key == null) return; + const now = node.context?.currentTime ?? 0; + getParams(node).forEach((param) => param.cancelScheduledValues(now)); + const pool = nodePools.get(key) ?? []; + if (pool.length < MAX_POOL_SIZE) { + pool.push(new WeakRef(node)); + nodePools.set(key, pool); + } +}; + +export const markWorkletAsDead = (worklet) => (worklet[IS_WORKLET_DEAD] = true); + +// Attempt to get node from the pool. If this fails, fall back +// to building it with the factory +export const getNodeFromPool = (key, factory) => { + const pool = nodePools.get(key) ?? []; + let node; + while (pool.length) { + const ref = pool.pop(); + node = ref?.deref(); + if (node != null && !node[IS_WORKLET_DEAD]) break; + } + if (node == null || node[IS_WORKLET_DEAD]) { + node = factory(); + } + node[POOL_KEY] = key; + return node; +}; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 6f610d51..57279aac 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -9,7 +9,8 @@ import './reverb.mjs'; import './vowel.mjs'; import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs'; import workletsUrl from './worklets.mjs?audioworklet'; -import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs'; +import { createFilter, effectSend, gainNode, getCompressor, getDistortion, getLfo, getWorklet } from './helpers.mjs'; +import { getNodeFromPool, isPoolable, releaseNodeToPool } from './nodePools.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; @@ -294,7 +295,7 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000 let fOffset = 0; const filterChain = []; for (let i = 0; i < numStages; i++) { - const filter = ac.createBiquadFilter(); + const filter = getNodeFromPool('filter', () => ac.createBiquadFilter()); filter.type = 'notch'; filter.gain.value = 1; filter.frequency.value = centerFrequency + fOffset; @@ -506,7 +507,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } else if (getSound(s)) { const { onTrigger } = getSound(s); const onEnded = () => { - audioNodes.forEach((n) => n?.disconnect()); + audioNodes.forEach((n) => (isPoolable(n) ? releaseNodeToPool(n) : n?.disconnect())); activeSoundSources.delete(chainID); }; const soundHandle = await onTrigger(t, value, onEnded, cps); diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs index d8ead7d0..7bb4864f 100644 --- a/packages/superdough/superdoughoutput.mjs +++ b/packages/superdough/superdoughoutput.mjs @@ -1,3 +1,12 @@ +/* +superdoughoutput.mjs - Output controller for superdough + +Handles setting up and mixing to the outputs as well as all global (orbit) effects + +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 { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs'; import { errorLogger } from './logger.mjs'; import { clamp } from './util.mjs'; diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index b0235446..87b25ff2 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -17,6 +17,7 @@ import { } from './helpers.mjs'; import { logger } from './logger.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; +import { getNodeFromPool, markWorkletAsDead, releaseNodeToPool } from './nodePools.mjs'; const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user']; const waveformAliases = [ @@ -164,22 +165,25 @@ export function registerSynthSounds() { const end = holdend + release + 0.01; const voices = clamp(unison, 1, 100); let panspread = voices > 1 ? clamp(spread, 0, 1) : 0; - let o = getWorklet( - ac, - 'supersaw-oscillator', - { - frequency, - begin, - end, - freqspread: detune, - voices, - panspread, - }, - { - outputChannelCount: [2], - }, - ); - + const params = { + frequency, + begin, + end: end + 0.5, // add a grace period for pooling + freqspread: detune, + voices, + panspread, + }; + const factory = () => new AudioWorkletNode(ac, 'supersaw-oscillator', { outputChannelCount: [2] }); + const o = getNodeFromPool('supersaw', factory); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined) { + o.parameters.get(key).value = value; + } + }); + o.port.postMessage({ type: 'initialize' }); + o.port.onMessage = (e) => { + if (e.data.type === 'died') markWorkletAsDead(o); + }; const gainAdjustment = 1 / Math.sqrt(voices); getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin); @@ -192,7 +196,7 @@ export function registerSynthSounds() { let timeoutNode = webAudioTimeout( ac, () => { - releaseAudioNode(o); + releaseNodeToPool(o); onended(); fm?.stop(); vibratoOscillator?.stop(); diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index e9d81041..9545fa1b 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -8,10 +8,9 @@ import { getParamADSR, getPitchEnvelope, getVibratoOscillator, - getWorklet, - releaseAudioNode, webAudioTimeout, } from './helpers.mjs'; +import { getNodeFromPool, markWorkletAsDead, releaseNodeToPool } from './nodePools.mjs'; import { logger } from './logger.mjs'; export const Warpmode = Object.freeze({ @@ -225,24 +224,29 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { } const endWithRelease = holdEnd + release; const envEnd = endWithRelease + 0.01; - const source = getWorklet( - ac, - 'wavetable-oscillator-processor', - { - begin: t, - end: envEnd, - frequency, - freqspread: value.detune, - position: value.wt, - warp: value.warp, - warpMode: warpmode, - voices: Math.max(value.unison ?? 1, 1), - panspread: value.spread, - phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0, - }, - { outputChannelCount: [2] }, - ); - source.port.postMessage({ type: 'table', payload }); + const params = { + begin: t, + end: envEnd + 0.5, // add a grace period for pooling + frequency, + freqspread: value.detune, + position: value.wt, + warp: value.warp, + warpMode: warpmode, + voices: Math.max(value.unison ?? 1, 1), + panspread: value.spread, + phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0, + }; + const factory = () => new AudioWorkletNode(ac, 'wavetable-oscillator-processor', { outputChannelCount: [2] }); + const source = getNodeFromPool('wavetable', factory); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined) { + source.parameters.get(key).value = value; + } + }); + source.port.postMessage({ type: 'initialize', payload }); + source.port.onMessage = (e) => { + if (e.data.type === 'died') markWorkletAsDead(source); + }; if (ac.currentTime > t) { logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight'); return; @@ -319,7 +323,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { const timeoutNode = webAudioTimeout( ac, () => { - releaseAudioNode(source); + releaseNodeToPool(source); vibratoOscillator?.stop(); fm?.stop(); wtPosModulators?.disconnect(); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 2f75453b..2a07e56c 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -458,6 +458,15 @@ registerProcessor('distort-processor', DistortProcessor); class SuperSawOscillatorProcessor extends AudioWorkletProcessor { constructor() { super(); + this.port.onmessage = (e) => { + const { type, payload } = e.data || {}; + if (type === 'initialize') { + this.initialize(payload); + } + }; + this.initialize(); + } + initialize(_options) { this.phase = []; } static get parameterDescriptors() { @@ -1135,37 +1144,27 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { constructor(options) { super(options); + this.port.onmessage = (e) => { + const { type, payload } = e.data || {}; + if (type === 'initialize') { + this.initialize(payload); + } + }; + this.initialize(); + } + initialize(options) { this.frameLen = 0; this.numFrames = 0; this.phase = []; - - this.port.onmessage = (e) => { - const { type, payload } = e.data || {}; - if (type === 'table') { - const key = payload.key; - this.frameLen = payload.frameLen; - if (!tablesCache[key]) { - const tables = [payload.frames]; - let table = tables[0]; - for (let level = 1; level < 1; level++) { - const nextLen = table.length >> 1; - const nextTable = table.map((frame) => { - const avg = new Float32Array(nextLen); - for (let i = 0; i < nextLen; i++) { - avg[i] = (frame[2 * i] + frame[2 * i + 1]) / 2; - } - return avg; - }); - tables.push(nextTable); - table = nextTable; - if (nextLen <= 32) break; - } - tablesCache[key] = tables; - } - this.tables = tablesCache[key]; - this.numFrames = this.tables[0].length; + if (options?.frames) { + const key = options.key; + this.frameLen = options.frameLen; + if (!tablesCache[key]) { + tablesCache[key] = options.frames; } - }; + this.table = tablesCache[key]; + this.numFrames = this.table.length; + } } _mirror(x) { @@ -1310,15 +1309,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { return a + (b - a) * frac; } - _chooseMip(dphi) { - const approxHarm = clamp(dphi, 1e-6, 64); - let level = 0; - while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) { - level++; - } - return level; - } - process(_inputs, outputs, parameters) { if (currentTime >= parameters.end[0]) { return false; @@ -1362,14 +1352,12 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } const fVoice = applySemitoneDetuneToFrequency(f, detuner(n)); // voice detune const dPhase = fVoice * INVSR; - const level = this._chooseMip(dPhase); - const table = this.tables[level]; // warp phase then sample this.phase[n] = this.phase[n] ?? Math.random() * phaseRand; 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); + const s0 = this._sampleFrame(this.table[fIdx], ph); + const s1 = this._sampleFrame(this.table[Math.min(this.numFrames - 1, fIdx + 1)], ph); let s = lerp(s0, s1, interpT); if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) { s = -s; From 66107130187ba248360a8381a5f1824fad5cd428 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 4 Dec 2025 12:40:28 -0600 Subject: [PATCH 091/194] Working version of supersaw, almost on wavetable --- packages/superdough/nodePools.mjs | 4 ++++ packages/superdough/synth.mjs | 3 ++- packages/superdough/wavetable.mjs | 3 ++- packages/superdough/worklets.mjs | 17 ++++++++++++++--- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/superdough/nodePools.mjs b/packages/superdough/nodePools.mjs index ae59a2ce..08ec9ce5 100644 --- a/packages/superdough/nodePools.mjs +++ b/packages/superdough/nodePools.mjs @@ -38,6 +38,10 @@ export const releaseNodeToPool = (node) => { // not reusable return; } + if (node[IS_WORKLET_DEAD]) { + // Worklet already terminated, don't pool it + return; + } // Fallback to a type-based key if the node was not created via getNodeFromPool const key = node[POOL_KEY]; if (key == null) return; diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 87b25ff2..b10136ab 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -181,8 +181,9 @@ export function registerSynthSounds() { } }); o.port.postMessage({ type: 'initialize' }); - o.port.onMessage = (e) => { + o.port.onmessage = (e) => { if (e.data.type === 'died') markWorkletAsDead(o); + o.port.onmessage = null; }; const gainAdjustment = 1 / Math.sqrt(voices); getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 9545fa1b..a74b3747 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -244,8 +244,9 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { } }); source.port.postMessage({ type: 'initialize', payload }); - source.port.onMessage = (e) => { + source.port.onmessage = (e) => { if (e.data.type === 'died') markWorkletAsDead(source); + source.port.onmessage = null; }; if (ac.currentTime > t) { logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight'); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 2a07e56c..73fbe945 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -458,6 +458,7 @@ registerProcessor('distort-processor', DistortProcessor); class SuperSawOscillatorProcessor extends AudioWorkletProcessor { constructor() { super(); + this.isAlive = true; // used internally to prevent multiple death messages this.port.onmessage = (e) => { const { type, payload } = e.data || {}; if (type === 'initialize') { @@ -519,6 +520,10 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { process(_input, outputs, params) { if (currentTime >= params.end[0]) { // should terminate + if (this.isAlive) { + this.port.postMessage({ type: 'died' }); + this.isAlive = false; + } return false; } if (currentTime <= params.begin[0]) { @@ -1144,6 +1149,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { constructor(options) { super(options); + this.isAlive = true; // used internally to prevent multiple death messages this.port.onmessage = (e) => { const { type, payload } = e.data || {}; if (type === 'initialize') { @@ -1153,8 +1159,9 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { this.initialize(); } initialize(options) { - this.frameLen = 0; - this.numFrames = 0; + this.table = null; + this.frameLen = null; + this.numFrames = null; this.phase = []; if (options?.frames) { const key = options.key; @@ -1311,6 +1318,10 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { process(_inputs, outputs, parameters) { if (currentTime >= parameters.end[0]) { + if (this.isAlive) { + this.port.postMessage({ type: 'died' }); + this.isAlive = false; + } return false; } if (currentTime <= parameters.begin[0]) { @@ -1318,7 +1329,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } const outL = outputs[0][0]; const outR = outputs[0][1] || outputs[0][0]; - if (!this.tables) { + if (!this.table) { outL.fill(0); if (outR !== outL) outR.set(outL); return true; From 4321814d36cac20ae5625bbd813b25ea3048411d Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 4 Dec 2025 13:04:33 -0600 Subject: [PATCH 092/194] Working version for compressor, filter, supersaw, wavetable --- packages/superdough/helpers.mjs | 16 ++++++++++++---- packages/superdough/synth.mjs | 2 +- packages/superdough/wavetable.mjs | 2 +- packages/superdough/worklets.mjs | 16 +++++++++------- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 219c66d6..c5c03831 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,7 +1,8 @@ import { getAudioContext } from './audioContext.mjs'; -import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs'; -import { getNoiseBuffer } from './noise.mjs'; import { logger } from './logger.mjs'; +import { getNoiseBuffer } from './noise.mjs'; +import { getNodeFromPool } from './nodePools.mjs'; +import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs'; export const noises = ['pink', 'white', 'brown', 'crackle']; @@ -128,6 +129,7 @@ export function getLfo(audioContext, begin, end, properties = {}) { } export function getCompressor(ac, threshold, ratio, knee, attack, release) { + const node = getNodeFromPool('compressor', () => new DynamicsCompressorNode(ac, {})); const options = { threshold: threshold ?? -3, ratio: ratio ?? 10, @@ -135,7 +137,12 @@ export function getCompressor(ac, threshold, ratio, knee, attack, release) { attack: attack ?? 0.005, release: release ?? 0.05, }; - return new DynamicsCompressorNode(ac, options); + node.threshold.value = options.threshold; + node.ratio.value = options.ratio; + node.knee.value = options.knee; + node.attack.value = options.attack; + node.release.value = options.release; + return node; } // changes the default values of the envelope based on what parameters the user has defined @@ -214,7 +221,8 @@ export function createFilter(context, start, end, params, cps, cycle) { filter = getWorklet(context, 'ladder-processor', { frequency, q, drive }); frequencyParam = filter.parameters.get('frequency'); } else { - filter = context.createBiquadFilter(); + const factory = () => context.createBiquadFilter(); + filter = getNodeFromPool('filter', factory); filter.type = type; filter.Q.value = q; filter.frequency.value = frequency; diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index b10136ab..6bf7a137 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -168,7 +168,7 @@ export function registerSynthSounds() { const params = { frequency, begin, - end: end + 0.5, // add a grace period for pooling + end, freqspread: detune, voices, panspread, diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index a74b3747..2cac9fac 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -226,7 +226,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { const envEnd = endWithRelease + 0.01; const params = { begin: t, - end: envEnd + 0.5, // add a grace period for pooling + end: envEnd, frequency, freqspread: value.detune, position: value.wt, diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 73fbe945..e6887a3f 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -518,16 +518,16 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { ]; } process(_input, outputs, params) { - if (currentTime >= params.end[0]) { - // should terminate + if (currentTime >= params.end[0] + 0.5) { + // Outside of grace period - should terminate if (this.isAlive) { this.port.postMessage({ type: 'died' }); this.isAlive = false; } return false; } - if (currentTime <= params.begin[0]) { - // keep alive + if (currentTime >= params.end[0] || currentTime <= params.begin[0]) { + // Inside of grace period or not yet started return true; } const output = outputs[0]; @@ -1163,7 +1163,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { this.frameLen = null; this.numFrames = null; this.phase = []; - if (options?.frames) { + if (options?.key) { const key = options.key; this.frameLen = options.frameLen; if (!tablesCache[key]) { @@ -1317,14 +1317,16 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } process(_inputs, outputs, parameters) { - if (currentTime >= parameters.end[0]) { + if (currentTime >= parameters.end[0] + 0.5) { + // Outside of grace period - should terminate if (this.isAlive) { this.port.postMessage({ type: 'died' }); this.isAlive = false; } return false; } - if (currentTime <= parameters.begin[0]) { + if (currentTime >= parameters.end[0] || currentTime <= parameters.begin[0]) { + // Inside of grace period or not yet started return true; } const outL = outputs[0][0]; From 0664f90178610c3cb781eb478f68ebacbb49d9a8 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 4 Dec 2025 13:07:13 -0600 Subject: [PATCH 093/194] Remove old comment --- packages/superdough/nodePools.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/superdough/nodePools.mjs b/packages/superdough/nodePools.mjs index 08ec9ce5..20535224 100644 --- a/packages/superdough/nodePools.mjs +++ b/packages/superdough/nodePools.mjs @@ -42,7 +42,6 @@ export const releaseNodeToPool = (node) => { // Worklet already terminated, don't pool it return; } - // Fallback to a type-based key if the node was not created via getNodeFromPool const key = node[POOL_KEY]; if (key == null) return; const now = node.context?.currentTime ?? 0; From 4c99f4866b03ba5ad7498bfa11da6408e7609153 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 4 Dec 2025 22:50:28 -0600 Subject: [PATCH 094/194] Use setvalueattime to pin values and prevent bleed --- packages/superdough/helpers.mjs | 15 ++++++++------- packages/superdough/synth.mjs | 7 ++++--- packages/superdough/wavetable.mjs | 7 ++++--- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 1a9ed379..1430b564 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -137,11 +137,10 @@ export function getCompressor(ac, threshold, ratio, knee, attack, release) { attack: attack ?? 0.005, release: release ?? 0.05, }; - node.threshold.value = options.threshold; - node.ratio.value = options.ratio; - node.knee.value = options.knee; - node.attack.value = options.attack; - node.release.value = options.release; + const now = ac.currentTime; + Object.entries(options).forEach(([key, value]) => { + node[key].setValueAtTime(value, now); + }); return node; } @@ -224,8 +223,10 @@ export function createFilter(context, start, end, params, cps, cycle) { const factory = () => context.createBiquadFilter(); filter = getNodeFromPool('filter', factory); filter.type = type; - filter.Q.value = q; - filter.frequency.value = frequency; + const now = context.currentTime; + Object.entries({ Q: q, frequency }).forEach(([key, value]) => { + filter[key].setValueAtTime(value, now); + }); frequencyParam = filter.frequency; } const envelopeValues = [params.attack, params.decay, params.sustain, params.release]; diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 6bf7a137..8f9efa40 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -175,10 +175,11 @@ export function registerSynthSounds() { }; const factory = () => new AudioWorkletNode(ac, 'supersaw-oscillator', { outputChannelCount: [2] }); const o = getNodeFromPool('supersaw', factory); + const now = ac.currentTime; Object.entries(params).forEach(([key, value]) => { - if (value !== undefined) { - o.parameters.get(key).value = value; - } + const param = o.parameters.get(key); + const target = value !== undefined ? value : param.defaultValue; + param.setValueAtTime(target, now); }); o.port.postMessage({ type: 'initialize' }); o.port.onmessage = (e) => { diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 2cac9fac..ea075c60 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -238,10 +238,11 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { }; const factory = () => new AudioWorkletNode(ac, 'wavetable-oscillator-processor', { outputChannelCount: [2] }); const source = getNodeFromPool('wavetable', factory); + const now = ac.currentTime; Object.entries(params).forEach(([key, value]) => { - if (value !== undefined) { - source.parameters.get(key).value = value; - } + const param = source.parameters.get(key); + const target = value !== undefined ? value : param.defaultValue; + param.setValueAtTime(target, now); }); source.port.postMessage({ type: 'initialize', payload }); source.port.onmessage = (e) => { From ad43259353a962fb48ec2374a5363715a77e816f Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 5 Dec 2025 13:39:52 -0600 Subject: [PATCH 095/194] Switch to omod --- packages/core/pattern.mjs | 57 +++++++++---------- packages/soundfonts/fontloader.mjs | 2 +- packages/superdough/helpers.mjs | 4 +- packages/superdough/superdough.mjs | 89 +++++++----------------------- 4 files changed, 52 insertions(+), 100 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index d1875a96..f1cd0653 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3726,7 +3726,8 @@ const resolveConfigKey = (funcName, key) => { addConfigAlias('lfo', 'target', 't'); addConfigAlias('lfo', 'param', 'p'); addConfigAlias('lfo', 'rate', 'r'); -addConfigAlias('lfo', 'depth', 'dep', 'dp', 'd'); +addConfigAlias('lfo', 'depth', 'dep', 'dr'); +addConfigAlias('lfo', 'depthabs', 'da'); addConfigAlias('lfo', 'dcoffset', 'dc'); addConfigAlias('lfo', 'shape', 'sh'); addConfigAlias('lfo', 'skew', 'sk'); @@ -3737,21 +3738,23 @@ addConfigAlias('env', 'attack', 'att', 'a'); addConfigAlias('env', 'decay', 'dec', 'd'); addConfigAlias('env', 'sustain', 'sus', 's'); addConfigAlias('env', 'release', 'rel', 'r'); -addConfigAlias('env', 'depth', 'dep', 'dp'); +addConfigAlias('env', 'depth', 'dep', 'dr'); +addConfigAlias('env', 'depthabs', 'da'); addConfigAlias('env', 'acurve', 'ac'); addConfigAlias('env', 'dcurve', 'dc'); addConfigAlias('env', 'rcurve', 'rc'); -addConfigAlias('send', 'id'); -addConfigAlias('send', 'target', 't'); -addConfigAlias('send', 'depth', 'dep', 'dp', 'd'); -addConfigAlias('send', 'dc'); +addConfigAlias('omod', 'orbit', 'o'); +addConfigAlias('omod', 'target', 't'); +addConfigAlias('omod', 'depth', 'dep', 'dr'); +addConfigAlias('omod', 'depthabs', 'da'); +addConfigAlias('omod', 'dc'); Pattern.prototype.modulate = function (type, config, idx) { if (config == null || typeof config !== 'object') { return this; } - if (!['lfo', 'send', 'env'].includes(type)) { - logger(`[core] Modulation type ${type} not found. Please use one of 'lfo', 'env', 'send'`); + if (!['lfo', 'env', 'omod'].includes(type)) { + logger(`[core] Modulation type ${type} not found. Please use one of 'lfo', 'env', 'omod'`); return this; } let output = this; @@ -3775,19 +3778,18 @@ Pattern.prototype.modulate = function (type, config, idx) { /** * Configures an LFO. Can be called in sequence like pat.lfo(...).lfo(...) to set up multiple LFOs * - * * @name lfo * @memberof Pattern * @param {Object} config LFO configuration. - * @param {string | Pattern} [config.target] Node (and parameter if specified like `lpf.frequency`) to modulate. Aliases: target, t + * @param {string | Pattern} [config.target] Node (and parameter if specified like `lpf.frequency`) to modulate. Aliases: t * @param {number | Pattern} [config.rate] Modulation rate. Aliases: rate, r - * @param {number | Pattern} [config.depth] Modulation depth. Aliases: dep, dp, d + * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr + * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da * @param {number | Pattern} [config.dcoffset] DC offset / bias for the waveform. Aliases: dc * @param {number | Pattern} [config.shape] Waveform shape index. Aliases: sh * @param {number | Pattern} [config.skew] Waveform skew amount. Aliases: sk * @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: c * @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s - * @param {number | null} idx Index of the LFO slot to overwrite. Omit to append a new LFO * @returns Pattern */ Pattern.prototype.lfo = function (config, idx) { @@ -3798,12 +3800,12 @@ export const lfo = (config) => pure({}).lfo(config); /** * Configures an envelope. Can be called in sequence like pat.env(...).env(...) to set up multiple envelopes * - * * @name env * @memberof Pattern * @param {Object} config Envelope configuration. - * @param {string | Pattern} [config.target] Node (and parameter if specified like `lpf.frequency`) to modulate. Aliases: target, t - * @param {number | Pattern} [config.depth] Modulation depth. Aliases: dep, dp + * @param {string | Pattern} [config.target] Node (and parameter if specified like `lpf.frequency`) to modulate. Aliases: t + * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr + * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da * @param {number | Pattern} [config.attack] Time to reach depth. Aliases: att, a * @param {number | Pattern} [config.decay] Time to reach sustain. Aliases: dec, d * @param {number | Pattern} [config.sustain] Sustain depth. Aliases: sus, s @@ -3811,7 +3813,6 @@ export const lfo = (config) => pure({}).lfo(config); * @param {number | Pattern} [config.acurve] Snappiness of attack curve (-1 = relaxed, 1 = snappy). Aliases: ac * @param {number | Pattern} [config.dcurve] Snappiness of decay curve (-1 = relaxed, 1 = snappy). Aliases: dc * @param {number | Pattern} [config.rcurve] Snappiness of release curve (-1 = relaxed, 1 = snappy). Aliases: rc - * @param {number | null} idx Index of the envelope slot to overwrite. Omit to append a new envelope * @returns Pattern */ Pattern.prototype.env = function (config, idx) { @@ -3820,21 +3821,21 @@ Pattern.prototype.env = function (config, idx) { export const env = (config) => pure({}).env(config); /** - * Sends the output of this pattern to a parameter on another pattern. - * Can be called in sequence like pat.send(...).send(...) to send to multiple parameters + * Modulates with the output from a given `orbit` + * Can be called in sequence like pat.obus(...).obus(...) to set up multiple modulators * - * - * @name send + * @name omod * @memberof Pattern - * @param {Object} config Send configuration. - * @param {string | Pattern} [config.id] Pattern id to modulate - * @param {string | Pattern} [config.target] Node (and parameter if specified like `lpf.frequency`) to modulate. Aliases: target, t - * @param {number | Pattern} [config.depth] Modulation depth. Aliases:dep, dp, d + * @param {Object} config Orbit bus configuration. + * @param {string | Pattern} [config.orbit] Orbit to get modulation signal from + * @param {string | Pattern} [config.target] Node (and parameter if specified like `lpf.frequency`) to modulate. Aliases: t + * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr + * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da + * @param {number | Pattern} [config.ratio] Modulation ratio. Aliases: rat * @param {number | Pattern} [config.dc] DC offset prior to application - * @param {number | null} idx Index of the send slot to overwrite. Omit to append a new send * @returns Pattern */ -Pattern.prototype.send = function (config, idx) { - return this.modulate('send', config, idx); +Pattern.prototype.omod = function (config, idx) { + return this.modulate('omod', config, idx); }; -export const send = (config) => pure({}).send(config); +export const omod = (config) => pure({}).omod(config); diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index ea5b6818..62ea5f80 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -176,7 +176,7 @@ export function registerSoundfonts() { vibratoOscillator?.stop(); node.disconnect(); onended(); - }; + }); return { node, stop, source: bufferSource }; }, { type: 'soundfont', prebake: true, fonts }, diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 1cd292b9..33c5a52b 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -111,13 +111,11 @@ export function getEnvelope(audioContext, properties = {}) { export function getLfo(audioContext, properties = {}) { // Extract some params we need for deriving other params - const { begin, shape = 0, ...props } = properties; + const { shape = 0, ...props } = properties; const lfoprops = { - time: begin, shape: getModulationShapeInput(shape), ...props, }; - return getWorklet(audioContext, 'lfo-processor', lfoprops); } diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 44304e3c..0a091b15 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -13,12 +13,10 @@ import { createFilter, effectSend, gainNode, - getADSRValues, getCompressor, getDistortion, getEnvelope, getLfo, - getParamADSR, getWorklet, webAudioTimeout, } from './helpers.mjs'; @@ -365,7 +363,6 @@ export function resetGlobalEffects() { controller?.reset(); analysers = {}; analysersData = {}; - idToNodes = {}; } function _getNodeParam(node, name) { @@ -455,12 +452,15 @@ function _getTargetParams(nodes, target) { } function connectLFO(idx, params, nodeTracker) { - const { rate = 1, sync, cps, target, ...filteredParams } = params; + // TODO: figure out min/max values and how to handle depth and depthabs here. + const { rate = 1, sync, cps, cycle, target, ...filteredParams } = params; filteredParams['frequency'] = sync !== undefined ? sync / cps : rate; + filteredParams['time'] = cycle / cps; const ac = getAudioContext(); const lfoNode = getLfo(ac, filteredParams); nodeTracker[`lfo${idx}`] = [lfoNode]; _getTargetParams(nodeTracker, target).forEach((t) => lfoNode.connect(t)); + return lfoNode; } function connectEnvelope(idx, params, nodeTracker) { @@ -474,10 +474,12 @@ function connectEnvelope(idx, params, nodeTracker) { }); nodeTracker[`env${idx}`] = [envNode]; _getTargetParams(nodeTracker, target).forEach((t) => envNode.connect(t)); + return envNode; } -function connectSendModulator(params, signal, nodeTracker, chainID) { +function connectOrbitModulator(params, nodeTracker) { const ac = getAudioContext(); + const signal = controller.getOrbit(params.orbit).output; const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 }); dc.start(params.begin); const shifted = dc.connect(gainNode(1)); @@ -491,15 +493,6 @@ function connectSendModulator(params, signal, nodeTracker, chainID) { 0, params.begin, ); - webAudioTimeout( - ac, - () => { - modulator.disconnect(); - delete pendingConnections[params.id][chainID]; - }, - 0, - params.end + 0.05, - ); return { modulator, nodes: [dc, shifted, modulator] }; } @@ -510,8 +503,6 @@ function mapChannelNumbers(channels) { return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); } -let idToNodes = {}; -let pendingConnections = {}; export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { const nodes = {}; // new: t is always expected to be the absolute target onset time @@ -641,7 +632,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) activeSoundSources.delete(chainID); } - let audioNodes = []; + const audioNodes = []; if (['-', '~', '_'].includes(s)) { return; @@ -939,26 +930,28 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // connect chain elements together chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); - audioNodes = audioNodes.concat(chain); + audioNodes.push(...chain); // finally, now that `nodes` is populated, set up modulators if (value.lfo) { for (const [idx, params] of Object.entries(value.lfo)) { - connectLFO( + const lfo = connectLFO( idx, { ...params, cps, + cycle, begin: t, end: endWithRelease, }, nodes, ); + audioNodes.push(lfo); } } if (value.env) { for (const [idx, params] of Object.entries(value.env)) { - connectEnvelope( + const env = connectEnvelope( idx, { ...params, @@ -967,57 +960,17 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) }, nodes, ); + audioNodes.push(env); } } - if (value.id) { - idToNodes[value.id] = new WeakRef(nodes); - } - if (value.send) { - for (const p of value.send) { - const modNodes = idToNodes[p.id].deref(); - if (!modNodes) { - logger( - `[superdough] Could not connect to pattern ${p.id} -- make sure a pattern with this name exists. Available targets: ${Object.keys(idToNodes).join(', ')}`, - ); - delete idToNodes[p.id]; - } else { - const { modulator, nodes: nodesToCleanup } = connectSendModulator( - { ...p, begin: t, end: endWithRelease }, - post, - modNodes, - chainID, - ); - audioNodes = audioNodes.concat(nodesToCleanup); - pendingConnections[p.id] ??= {}; - pendingConnections[p.id][chainID] = new WeakRef([modulator, p.target, endWithRelease]); - } - } - } - if (value.id in pendingConnections) { - for (const data of Object.values(pendingConnections[value.id])) { - const derefData = data.deref(); - if (!derefData) { - delete pendingConnections[value.id]; - continue; - } - const [modulator, target, end] = derefData; - const targets = _getTargetParams(nodes, target); - webAudioTimeout( - ac, - () => { - targets.forEach((target) => modulator.connect(target)); - }, - 0, - t, - ); - webAudioTimeout( - ac, - () => { - modulator.disconnect(); - }, - 0, - end, + if (value.omod) { + for (const p of value.omod) { + const { nodes: nodesToCleanup } = connectOrbitModulator( + { ...p, begin: t, end: endWithRelease }, + nodes, + chainID, ); + audioNodes.push(...nodesToCleanup); } } }; From 177c3a4a2ef848e7f532a0fcf374fb63cff9c209 Mon Sep 17 00:00:00 2001 From: scrappy_fiddler Date: Sat, 6 Dec 2025 16:36:25 +0100 Subject: [PATCH 096/194] some items from review --- website/src/pages/learn/faq.mdx | 44 +++++++++++++++++---------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/website/src/pages/learn/faq.mdx b/website/src/pages/learn/faq.mdx index c2239133..cef6fbf5 100644 --- a/website/src/pages/learn/faq.mdx +++ b/website/src/pages/learn/faq.mdx @@ -263,8 +263,6 @@ Try adding `.punchcard()` after the `release(.2)` for a visualization. ## I saw Switch Angel using functions which I cannot find in the reference (e.g. `trancegate`). How do I make it work? -Methods like `trancegate()`, `rlpf()` and `acidenv()` are currently not pattern methods which come natively with strudel. - They are part of a script/prebake for strudel which was written by Switch Angel and published [here](https://github.com/switchangel/strudel-scripts) You can find the instructions how to use that script in the readme.md there. @@ -277,17 +275,20 @@ The method `note` is used to reference a certain note (either as its name, such On the other hand, `n` is a way to reference the nth index of something. This something can be a scale (eg `n("0 2 4").scale("C:major")`) , but it can also be a particular note in a chord (see https://strudel.cc/recipes/recipes/#arpeggios for an example) . -The method `n` can also be used for something completely unrelated to notes like the nth sample from a sample map `s("hh*8").bank("RolandTR909").n("0 1 2 3")`. Note that `pick` does _not_ use `n`. +The method `n` can also be used for something completely unrelated to notes, in particular the nth sample from a sample map `s("hh*8").bank("RolandTR909").n("0 1 2 3")`. ".pickRestart([ - n("0 1 2 0"), - n("2 3 4 ~"), - n("[4 5] [4 3] 2 0"), - n("0 -3 0 ~") - ]).scale("C:major").s("piano")`} -/>{' '} + tune={` + n("<[0 1 2 3@3 -@2] [3 2 1 0@3 -@2] >").scale("A:minor:pentatonic").s("gm_acoustic_guitar_steel").n("<0 1 2 3>/2")`} +/> + + + + + +Note that `n` is not the only way that functions use indices, some take numbered patterns instead. + ## Is there a cheat sheet for all symbols? @@ -297,29 +298,30 @@ Yes! ' marks start and end of strings, is different from " " marks start and end of single line patterns in mini notation, is different from ' ` marks start and end of patterns with line breaks in mini notation, is different from ' -[] used for patterns, each item in it has the same length -<> used for patterns, alternates each item each cycle +[] used for patterns in mini notation, each item in it has the same length +<> used for patterns, alternates between items each cycle {} historically used for polyrhythmic patterns. {a b c}%4 is the same as
*4. -@3 elongates the item by a factor of 3 (other numbers work too) +@3 elongates the item by a factor of 3 (other numbers work too, even non-integer, but for numbers between 0 and 1 you need a leading zero like this: @0.5) @ after an item: elongates the item once (multiple @ work too c @ @ is the same as c@3) -_ after an item: also elongates an item once (multiple _ work too c _ _ is the same as c@3) +_ after an item: also elongates an item once (multiple _ work too c _ _ is the same as c@3), see below for a different usage. . this divides equal parts of a pattern and is called a foot. Can be used instead of [] like this: "1 6 7 8 . 2 . 3 . 4" is the same as "[1 6 7 8] 2 3 4" - silence ~ also silence -x not silence (for the use in struct, same as 1) -s increase a note of a scale by one semitone, i.e. sharp -b decrease a note of a scale by one semitone, i.e. flat -# used in mondo notation -# also used for names of chords, like F# +x not silence (for the use in struct, any non-silence symbol works there) +b decrease by one semitone, i.e. flat, works for steps of scales, note names (but not midi numbers) and chord names +s increase by one semitone, i.e. sharp, works for steps of scales, note names (but not midi numbers) but not chord names +# increase by one semitone, i.e. sharp works for steps of scales, note names (but not midi numbers) and chord names +# also used in mondo notation + *3 play the sample or pattern at thrice the speed, fast(3) !3 play the sample or pattern three times /2 play the sample or pattern at half speed, slow(2) ? play the pattern sometimes | once per cycle, choose randomly a pattern of those separated by i.e. chooseCycles() , play all items separated by it at the same time, i.e. stack() -: is used to separate multiple parameters, such as adsr(".1:.1:.5:.2") +: is used to separate multiple parameters, such as adsr(".1:.1:.5:.2"), this is is an operator which creates a list of these objects. $: at the start of a line, defines a member of the stack. is the only stack name that should occur multiple names -_ before a stack name: mutes the stack, i.e. hush(), for example _$: s("bd") +_ before a stack name: mutes the stack, i.e. hush(), for example _$: s("bd"), see above for a different usage. ``` ## Are there more FAQ items? From bd282f439ab3b30f613e6f60eee33c098d37600e Mon Sep 17 00:00:00 2001 From: scrappy_fiddler Date: Sat, 6 Dec 2025 16:37:39 +0100 Subject: [PATCH 097/194] everyone wants that trancegate --- website/src/pages/learn/faq.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/src/pages/learn/faq.mdx b/website/src/pages/learn/faq.mdx index cef6fbf5..d4c38d41 100644 --- a/website/src/pages/learn/faq.mdx +++ b/website/src/pages/learn/faq.mdx @@ -263,6 +263,8 @@ Try adding `.punchcard()` after the `release(.2)` for a visualization. ## I saw Switch Angel using functions which I cannot find in the reference (e.g. `trancegate`). How do I make it work? +Methods like `trancegate()`, `rlpf()` and `acidenv()` are currently not pattern methods which come natively with strudel. + They are part of a script/prebake for strudel which was written by Switch Angel and published [here](https://github.com/switchangel/strudel-scripts) You can find the instructions how to use that script in the readme.md there. From 173e359a653895d3728aac34fa839f6d89a16dbb Mon Sep 17 00:00:00 2001 From: miranda Date: Sat, 6 Dec 2025 17:46:16 +0200 Subject: [PATCH 098/194] simplify envValAtTime and remove asymmetric behavior (fix #1653) --- packages/superdough/helpers.mjs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 0b696df9..0f29e1d6 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -42,6 +42,7 @@ export const getParamADSR = ( decay, sustain, release, + // min = value at start of attack, max = value at end of attack; it is possible that max < min min, max, begin, @@ -59,17 +60,15 @@ export const getParamADSR = ( max = max === 0 ? 0.001 : max; } const range = max - min; - const peak = max; const sustainVal = min + sustain * range; const duration = end - begin; const envValAtTime = (time) => { let val; if (attack > time) { - let slope = getSlope(min, peak, 0, attack); - val = time * slope + (min > peak ? min : 0); + val = time * getSlope(min, max, 0, attack) + min; } else { - val = (time - attack) * getSlope(peak, sustainVal, 0, decay) + peak; + val = (time - attack) * getSlope(max, sustainVal, 0, decay) + max; } if (curve === 'exponential') { val = val || 0.001; From ab03ece2b7ea61e9de4761e73f318e62812168c4 Mon Sep 17 00:00:00 2001 From: scrappy_fiddler Date: Sat, 6 Dec 2025 16:48:40 +0100 Subject: [PATCH 099/194] prettier --- packages/osc/server.js | 0 website/src/pages/learn/faq.mdx | 12 +++--------- 2 files changed, 3 insertions(+), 9 deletions(-) mode change 100755 => 100644 packages/osc/server.js diff --git a/packages/osc/server.js b/packages/osc/server.js old mode 100755 new mode 100644 diff --git a/website/src/pages/learn/faq.mdx b/website/src/pages/learn/faq.mdx index d4c38d41..36a8bbef 100644 --- a/website/src/pages/learn/faq.mdx +++ b/website/src/pages/learn/faq.mdx @@ -267,7 +267,7 @@ Methods like `trancegate()`, `rlpf()` and `acidenv()` are currently not pattern They are part of a script/prebake for strudel which was written by Switch Angel and published [here](https://github.com/switchangel/strudel-scripts) -You can find the instructions how to use that script in the readme.md there. +You can find the instructions how to use that script in the readme.md there. ## Is there difference between `n` and `note`? @@ -277,21 +277,16 @@ The method `note` is used to reference a certain note (either as its name, such On the other hand, `n` is a way to reference the nth index of something. This something can be a scale (eg `n("0 2 4").scale("C:major")`) , but it can also be a particular note in a chord (see https://strudel.cc/recipes/recipes/#arpeggios for an example) . -The method `n` can also be used for something completely unrelated to notes, in particular the nth sample from a sample map `s("hh*8").bank("RolandTR909").n("0 1 2 3")`. +The method `n` can also be used for something completely unrelated to notes, in particular the nth sample from a sample map `s("hh*8").bank("RolandTR909").n("0 1 2 3")`. ").scale("A:minor:pentatonic").s("gm_acoustic_guitar_steel").n("<0 1 2 3>/2")`} /> - - - - Note that `n` is not the only way that functions use indices, some take numbered patterns instead. - ## Is there a cheat sheet for all symbols? Yes! @@ -314,14 +309,13 @@ b decrease by one semitone, i.e. flat, works for steps of scales, note names ( s increase by one semitone, i.e. sharp, works for steps of scales, note names (but not midi numbers) but not chord names # increase by one semitone, i.e. sharp works for steps of scales, note names (but not midi numbers) and chord names # also used in mondo notation - *3 play the sample or pattern at thrice the speed, fast(3) !3 play the sample or pattern three times /2 play the sample or pattern at half speed, slow(2) ? play the pattern sometimes | once per cycle, choose randomly a pattern of those separated by i.e. chooseCycles() , play all items separated by it at the same time, i.e. stack() -: is used to separate multiple parameters, such as adsr(".1:.1:.5:.2"), this is is an operator which creates a list of these objects. +: is used to separate multiple parameters, such as adsr(".1:.1:.5:.2"), this is is an operator which creates a list of these objects. $: at the start of a line, defines a member of the stack. is the only stack name that should occur multiple names _ before a stack name: mutes the stack, i.e. hush(), for example _$: s("bd"), see above for a different usage. ``` From 6402ad5d37f70583ba967e8364e1610620a95e01 Mon Sep 17 00:00:00 2001 From: scrappy_fiddler Date: Sat, 6 Dec 2025 16:55:57 +0100 Subject: [PATCH 100/194] better linebreak in example --- website/src/pages/learn/faq.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/src/pages/learn/faq.mdx b/website/src/pages/learn/faq.mdx index 36a8bbef..b91b48cc 100644 --- a/website/src/pages/learn/faq.mdx +++ b/website/src/pages/learn/faq.mdx @@ -282,7 +282,9 @@ The method `n` can also be used for something completely unrelated to notes, in ").scale("A:minor:pentatonic").s("gm_acoustic_guitar_steel").n("<0 1 2 3>/2")`} + n("<[0 1 2 3@3 -@2] [3 2 1 0@3 -@2] >") + .scale("A:minor:pentatonic") + .s("gm_acoustic_guitar_steel").n("<0 1 2 3>/2")`} /> Note that `n` is not the only way that functions use indices, some take numbered patterns instead. From 82ca77be9d74c5cae55fb734f3262bbe9f01c1f6 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 6 Dec 2025 12:02:11 -0600 Subject: [PATCH 101/194] Working version --- packages/core/pattern.mjs | 13 ++- packages/superdough/superdough.mjs | 108 +++++++++++++++++++------ packages/superdough/superdoughdata.mjs | 85 +++++++++++++++++++ packages/superdough/worklets.mjs | 17 ++-- 4 files changed, 191 insertions(+), 32 deletions(-) create mode 100644 packages/superdough/superdoughdata.mjs diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index f1cd0653..986ac9af 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3758,19 +3758,26 @@ Pattern.prototype.modulate = function (type, config, idx) { return this; } let output = this; + let target = config.target; for (const [rawKey, value] of Object.entries(config)) { const key = resolveConfigKey(type, rawKey); - const pat = reify(value); + if (key === 'target') continue; // we will set/default it below + const valuePat = reify(value); output = output .fmap((v) => (c) => { + if (target == null) { + // default target to the control set just before this in the chain + // e.g. pat.gain(0.5).lfo({..}) will be a gain-LFO + target = Object.keys(v).at(-1); + } v[type] ??= []; const t = v[type]; idx ??= t.length; - t[idx] ??= {}; + t[idx] ??= { target }; // set target t[idx][key] = c; return v; }) - .appLeft(pat); + .appLeft(valuePat); } return output; }; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 0a091b15..f32a1716 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -25,6 +25,7 @@ import { errorLogger, logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; import { getAudioContext } from './audioContext.mjs'; import { SuperdoughAudioController } from './superdoughoutput.mjs'; +import { getSuperdoughControlData } from './superdoughdata.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; @@ -416,6 +417,28 @@ const targetToParamGuess = { send: 'depth', }; +const controlData = getSuperdoughControlData(); + +// TODO: We would like to use the aliases in `controls` here, but cannot as +// the modules are independent. We may want to inject that method here in situations +// where superdough/core are run in tandem +export let getControlName; +const _getMainName = (control) => getControlName?.(control) ?? control; + +function _getControlData(control) { + return controlData[_getMainName(control)]; +} + +function _getControlValue(control, value, data) { + const main = _getMainName(control); + return Number(value[main] ?? data.default); +} + +function _getControlClamp(data, currentValue) { + const { min, max } = data; + return { min: min - currentValue, max: max - currentValue }; +} + function _getTargetParams(nodes, target) { let param; if (target.includes('.')) { @@ -451,49 +474,77 @@ function _getTargetParams(nodes, target) { return audioParams; } -function connectLFO(idx, params, nodeTracker) { - // TODO: figure out min/max values and how to handle depth and depthabs here. - const { rate = 1, sync, cps, cycle, target, ...filteredParams } = params; - filteredParams['frequency'] = sync !== undefined ? sync / cps : rate; - filteredParams['time'] = cycle / cps; - const ac = getAudioContext(); - const lfoNode = getLfo(ac, filteredParams); +function _getTargetParamsForControl(control, nodes) { + const main = _getMainName(control); + const data = _getControlData(main); + const targetParams = _getTargetParams(nodes, data.param); + return { targetParams, data }; +} + +function connectLFO(idx, params, nodeTracker, value) { + const { rate = 1, sync, cps, cycle, target, depth = 1, depthabs, ...filteredParams } = params; + const { targetParams, data } = _getTargetParamsForControl(target, nodeTracker); + const currentValue = _getControlValue(target, value, data); + const { min, max } = _getControlClamp(data, currentValue); + const depthValue = depthabs != null ? depthabs : depth * currentValue; + const modParams = { + ...filteredParams, + frequency: sync !== undefined ? sync / cps : rate, + time: cycle / cps, + depth: depthValue, + min, + max, + }; + const lfoNode = getLfo(getAudioContext(), modParams); nodeTracker[`lfo${idx}`] = [lfoNode]; - _getTargetParams(nodeTracker, target).forEach((t) => lfoNode.connect(t)); + targetParams.forEach((t) => lfoNode.connect(t)); return lfoNode; } -function connectEnvelope(idx, params, nodeTracker) { - const { target, acurve, dcurve, rcurve, ...filteredParams } = params; - const ac = getAudioContext(); - const envNode = getEnvelope(ac, { +function connectEnvelope(idx, params, nodeTracker, value) { + const { target, acurve, dcurve, rcurve, depth = 1, depthabs, ...filteredParams } = params; + const { targetParams, data } = _getTargetParamsForControl(target, nodeTracker); + const currentValue = _getControlValue(target, value, data); + const { min, max } = _getControlClamp(data, currentValue); + const depthValue = depthabs != null ? depthabs : depth * currentValue; + const envNode = getEnvelope(getAudioContext(), { ...filteredParams, + depth: depthValue, + min, + max, attackCurve: acurve, decayCurve: dcurve, releaseCurve: rcurve, }); nodeTracker[`env${idx}`] = [envNode]; - _getTargetParams(nodeTracker, target).forEach((t) => envNode.connect(t)); + targetParams.forEach((t) => envNode.connect(t)); return envNode; } -function connectOrbitModulator(params, nodeTracker) { +function connectOrbitModulator(params, nodeTracker, value) { const ac = getAudioContext(); + const { target, depth = 1, depthabs } = params; const signal = controller.getOrbit(params.orbit).output; const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 }); dc.start(params.begin); const shifted = dc.connect(gainNode(1)); signal.connect(shifted); - const modulator = shifted.connect(gainNode((params.depth ?? 1) / 0.3)); + const { targetParams, data } = _getTargetParamsForControl(target, nodeTracker); + const currentValue = _getControlValue(target, value, data); + const { min, max } = _getControlClamp(data, currentValue); + const depthValue = depthabs != null ? depthabs : depth * currentValue; + const maxAbsDepth = Math.min(Math.abs(min), Math.abs(max)); + const boundedDepth = Math.min(Math.abs(depthValue), maxAbsDepth); + const modulator = shifted.connect(gainNode((Math.sign(depthValue) * boundedDepth) / 0.3)); webAudioTimeout( ac, () => { - _getTargetParams(nodeTracker, params.target).forEach((t) => modulator.connect(t)); + targetParams.forEach((t) => modulator.connect(t)); }, 0, params.begin, ); - return { modulator, nodes: [dc, shifted, modulator] }; + return { modulator, toCleanup: [dc, shifted, modulator] }; } let activeSoundSources = new Map(); @@ -710,11 +761,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) lpParams.type = 'lowpass'; const { filter: lpf1, lfo: lfo1 } = filt(lpParams); nodes['lpf'] = [lpf1]; + nodes['lpf_lfo'] = [lfo1]; chain.push(lpf1); lfo1 && audioNodes.push(lfo1); if (ftype === '24db') { const { filter: lpf2, lfo: lfo2 } = filt(lpParams); nodes['lpf'].push(lpf2); + nodes['lpf_lfo'].push(lfo2); chain.push(lpf2); lfo2 && audioNodes.push(lfo2); } @@ -744,11 +797,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) hpParams.type = 'highpass'; const { filter: hpf1, lfo: lfo1 } = filt(hpParams); nodes['hpf'] = [hpf1]; + nodes['hpf_lfo'] = [lfo1]; lfo1 && audioNodes.push(lfo1); chain.push(hpf1); if (ftype === '24db') { const { filter: hpf2, lfo: lfo2 } = filt(hpParams); nodes['hpf'].push(hpf2); + nodes['hpf_lfo'].push(lfo2); chain.push(hpf2); lfo2 && audioNodes.push(lfo2); } @@ -777,12 +832,16 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) const bpParams = pickAndRename(value, bpMap); bpParams.type = 'bandpass'; const { filter: bpf1, lfo: lfo1 } = filt(bpParams); + nodes['bpf'] = [bpf1]; + nodes['bpf_lfo'] = [lfo1]; chain.push(bpf1); lfo1 && audioNodes.push(lfo1); if (ftype === '24db') { const { filter: bpf2, lfo: lfo2 } = filt(bpParams); nodes['bpf'].push(bpf2); + nodes['bpf_lfo'].push(lfo2); chain.push(bpf2); + lfo2 && audioNodes.push(lfo2); } } @@ -847,6 +906,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) begin: t, end: endWithRelease, }); + nodes['tremolo'] = [lfo]; + nodes['tremolo_gain'] = [amGain]; lfo.connect(amGain.gain); audioNodes.push(lfo); chain.push(amGain); @@ -904,7 +965,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } roomIR = await loadBuffer(url, ac, ir, 0); } - orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin); + const roomNode = orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin); + nodes['room'] = [roomNode]; const reverbSend = orbitBus.sendReverb(post, room); audioNodes.push(reverbSend); } @@ -945,6 +1007,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) end: endWithRelease, }, nodes, + value, ); audioNodes.push(lfo); } @@ -959,18 +1022,15 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) end: endWithRelease, }, nodes, + value, ); audioNodes.push(env); } } if (value.omod) { for (const p of value.omod) { - const { nodes: nodesToCleanup } = connectOrbitModulator( - { ...p, begin: t, end: endWithRelease }, - nodes, - chainID, - ); - audioNodes.push(...nodesToCleanup); + const { toCleanup } = connectOrbitModulator({ ...p, begin: t, end: endWithRelease }, nodes, value); + audioNodes.push(...toCleanup); } } }; diff --git a/packages/superdough/superdoughdata.mjs b/packages/superdough/superdoughdata.mjs new file mode 100644 index 00000000..1f3235e5 --- /dev/null +++ b/packages/superdough/superdoughdata.mjs @@ -0,0 +1,85 @@ +/* +superdoughdata.mjs - Data needed for running superdough (defaults, mappings, etc.) +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 . +*/ + +const CONTROL_DATA = { + stretch: { param: 'stretch.pitchFactor', default: 1, min: -4, max: 100 }, + gain: { param: 'gain.gain', default: 0.8, min: 0, max: 10 }, + postgain: { param: 'post.gain', default: 1, min: 0, max: 10 }, + pan: { param: 'pan.pan', min: 0, max: 1 }, + tremolo: { param: 'tremolo.rate', min: 0, max: 40 }, + tremolosync: { param: 'tremolo.sync', min: 0, max: 8 }, + tremolodepth: { param: 'tremolo_gain.gain', default: 1, min: 0, max: 10 }, + tremoloskew: { param: 'tremolo.skew', min: 0, max: 1 }, + tremolophase: { param: 'tremolo.phase', default: 0, min: 0, max: 1 }, + tremoloshape: { param: 'tremolo.shape', min: 0, max: 4 }, + + // LPF + cutoff: { param: 'lpf.frequency', min: 20, max: 24000 }, + resonance: { param: 'lpf.Q', min: 0.1, max: 30 }, + lprate: { param: 'lpf_lfo.rate', min: 0, max: 40 }, + lpsync: { param: 'lpf_lfo.sync', min: 0, max: 8 }, + lpdepth: { param: 'lpf_lfo.depth', min: 20, max: 24000 }, + lpdepthfrequency: { param: 'lpf_lfo.depth', min: 20, max: 24000 }, + lpshape: { param: 'lpf_lfo.shape', min: 0, max: 4 }, + lpdc: { param: 'lpf_lfo.dcoffset', min: -1, max: 1 }, + lpskew: { param: 'lpf_lfo.skew', min: 0, max: 1 }, + + // HPF + hcutoff: { param: 'hpf.frequency', min: 20, max: 24000 }, + hresonance: { param: 'hpf.Q', min: 0.1, max: 30 }, + + // BPF + bandf: { param: 'bpf.frequency', min: 20, max: 24000 }, + bandq: { param: 'bpf.Q', min: 0.1, max: 30 }, + + // FILTERS + // fanchor: { param: ['lpf.anchor', 'hpf.anchor', 'bpf.anchor'], min: 0, max: 1 }, + // drive: { param: ['lpf.drive', 'hpf.drive', 'bpf.drive'], min: 0, max: 2 }, + // vowel: { param: 'vowel.frequency', min: 200, max: 4000 }, + + // DISTORTION + coarse: { param: 'coarse.coarse', min: 1, max: 64 }, + crush: { param: 'crush.crush', min: 1, max: 16 }, + shape: { param: 'shape.shape', min: -1, max: 0.999 }, + shapevol: { param: 'shape.postgain', default: 1, min: 0, max: 1 }, + distort: { param: 'distort.distort', min: 0, max: 5 }, + distortvol: { param: 'distort.postgain', default: 1, min: 0, max: 1 }, + // distorttype: { param: 'distort.algorithm', default: 0, min: 0, max: 4 }, + + // COMPRESSOR + compressor: { param: 'compressor.threshold', default: -3, min: -100, max: 0 }, + compressorRatio: { param: 'compressor.ratio', default: 10, min: 1, max: 20 }, + compressorKnee: { param: 'compressor.knee', default: 10, min: 0, max: 40 }, + compressorAttack: { param: 'compressor.attack', default: 0.005, min: 0, max: 1 }, + compressorRelease: { param: 'compressor.release', default: 0.05, min: 0, max: 2 }, + + // PHASER + phaserrate: { param: 'phaser.rate', min: 0, max: 40 }, + phaserdepth: { param: 'phaser.depth', default: 0.75, min: 0, max: 1 }, + phasersweep: { param: 'phaser.sweep', min: 0, max: 5000 }, + phasercenter: { param: 'phaser.frequency', min: 20, max: 24000 }, + + // ORBIT EFFECTS + // delay: { param: 'delay.delayTime', default: 0, min: 0, max: 4 }, + delaytime: { param: 'delay.delayTime', min: 0, max: 4 }, + // delayfeedback: { param: 'delay.feedback', default: 0.5, min: 0, max: 0.98 }, + // delaysync: { param: 'delay.sync', default: 3 / 16, min: 0, max: 2 }, + dry: { param: 'dry.gain', min: 0, max: 1 }, + room: { param: 'room.wet', min: 0, max: 1 }, + // roomfade: { param: 'room.fade', min: 0, max: 1 }, + roomlp: { param: 'room.lp', min: 20, max: 24000 }, + djf: { param: 'djf.value', min: 0, max: 1 }, + + // SYNTHS + detune: { param: 'source.detune', min: 0, max: 1 }, + wt: { param: 'source.position', min: 0, max: 1 }, + warp: { param: 'source.warp', min: 0, max: 1 }, + freq: { param: 'source.frequency', min: 20, max: 24000 }, +}; + +export function getSuperdoughControlData() { + return CONTROL_DATA; +} diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 65da72bb..bb33a24f 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -121,8 +121,8 @@ class LFOProcessor extends AudioWorkletProcessor { { name: 'shape', defaultValue: 0 }, { name: 'curve', defaultValue: 1 }, { name: 'dcoffset', defaultValue: 0 }, - { name: 'min', defaultValue: 0 }, - { name: 'max', defaultValue: 1 }, + { name: 'min', defaultValue: -1e9 }, + { name: 'max', defaultValue: 1e9 }, ]; } @@ -160,8 +160,10 @@ class LFOProcessor extends AudioWorkletProcessor { const dcoffset = parameters['dcoffset'][0]; - const min = dcoffset * depth; - const max = dcoffset * depth + depth; + const userMin = parameters['min'][0]; + const userMax = parameters['max'][0]; + const min = Math.min(userMin, userMax); + const max = Math.max(userMin, userMax); const shape = waveShapeNames[parameters['shape'][0]]; const blockSize = output[0].length ?? 0; @@ -967,6 +969,8 @@ class EnvelopeProcessor extends AudioWorkletProcessor { { name: 'decayCurve', defaultValue: 0, minValue: -1, maxValue: 1 }, { name: 'releaseCurve', defaultValue: 0, minValue: -1, maxValue: 1 }, { name: 'depth', defaultValue: 1 }, + { name: 'min', defaultValue: -1e9 }, + { name: 'max', defaultValue: 1e9 }, { name: 'retrigger', defaultValue: 1, minValue: 0, maxValue: 1 }, ]; } @@ -1029,6 +1033,8 @@ class EnvelopeProcessor extends AudioWorkletProcessor { const dCurve = pv(params.decayCurve, i); const rCurve = pv(params.releaseCurve, i); const depth = pv(params.depth, i); + const clampMin = pv(params.min, i); + const clampMax = pv(params.max, i); const states = [ { time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle { time: attack, start: this.attackStart, target: 1, curve: aCurve }, @@ -1042,7 +1048,8 @@ class EnvelopeProcessor extends AudioWorkletProcessor { this.state = (this.state + 1) % states.length; time = states[this.state].time; } - out[i] = this.val * depth; + const clamped = clamp(this.val * depth, Math.min(clampMin, clampMax), Math.max(clampMin, clampMax)); + out[i] = clamped; } return true; } From 53c101005c9f4bee299842a819d7ae27a7ddc58a Mon Sep 17 00:00:00 2001 From: Wuzzy Date: Sun, 7 Dec 2025 00:12:40 +0100 Subject: [PATCH 102/194] Document "-" in mini-notation --- website/src/pages/learn/mini-notation.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/src/pages/learn/mini-notation.mdx b/website/src/pages/learn/mini-notation.mdx index 02ead743..8b09f12f 100644 --- a/website/src/pages/learn/mini-notation.mdx +++ b/website/src/pages/learn/mini-notation.mdx @@ -142,6 +142,8 @@ The "~" represents a rest, and will create silence between other events: +Alternatively, "-" can be used instead of "~". It means the same thing. + ## Parallel / polyphony Using commas, we can play chords. From a79491dd16fbe10460d5263c77273abe1b305113 Mon Sep 17 00:00:00 2001 From: Aria Date: Mon, 8 Dec 2025 16:32:54 -0600 Subject: [PATCH 103/194] Update loopbegin/end to not be offset --- packages/superdough/sampler.mjs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 2deeb4b7..8e46426d 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -65,21 +65,22 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => { bufferSource.playbackRate.value = playbackRate; const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue; + const bufferDuration = bufferSource.buffer.duration; - // "The computation of the offset into the sound is performed using the sound buffer's natural sample rate, - // rather than the current playback rate, so even if the sound is playing at twice its normal speed, - // the midway point through a 10-second audio buffer is still 5." - const offset = begin * bufferSource.buffer.duration; + // The computation of the offset into the sound is performed using the sound buffer's natural duration, + // rather than the playback duration, so that even if the sound is playing at twice its normal speed, + // the midway point through a 10-second audio buffer is still 5. + const offset = begin * bufferDuration; const loop = hapValue.loop; if (loop) { bufferSource.loop = true; - bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset; - bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset; + bufferSource.loopStart = loopBegin * bufferDuration; + bufferSource.loopEnd = loopEnd * bufferDuration; } - const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value; - const sliceDuration = (end - begin) * bufferDuration; - return { bufferSource, offset, bufferDuration, sliceDuration }; + const playbackDuration = bufferDuration / bufferSource.playbackRate.value; + const sliceDuration = (end - begin) * playbackDuration; + return { bufferSource, offset, bufferDuration, playbackDuration, sliceDuration }; }; export const loadBuffer = (url, ac, s, n = 0) => { From e89414e400362ebe064858e4c0b1cc3441b56dca Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 10 Dec 2025 09:19:12 +0000 Subject: [PATCH 104/194] fix .as so it doesn't set undefined values --- packages/core/controls.mjs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index c7edef74..f28d13f6 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2753,8 +2753,13 @@ export const as = register('as', (mapping, pat) => { mapping = Array.isArray(mapping) ? mapping : [mapping]; return pat.fmap((v) => { v = Array.isArray(v) ? v : [v]; - v = Object.fromEntries(mapping.map((prop, i) => [getControlName(prop), v[i]])); - return v; + const entries = []; + for (let i = 0; i < mapping.length; ++i) { + if (v[i] !== undefined) { + entries.push([getControlName(mapping[i]), v[i]]); + } + } + return Object.fromEntries(entries); }); }); From 12884cba3a0db216fa14a1c1c3d2521ab38fcccf Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Dec 2025 12:09:26 +0000 Subject: [PATCH 105/194] Updates relating to LLM, github, etc --- CONTRIBUTING.md | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7f65b5b..a4cf5d9e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,20 +4,13 @@ Thanks for wanting to contribute!!! There are many ways you can add value to thi ## Move to codeberg -We are currently in the process of moving from github to codeberg -- not everything is working, please bear with us. - -To update your local clone, you can run this command: - -``` -git remote set-url origin git@codeberg.org:uzu/strudel.git -``` - +Along with many other live coding projects, we have moved from Microsoft's Github platform to Codeberg for ethical reasons. Please don't fork the project back to github. ## Communication Channels -To get in touch with the contributors, either +To get in touch with the community, either -- [join the Tidal Discord Channel](https://discord.com/invite/HGEdXmRkzT) and go to the #strudel channel +- [join the Tidal Discord Channel](https://discord.com/invite/HGEdXmRkzT) and go to the #strudel channels - Find related discussions on the [tidal club forum](https://club.tidalcycles.org/) ## Ask a Question @@ -36,22 +29,33 @@ Use one of the Communication Channels listed above and drop us a line or two! ## Share Music If you made some music with strudel, you can give back some love and share what you've done! -Your creation could also be part of the random selection in the REPL if you want. Use one of the Communication Channels listed above. +(There used to be a random selection of contributed patterns in the REPL, but that is unfortunately disabled for now due to abuse) ## Improve the Docs -If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/), -you can edit each file directly on codeburg. (we are currently fixing the "Edit this page" links in the right sidebar) +If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/), you can edit each file directly on codeburg. There are "Edit this page" links in the right sidebar that take you to the right place. ## Propose a Feature -If you want a specific feature that is not part of strudel yet, feel free to use one of the communication channels above. -Maybe you even want to help with the implementation of that feature! +If you want a specific feature that is not part of strudel yet, feel free to use one of the communication channels above. Please bear in mind that this is a free/open source project, oriented around collaborative discussion. Maybe you even want to help with the implementation of that feature! + +## Contribute a feature + +It's generally best to start with discussion rather than do loads of work on something and then find it doesn't fit the collective goals of the project, or something like that. + +## AI/LLM policy + +Strudel is a project handmade by humans, with a lot of thought and nuance. We are still developing our response to the onslaught of LLM technology. + +If you have used LLMs (so called 'AI'), please make that clear in the pull request. If code is LLM-generated/isn't your own work, then the PR will not be merged, for practical and legal reasons. + +There are #llm-chat and #llm-share channels on our discord. Please do not discuss or share LLM-related things outside of those channels. ## Report a Bug If you've found a bug, or some behaviour that does not seem right, you are welcome to file an [issue](https://codeberg.org/uzu/strudel/issues). + Please check that it has not been reported before. ## Fix a Bug @@ -118,8 +122,7 @@ There are also eslint extensions / plugins for most editors. ## Running all CI Checks -When opening a PR, the CI runner will automatically check the code style and eslint, as well as run all tests. -You can run the same check with `pnpm check` +When opening a PR, the CI runner will (once approved by a human) check the code style and eslint, as well as run all tests. You can run the same check with `pnpm check`. ## Package Workflow From 960f1de9d2d125c95737b73abaf382fcfe4b5ab7 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Dec 2025 12:16:54 +0000 Subject: [PATCH 106/194] tweaks --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a4cf5d9e..10146bec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ Along with many other live coding projects, we have moved from Microsoft's Githu To get in touch with the community, either -- [join the Tidal Discord Channel](https://discord.com/invite/HGEdXmRkzT) and go to the #strudel channels +- [join the Uzulang Discord Server](https://discord.com/invite/HGEdXmRkzT) and go to the strudel channels (Uzulangs are a family of live coding languages inspired by each other, including TidalCycles as well as Strudel) - Find related discussions on the [tidal club forum](https://club.tidalcycles.org/) ## Ask a Question @@ -42,9 +42,9 @@ If you want a specific feature that is not part of strudel yet, feel free to use ## Contribute a feature -It's generally best to start with discussion rather than do loads of work on something and then find it doesn't fit the collective goals of the project, or something like that. +Pull requests welcome! Consider starting with discussion or proof-of-concept first, rather than do loads of work on something and then find it doesn't fit the collective goals of the project, or something like that. -## AI/LLM policy +### AI/LLM policy Strudel is a project handmade by humans, with a lot of thought and nuance. We are still developing our response to the onslaught of LLM technology. From 96835be056e626020b6917ab8fad2cbcbcae1232 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Dec 2025 15:07:57 +0000 Subject: [PATCH 107/194] tweaks --- CONTRIBUTING.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 10146bec..b8a941fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ Use one of the Communication Channels listed above. ## Improve the Docs -If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/), you can edit each file directly on codeburg. There are "Edit this page" links in the right sidebar that take you to the right place. +If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/), you can edit each file directly on codeberg. There are "Edit this page" links in the right sidebar that take you to the right place. ## Propose a Feature @@ -44,11 +44,13 @@ If you want a specific feature that is not part of strudel yet, feel free to use Pull requests welcome! Consider starting with discussion or proof-of-concept first, rather than do loads of work on something and then find it doesn't fit the collective goals of the project, or something like that. +At the time of writing we have a PR backlog, and generally prioritise bugfixes and contributions that have arisen through community discussion. + ### AI/LLM policy -Strudel is a project handmade by humans, with a lot of thought and nuance. We are still developing our response to the onslaught of LLM technology. +Strudel is a project handmade by humans, with thought and nuance. -If you have used LLMs (so called 'AI'), please make that clear in the pull request. If code is LLM-generated/isn't your own work, then the PR will not be merged, for practical and legal reasons. +If you have used LLMs (so called 'AI'), please detail that in the pull request. We are still developing our response to the onslaught of LLM technology, but for practical and legal reasons are currently not accepting wholly LLM-generated code. We are also not accepting PRs that add LLM features to strudel itself. There are #llm-chat and #llm-share channels on our discord. Please do not discuss or share LLM-related things outside of those channels. From 8cbe4b945cfbd050ddbd85755c459a65135a90f0 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Dec 2025 16:39:08 +0000 Subject: [PATCH 108/194] Respond to glossing's review --- packages/core/impure.mjs | 35 +++++++++++++++++++++++++++++------ packages/core/repl.mjs | 4 ++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/core/impure.mjs b/packages/core/impure.mjs index e16cfbe4..84886aa4 100644 --- a/packages/core/impure.mjs +++ b/packages/core/impure.mjs @@ -8,24 +8,48 @@ import { register, reify, Pattern } from './pattern.mjs'; let timelines = {}; +export const reset_state = function () { + reset_timelines(); +}; + export const reset_timelines = function () { timelines = {}; }; +/*** + * Allows you to switch a pattern between different 'timelines'. This is particularly useful when + * live coding, for example when you want to cue a pattern up to play from its start. + * + * Timelines are specified by number, so that if you had a pattern like + * `n("<0 1 2 3>").s("num").timeline(1)` playing, then changed the '1' + * to '2', it would always align '0' to the nearest cycle. You will likely want to trigger + * an evaluation a little bit before the cycle starts, to avoid missing events. + * + * If you change and evaluate a pattern without changing the timeline, it will stay on that timeline + * without resetting. + * + * Rather than incrementing a timeline to reset it, it's easier to negate it, e.g. by switching between `-2` + * and `2`. This is because when you negate a timeline it will always reset. + * + * You can also pattern the timeline if you want, to create strange resetting patterns. + */ + export const timeline = register( 'timeline', function (tpat, pat) { tpat = reify(tpat); const f = function (state) { - let scheduler = !!state.controls._cps; + // Is this called from the scheduler? (rather than from e.g. the visualiser) + const scheduler = !!state.controls._cyclist; 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) { + if (tlid === 0) { + offset = 0; + } else if (tlid in timelines) { offset = timelines[tlid]; } else { const timearc = timehap.wholeOrPart(); @@ -40,9 +64,8 @@ export const timeline = register( if (scheduler) { // update state timelines[tlid] = offset; - const negative = 0 - tlid; - if (negative in timelines) { - delete timelines[negative]; + if (tlid !== 0) { + delete timelines[-tlid]; } } diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 171697eb..cd78cca4 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -5,6 +5,7 @@ import { errorLogger, logger } from './logger.mjs'; import { setTime } from './time.mjs'; import { evalScope } from './evaluate.mjs'; import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; +import { reset_state } from './impure.mjs'; export function repl({ defaultOutput, @@ -52,6 +53,9 @@ export function repl({ onToggle: (started) => { updateState({ started }); onToggle?.(started); + if (!started) { + reset_state(); + } }, setInterval, clearInterval, From 37990b459f3571d4b254ca99030e21d4cf836104 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Dec 2025 16:51:25 +0000 Subject: [PATCH 109/194] bugfix and tweak doc --- packages/core/impure.mjs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/core/impure.mjs b/packages/core/impure.mjs index 84886aa4..304f801f 100644 --- a/packages/core/impure.mjs +++ b/packages/core/impure.mjs @@ -25,13 +25,21 @@ export const reset_timelines = function () { * to '2', it would always align '0' to the nearest cycle. You will likely want to trigger * an evaluation a little bit before the cycle starts, to avoid missing events. * - * If you change and evaluate a pattern without changing the timeline, it will stay on that timeline - * without resetting. + * After the first use, a timeline will continue with the same 'offset'. That is, if you change + * a pattern without changing its timeline number, it will stay on that timeline without resetting. * * Rather than incrementing a timeline to reset it, it's easier to negate it, e.g. by switching between `-2` * and `2`. This is because when you negate a timeline it will always reset. * * You can also pattern the timeline if you want, to create strange resetting patterns. + * @param {number | Pattern} timeline The timeline that the pattern should play on. + * @example + * n("<0 1 2 3>(3,8)") + * .sound("num") + * // resets the timeline every two cycles, by negating the timeline. + * // in a lot of cases this will be edited by a human live coder + * // rather than patterned! + * .timeline("<2 -2>".slow(2)) */ export const timeline = register( @@ -40,8 +48,7 @@ export const timeline = register( tpat = reify(tpat); const f = function (state) { // Is this called from the scheduler? (rather than from e.g. the visualiser) - const scheduler = !!state.controls._cyclist; - + const scheduler = !!state.controls.cyclist; const timehaps = tpat.query(state); const result = []; for (const timehap of timehaps) { From 2aeba2e53fa3a72b12b21b97300b5fc0a724a1a0 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 11 Dec 2025 11:37:20 -0600 Subject: [PATCH 110/194] Working version of bus --- packages/core/controls.mjs | 22 ++++++++++++++ packages/core/pattern.mjs | 32 ++++++++++---------- packages/superdough/superdough.mjs | 19 ++++++++---- packages/superdough/superdoughdata.mjs | 2 ++ packages/superdough/superdoughoutput.mjs | 24 ++++++++++++--- packages/superdough/synth.mjs | 37 +++++++++++++++++++++++- 6 files changed, 111 insertions(+), 25 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index bcbac625..6483880b 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2046,6 +2046,28 @@ export const { octave, oct } = registerControl('octave', 'oct'); * ) */ export const { orbit } = registerControl('orbit', 'o'); + +/** + * A `bus` is a send which can be used for mixing patterns. It combines with.. + * s("bus") to play that bus through another pattern (for, say, applying non-linear + * effects like distortion to multiple signals) + * + * otherPat.bmod(..) (to modulate another pattern with the bus) + * + * @name bus + * @param {number | Pattern} number + */ +export const { bus } = registerControl('bus'); + +/** + * Postgain multiplier prior to sending the signal to the audio bus. + * + * @name busgain + * @synonyms bgain + * @param {number | Pattern} number + */ +export const { busgain, bgain } = registerControl('busgain', 'bgain'); + // 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 diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 986ac9af..b0420a2e 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3743,18 +3743,18 @@ addConfigAlias('env', 'depthabs', 'da'); addConfigAlias('env', 'acurve', 'ac'); addConfigAlias('env', 'dcurve', 'dc'); addConfigAlias('env', 'rcurve', 'rc'); -addConfigAlias('omod', 'orbit', 'o'); -addConfigAlias('omod', 'target', 't'); -addConfigAlias('omod', 'depth', 'dep', 'dr'); -addConfigAlias('omod', 'depthabs', 'da'); -addConfigAlias('omod', 'dc'); +addConfigAlias('bmod', 'orbit', 'o'); +addConfigAlias('bmod', 'target', 't'); +addConfigAlias('bmod', 'depth', 'dep', 'dr'); +addConfigAlias('bmod', 'depthabs', 'da'); +addConfigAlias('bmod', 'dc'); Pattern.prototype.modulate = function (type, config, idx) { if (config == null || typeof config !== 'object') { return this; } - if (!['lfo', 'env', 'omod'].includes(type)) { - logger(`[core] Modulation type ${type} not found. Please use one of 'lfo', 'env', 'omod'`); + if (!['lfo', 'env', 'bmod'].includes(type)) { + logger(`[core] Modulation type ${type} not found. Please use one of 'lfo', 'env', 'bmod'`); return this; } let output = this; @@ -3828,13 +3828,15 @@ Pattern.prototype.env = function (config, idx) { export const env = (config) => pure({}).env(config); /** - * Modulates with the output from a given `orbit` - * Can be called in sequence like pat.obus(...).obus(...) to set up multiple modulators + * Modulates with the output from a given `bus`. + * Can be called in sequence like pat.bmod(...).bmod(...) to set up multiple modulators * - * @name omod + * Send to an audio bus with `otherPat.bus(..)`. + * + * @name bmod * @memberof Pattern - * @param {Object} config Orbit bus configuration. - * @param {string | Pattern} [config.orbit] Orbit to get modulation signal from + * @param {Object} config Bus modulation configuration. + * @param {string | Pattern} [config.bus] Bus to get modulation signal from * @param {string | Pattern} [config.target] Node (and parameter if specified like `lpf.frequency`) to modulate. Aliases: t * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da @@ -3842,7 +3844,7 @@ export const env = (config) => pure({}).env(config); * @param {number | Pattern} [config.dc] DC offset prior to application * @returns Pattern */ -Pattern.prototype.omod = function (config, idx) { - return this.modulate('omod', config, idx); +Pattern.prototype.bmod = function (config, idx) { + return this.modulate('bmod', config, idx); }; -export const omod = (config) => pure({}).omod(config); +export const bmod = (config) => pure({}).bmod(config); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index f32a1716..32d7f0d6 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -163,6 +163,7 @@ let defaultDefaultValues = { distortvol: 1, distorttype: 0, delay: 0, + busgain: 1, byteBeatExpression: '0', delayfeedback: 0.5, delaysync: 3 / 16, @@ -521,10 +522,10 @@ function connectEnvelope(idx, params, nodeTracker, value) { return envNode; } -function connectOrbitModulator(params, nodeTracker, value) { +function connectBusModulator(params, nodeTracker, value) { const ac = getAudioContext(); const { target, depth = 1, depthabs } = params; - const signal = controller.getOrbit(params.orbit).output; + const signal = controller.getBus(params.bus).output; const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 }); dc.start(params.begin); const shifted = dc.connect(gainNode(1)); @@ -628,6 +629,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) delaysync = getDefaultValue('delaysync'), delaytime, orbit = getDefaultValue('orbit'), + bus, + busgain = getDefaultValue('busgain'), room, roomfade, roomlp, @@ -666,6 +669,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) delay = applyGainCurve(delay); velocity = applyGainCurve(velocity); tremolodepth = applyGainCurve(tremolodepth); + busgain = applyGainCurve(busgain); gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future const end = t + hapDuration; @@ -970,6 +974,11 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) const reverbSend = orbitBus.sendReverb(post, room); audioNodes.push(reverbSend); } + if (bus != null) { + const busNode = audioController.getBus(bus); + const busSend = effectSend(post, busNode, busgain); + audioNodes.push(busSend); + } if (djf != null) { nodes['djf'] = orbitBus.getDjf(djf, t); @@ -1027,9 +1036,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) audioNodes.push(env); } } - if (value.omod) { - for (const p of value.omod) { - const { toCleanup } = connectOrbitModulator({ ...p, begin: t, end: endWithRelease }, nodes, value); + if (value.bmod) { + for (const p of value.bmod) { + const { toCleanup } = connectBusModulator({ ...p, begin: t, end: endWithRelease }, nodes, value); audioNodes.push(...toCleanup); } } diff --git a/packages/superdough/superdoughdata.mjs b/packages/superdough/superdoughdata.mjs index 1f3235e5..8284876e 100644 --- a/packages/superdough/superdoughdata.mjs +++ b/packages/superdough/superdoughdata.mjs @@ -72,6 +72,8 @@ const CONTROL_DATA = { // roomfade: { param: 'room.fade', min: 0, max: 1 }, roomlp: { param: 'room.lp', min: 20, max: 24000 }, djf: { param: 'djf.value', min: 0, max: 1 }, + busgain: { param: 'bus.gain', default: 1, min: 0, max: 10 }, + bgain: { param: 'bus.gain', default: 1, min: 0, max: 10 }, // SYNTHS detune: { param: 'source.detune', min: 0, max: 1 }, diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs index d8ead7d0..204eb62a 100644 --- a/packages/superdough/superdoughoutput.mjs +++ b/packages/superdough/superdoughoutput.mjs @@ -2,7 +2,10 @@ import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs'; import { errorLogger } from './logger.mjs'; import { clamp } from './util.mjs'; -let hasChanged = (now, before) => now !== undefined && now !== before; +const hasChanged = (now, before) => now !== undefined && now !== before; +// Node with fixed stereo channel count to prevent clicking when the input signal +// switches from mono to stereo +const getStereoNode = (ac) => new GainNode(ac, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); export class Orbit { reverbNode; @@ -11,10 +14,11 @@ export class Orbit { summingNode; djfNode; audioContext; + constructor(audioContext) { this.audioContext = audioContext; - this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); - this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); + this.output = getStereoNode(audioContext); + this.summingNode = getStereoNode(audioContext); this.summingNode.connect(this.output); } @@ -164,6 +168,7 @@ export class SuperdoughAudioController { audioContext; output; nodes = {}; + buses = {}; constructor(audioContext) { this.audioContext = audioContext; @@ -171,10 +176,14 @@ export class SuperdoughAudioController { } reset() { - Array.from(this.nodes).forEach((node) => { + Object.values(this.nodes).forEach((node) => { node.disconnect(); }); + Object.values(this.buses).forEach((bus) => { + bus.disconnect(); + }); this.nodes = {}; + this.buses = {}; this.output.reset(); } @@ -206,4 +215,11 @@ export class SuperdoughAudioController { } return this.nodes[orbitNum]; } + + getBus(busNum) { + if (this.buses[busNum] == null) { + this.buses[busNum] = getStereoNode(this.audioContext); + } + return this.buses[busNum]; + } } diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index cffbdc0c..3686e13d 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,5 +1,5 @@ import { clamp } from './util.mjs'; -import { registerSound, soundMap } from './superdough.mjs'; +import { getSuperdoughAudioController, registerSound, soundMap } from './superdough.mjs'; import { getAudioContext } from './audioContext.mjs'; import { applyFM, @@ -367,6 +367,41 @@ export function registerSynthSounds() { { prebake: true, type: 'synth' }, ); + registerSound( + 'bus', + (begin, value, onended) => { + const ac = getAudioContext(); + const [attack, decay, sustain, release] = getADSRValues( + [value.attack, value.decay, value.sustain, value.release], + 'linear', + [0.001, 0.05, 0.6, 0.01], + ); + const holdend = begin + value.duration; + const end = holdend + release + 0.01; + const bus = getSuperdoughAudioController().getBus(value.n ?? 0); + const envGain = bus.connect(gainNode(1)); + getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); + const timeoutNode = webAudioTimeout( + ac, + () => { + bus.disconnect(envGain); + onended(); + }, + begin, + end, + ); + + return { + node: envGain, + source: bus, + stop: (time) => { + timeoutNode.stop(time); + }, + }; + }, + { prebake: true, type: 'input' }, + ); + [...noises].forEach((s) => { registerSound( s, From 3a17aeabf3227e87d7a9126d7d886043a0ea56c7 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 11 Dec 2025 11:41:40 -0600 Subject: [PATCH 111/194] Some cleanup --- packages/core/controls.mjs | 9 --------- packages/superdough/worklets.mjs | 6 ++---- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 6483880b..808babe6 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2530,15 +2530,6 @@ export const { clip, legato } = registerControl('clip', 'legato'); */ export const { duration, dur } = registerControl('duration', 'dur'); -/** - * Sets the ID of the pattern for later reference - * - * @name id - * @param {number | Pattern} id ID of the pattern - * - */ -export const { id } = registerControl('id'); - // ZZFX export const { zrand } = registerControl('zrand'); export const { curve } = registerControl('curve'); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index bb33a24f..6e850b28 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -160,10 +160,8 @@ class LFOProcessor extends AudioWorkletProcessor { const dcoffset = parameters['dcoffset'][0]; - const userMin = parameters['min'][0]; - const userMax = parameters['max'][0]; - const min = Math.min(userMin, userMax); - const max = Math.max(userMin, userMax); + const min = parameters['min'][0]; + const max = parameters['max'][0]; const shape = waveShapeNames[parameters['shape'][0]]; const blockSize = output[0].length ?? 0; From 45aa1f5384263fbf52e2be20bbca1a13203de977 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 11 Dec 2025 12:52:40 -0600 Subject: [PATCH 112/194] Control cleanup --- packages/superdough/superdoughdata.mjs | 28 +++++++++++++++++--------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/superdough/superdoughdata.mjs b/packages/superdough/superdoughdata.mjs index 8284876e..fe9bfe46 100644 --- a/packages/superdough/superdoughdata.mjs +++ b/packages/superdough/superdoughdata.mjs @@ -30,15 +30,26 @@ const CONTROL_DATA = { // HPF hcutoff: { param: 'hpf.frequency', min: 20, max: 24000 }, hresonance: { param: 'hpf.Q', min: 0.1, max: 30 }, + hprate: { param: 'hpf_lfo.rate', min: 0, max: 40 }, + hpsync: { param: 'hpf_lfo.sync', min: 0, max: 8 }, + hpdepth: { param: 'hpf_lfo.depth', min: 20, max: 24000 }, + hpdepthfrequency: { param: 'hpf_lfo.depth', min: 20, max: 24000 }, + hpshape: { param: 'hpf_lfo.shape', min: 0, max: 4 }, + hpdc: { param: 'hpf_lfo.dcoffset', min: -1, max: 1 }, + hpskew: { param: 'hpf_lfo.skew', min: 0, max: 1 }, // BPF bandf: { param: 'bpf.frequency', min: 20, max: 24000 }, bandq: { param: 'bpf.Q', min: 0.1, max: 30 }, + bprate: { param: 'bpf_lfo.rate', min: 0, max: 40 }, + bpsync: { param: 'bpf_lfo.sync', min: 0, max: 8 }, + bpdepth: { param: 'bpf_lfo.depth', min: 20, max: 24000 }, + bpdepthfrequency: { param: 'bpf_lfo.depth', min: 20, max: 24000 }, + bpshape: { param: 'bpf_lfo.shape', min: 0, max: 4 }, + bpdc: { param: 'bpf_lfo.dcoffset', min: -1, max: 1 }, + bpskew: { param: 'bpf_lfo.skew', min: 0, max: 1 }, - // FILTERS - // fanchor: { param: ['lpf.anchor', 'hpf.anchor', 'bpf.anchor'], min: 0, max: 1 }, - // drive: { param: ['lpf.drive', 'hpf.drive', 'bpf.drive'], min: 0, max: 2 }, - // vowel: { param: 'vowel.frequency', min: 200, max: 4000 }, + vowel: { param: 'vowel.frequency', min: 200, max: 4000 }, // DISTORTION coarse: { param: 'coarse.coarse', min: 1, max: 64 }, @@ -47,7 +58,6 @@ const CONTROL_DATA = { shapevol: { param: 'shape.postgain', default: 1, min: 0, max: 1 }, distort: { param: 'distort.distort', min: 0, max: 5 }, distortvol: { param: 'distort.postgain', default: 1, min: 0, max: 1 }, - // distorttype: { param: 'distort.algorithm', default: 0, min: 0, max: 4 }, // COMPRESSOR compressor: { param: 'compressor.threshold', default: -3, min: -100, max: 0 }, @@ -63,17 +73,15 @@ const CONTROL_DATA = { phasercenter: { param: 'phaser.frequency', min: 20, max: 24000 }, // ORBIT EFFECTS - // delay: { param: 'delay.delayTime', default: 0, min: 0, max: 4 }, delaytime: { param: 'delay.delayTime', min: 0, max: 4 }, - // delayfeedback: { param: 'delay.feedback', default: 0.5, min: 0, max: 0.98 }, - // delaysync: { param: 'delay.sync', default: 3 / 16, min: 0, max: 2 }, + delayfeedback: { param: 'delay.feedback', default: 0.5, min: 0, max: 0.98 }, + delaysync: { param: 'delay.sync', default: 3 / 16, min: 0, max: 2 }, dry: { param: 'dry.gain', min: 0, max: 1 }, room: { param: 'room.wet', min: 0, max: 1 }, - // roomfade: { param: 'room.fade', min: 0, max: 1 }, + roomfade: { param: 'room.fade', min: 0, max: 1 }, roomlp: { param: 'room.lp', min: 20, max: 24000 }, djf: { param: 'djf.value', min: 0, max: 1 }, busgain: { param: 'bus.gain', default: 1, min: 0, max: 10 }, - bgain: { param: 'bus.gain', default: 1, min: 0, max: 10 }, // SYNTHS detune: { param: 'source.detune', min: 0, max: 1 }, From e7d1613bfb4978adb498d9ccff87475c584698f5 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 11 Dec 2025 14:42:16 -0600 Subject: [PATCH 113/194] Broken tests, but otherwise working --- packages/core/pattern.mjs | 21 ++-- packages/superdough/superdough.mjs | 113 +++++++------------ packages/superdough/superdoughdata.mjs | 144 +++++++++++++------------ packages/superdough/synth.mjs | 2 +- 4 files changed, 128 insertions(+), 152 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index b0420a2e..7d0e4613 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -26,6 +26,7 @@ import { } from './util.mjs'; import drawLine from './drawLine.mjs'; import { errorLogger, logger } from './logger.mjs'; +import { getControlName } from './controls.mjs'; let stringParser; @@ -3753,28 +3754,34 @@ Pattern.prototype.modulate = function (type, config, idx) { if (config == null || typeof config !== 'object') { return this; } - if (!['lfo', 'env', 'bmod'].includes(type)) { + const modulatorKeys = ['lfo', 'env', 'bmod']; + if (!modulatorKeys.includes(type)) { logger(`[core] Modulation type ${type} not found. Please use one of 'lfo', 'env', 'bmod'`); return this; } let output = this; - let target = config.target; + let defaultValue = {}; + let defaultSet = 'target' in config; for (const [rawKey, value] of Object.entries(config)) { const key = resolveConfigKey(type, rawKey); - if (key === 'target') continue; // we will set/default it below const valuePat = reify(value); output = output .fmap((v) => (c) => { - if (target == null) { + if (!defaultSet) { // default target to the control set just before this in the chain // e.g. pat.gain(0.5).lfo({..}) will be a gain-LFO - target = Object.keys(v).at(-1); + let control = getControlName(Object.keys(v).at(-1)); + if (modulatorKeys.includes(control)) { + control = `${control}${v[type].length - 1}`; + } + defaultValue = { target: control }; + defaultSet = true; } v[type] ??= []; const t = v[type]; idx ??= t.length; - t[idx] ??= { target }; // set target - t[idx][key] = c; + t[idx] ??= defaultValue; + t[idx][key] = key === 'target' ? getControlName(c) : c; return v; }) .appLeft(valuePat); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 32d7f0d6..7cbc58b9 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -25,7 +25,7 @@ import { errorLogger, logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; import { getAudioContext } from './audioContext.mjs'; import { SuperdoughAudioController } from './superdoughoutput.mjs'; -import { getSuperdoughControlData } from './superdoughdata.mjs'; +import { getSuperdoughControlTargets } from './superdoughdata.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; @@ -396,97 +396,60 @@ function _getNodeParams(node) { return Array.from(params); } -const targetToParamGuess = { - source: 'detune', - lpf: 'frequency', - bpf: 'frequency', - hpf: 'frequency', - distort: 'distort', - gain: 'gain', - vowel: 'frequency', - coarse: 'coarse', - crush: 'crush', - shape: 'shape', - compressor: 'threshold', - pan: 'pan', - phaser: 'rate', - post: 'gain', - delay: 'delayTime', - djf: 'value', - lfo: 'frequency', - env: 'depth', - send: 'depth', -}; - -const controlData = getSuperdoughControlData(); - -// TODO: We would like to use the aliases in `controls` here, but cannot as -// the modules are independent. We may want to inject that method here in situations -// where superdough/core are run in tandem -export let getControlName; -const _getMainName = (control) => getControlName?.(control) ?? control; +const controlTargets = getSuperdoughControlTargets(); +const _stripIndex = (control) => control?.replace(/\d+$/, ''); function _getControlData(control) { - return controlData[_getMainName(control)]; + return controlTargets[_stripIndex(control)]; } -function _getControlValue(control, value, data) { - const main = _getMainName(control); - return Number(value[main] ?? data.default); -} - -function _getControlClamp(data, currentValue) { - const { min, max } = data; - return { min: min - currentValue, max: max - currentValue }; -} - -function _getTargetParams(nodes, target) { - let param; - if (target.includes('.')) { - const split = target.split('.'); - target = split[0]; - param = split[1]; - } else { - const targetWithoutIndex = target.replace(/(\d+)$/, ''); - param = targetToParamGuess[targetWithoutIndex]; +function _getRangeForParam(paramName, targetParams, currentValue) { + if (paramName === 'frequency') { + const liveValue = targetParams?.[0]?.value ?? currentValue ?? 0; + return { min: 20 - liveValue, max: 24000 - liveValue }; } - const targetNodes = nodes[target]; + return { min: undefined, max: undefined }; +} + +function _getTargetParamsForControl(control, nodes, paramOverride) { + const targetInfo = _getControlData(control); + if (!targetInfo) { + errorLogger(`Could not find control data for target '${control}'`, 'superdough'); + return { targetParams: [], paramName: control }; + } + const paramName = paramOverride ?? targetInfo.param; + const nodeKey = nodes[control] ? control : targetInfo.node; + const targetNodes = nodes[nodeKey]; if (!targetNodes) { const keys = Object.keys(nodes); errorLogger( - `Could not connect to target '${target}' — it does not exist. Available targets: ${keys.join(', ')}`, + `Could not connect to target '${nodeKey}' — it does not exist. Available targets: ${keys.join(', ')}`, 'superdough', ); - return []; + return { targetParams: [], paramName }; } const audioParams = []; targetNodes.forEach((targetNode) => { - const targetParam = _getNodeParam(targetNode, param); + const targetParam = _getNodeParam(targetNode, paramName); if (!targetParam) { const available = _getNodeParams(targetNode); errorLogger( - `Could not connect to parameter '${param}' on '${target}'. Available parameters: ${available.join(', ')}`, + `Could not connect to parameter '${paramName}' on '${nodeKey}'. Available parameters: ${available.join(', ')}`, 'superdough', ); return; } audioParams.push(targetParam); }); - return audioParams; -} - -function _getTargetParamsForControl(control, nodes) { - const main = _getMainName(control); - const data = _getControlData(main); - const targetParams = _getTargetParams(nodes, data.param); - return { targetParams, data }; + return { targetParams: audioParams, paramName }; } function connectLFO(idx, params, nodeTracker, value) { - const { rate = 1, sync, cps, cycle, target, depth = 1, depthabs, ...filteredParams } = params; - const { targetParams, data } = _getTargetParamsForControl(target, nodeTracker); - const currentValue = _getControlValue(target, value, data); - const { min, max } = _getControlClamp(data, currentValue); + const { rate = 1, sync, cps, cycle, target = 'lfo', depth = 1, depthabs, param, p, ...filteredParams } = params; + const targetParam = param ?? p; + const { targetParams, paramName } = _getTargetParamsForControl(target, nodeTracker, targetParam); + const currentValue = targetParams[0].value; + const { min, max } = _getRangeForParam(paramName, targetParams, currentValue); const depthValue = depthabs != null ? depthabs : depth * currentValue; const modParams = { ...filteredParams, @@ -504,9 +467,9 @@ function connectLFO(idx, params, nodeTracker, value) { function connectEnvelope(idx, params, nodeTracker, value) { const { target, acurve, dcurve, rcurve, depth = 1, depthabs, ...filteredParams } = params; - const { targetParams, data } = _getTargetParamsForControl(target, nodeTracker); - const currentValue = _getControlValue(target, value, data); - const { min, max } = _getControlClamp(data, currentValue); + const { targetParams, paramName } = _getTargetParamsForControl(target, nodeTracker); + const currentValue = targetParams[0].value; + const { min, max } = _getRangeForParam(paramName, targetParams, currentValue); const depthValue = depthabs != null ? depthabs : depth * currentValue; const envNode = getEnvelope(getAudioContext(), { ...filteredParams, @@ -525,17 +488,17 @@ function connectEnvelope(idx, params, nodeTracker, value) { function connectBusModulator(params, nodeTracker, value) { const ac = getAudioContext(); const { target, depth = 1, depthabs } = params; - const signal = controller.getBus(params.bus).output; + const signal = controller.getBus(params.bus); const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 }); dc.start(params.begin); const shifted = dc.connect(gainNode(1)); signal.connect(shifted); - const { targetParams, data } = _getTargetParamsForControl(target, nodeTracker); - const currentValue = _getControlValue(target, value, data); - const { min, max } = _getControlClamp(data, currentValue); + const { targetParams, paramName } = _getTargetParamsForControl(target, nodeTracker); + const currentValue = targetParams[0].value; + const { min, max } = _getRangeForParam(paramName, targetParams, currentValue); const depthValue = depthabs != null ? depthabs : depth * currentValue; const maxAbsDepth = Math.min(Math.abs(min), Math.abs(max)); - const boundedDepth = Math.min(Math.abs(depthValue), maxAbsDepth); + const boundedDepth = Math.min(Math.abs(depthValue), maxAbsDepth) || Math.abs(depthValue); const modulator = shifted.connect(gainNode((Math.sign(depthValue) * boundedDepth) / 0.3)); webAudioTimeout( ac, diff --git a/packages/superdough/superdoughdata.mjs b/packages/superdough/superdoughdata.mjs index fe9bfe46..cc161b0e 100644 --- a/packages/superdough/superdoughdata.mjs +++ b/packages/superdough/superdoughdata.mjs @@ -4,92 +4,98 @@ Copyright (C) 2025 Strudel contributors - see . */ -const CONTROL_DATA = { - stretch: { param: 'stretch.pitchFactor', default: 1, min: -4, max: 100 }, - gain: { param: 'gain.gain', default: 0.8, min: 0, max: 10 }, - postgain: { param: 'post.gain', default: 1, min: 0, max: 10 }, - pan: { param: 'pan.pan', min: 0, max: 1 }, - tremolo: { param: 'tremolo.rate', min: 0, max: 40 }, - tremolosync: { param: 'tremolo.sync', min: 0, max: 8 }, - tremolodepth: { param: 'tremolo_gain.gain', default: 1, min: 0, max: 10 }, - tremoloskew: { param: 'tremolo.skew', min: 0, max: 1 }, - tremolophase: { param: 'tremolo.phase', default: 0, min: 0, max: 1 }, - tremoloshape: { param: 'tremolo.shape', min: 0, max: 4 }, +const CONTROL_TARGETS = { + stretch: { node: 'stretch', param: 'pitchFactor' }, + gain: { node: 'gain', param: 'gain' }, + postgain: { node: 'post', param: 'gain' }, + pan: { node: 'pan', param: 'pan' }, + tremolo: { node: 'tremolo', param: 'rate' }, + tremolosync: { node: 'tremolo', param: 'sync' }, + tremolodepth: { node: 'tremolo_gain', param: 'gain' }, + tremoloskew: { node: 'tremolo', param: 'skew' }, + tremolophase: { node: 'tremolo', param: 'phase' }, + tremoloshape: { node: 'tremolo', param: 'shape' }, + + // MODULATORS + lfo: { node: 'lfo', param: 'frequency' }, + env: { node: 'env', param: 'depth' }, + bmod: { node: 'bmod', param: 'depth' }, // LPF - cutoff: { param: 'lpf.frequency', min: 20, max: 24000 }, - resonance: { param: 'lpf.Q', min: 0.1, max: 30 }, - lprate: { param: 'lpf_lfo.rate', min: 0, max: 40 }, - lpsync: { param: 'lpf_lfo.sync', min: 0, max: 8 }, - lpdepth: { param: 'lpf_lfo.depth', min: 20, max: 24000 }, - lpdepthfrequency: { param: 'lpf_lfo.depth', min: 20, max: 24000 }, - lpshape: { param: 'lpf_lfo.shape', min: 0, max: 4 }, - lpdc: { param: 'lpf_lfo.dcoffset', min: -1, max: 1 }, - lpskew: { param: 'lpf_lfo.skew', min: 0, max: 1 }, + cutoff: { node: 'lpf', param: 'frequency' }, + resonance: { node: 'lpf', param: 'Q' }, + lprate: { node: 'lpf_lfo', param: 'rate' }, + lpsync: { node: 'lpf_lfo', param: 'sync' }, + lpdepth: { node: 'lpf_lfo', param: 'depth' }, + lpdepthfrequency: { node: 'lpf_lfo', param: 'depth' }, + lpshape: { node: 'lpf_lfo', param: 'shape' }, + lpdc: { node: 'lpf_lfo', param: 'dcoffset' }, + lpskew: { node: 'lpf_lfo', param: 'skew' }, // HPF - hcutoff: { param: 'hpf.frequency', min: 20, max: 24000 }, - hresonance: { param: 'hpf.Q', min: 0.1, max: 30 }, - hprate: { param: 'hpf_lfo.rate', min: 0, max: 40 }, - hpsync: { param: 'hpf_lfo.sync', min: 0, max: 8 }, - hpdepth: { param: 'hpf_lfo.depth', min: 20, max: 24000 }, - hpdepthfrequency: { param: 'hpf_lfo.depth', min: 20, max: 24000 }, - hpshape: { param: 'hpf_lfo.shape', min: 0, max: 4 }, - hpdc: { param: 'hpf_lfo.dcoffset', min: -1, max: 1 }, - hpskew: { param: 'hpf_lfo.skew', min: 0, max: 1 }, + hcutoff: { node: 'hpf', param: 'frequency' }, + hresonance: { node: 'hpf', param: 'Q' }, + hprate: { node: 'hpf_lfo', param: 'rate' }, + hpsync: { node: 'hpf_lfo', param: 'sync' }, + hpdepth: { node: 'hpf_lfo', param: 'depth' }, + hpdepthfrequency: { node: 'hpf_lfo', param: 'depth' }, + hpshape: { node: 'hpf_lfo', param: 'shape' }, + hpdc: { node: 'hpf_lfo', param: 'dcoffset' }, + hpskew: { node: 'hpf_lfo', param: 'skew' }, // BPF - bandf: { param: 'bpf.frequency', min: 20, max: 24000 }, - bandq: { param: 'bpf.Q', min: 0.1, max: 30 }, - bprate: { param: 'bpf_lfo.rate', min: 0, max: 40 }, - bpsync: { param: 'bpf_lfo.sync', min: 0, max: 8 }, - bpdepth: { param: 'bpf_lfo.depth', min: 20, max: 24000 }, - bpdepthfrequency: { param: 'bpf_lfo.depth', min: 20, max: 24000 }, - bpshape: { param: 'bpf_lfo.shape', min: 0, max: 4 }, - bpdc: { param: 'bpf_lfo.dcoffset', min: -1, max: 1 }, - bpskew: { param: 'bpf_lfo.skew', min: 0, max: 1 }, + bandf: { node: 'bpf', param: 'frequency' }, + bandq: { node: 'bpf', param: 'Q' }, + bprate: { node: 'bpf_lfo', param: 'rate' }, + bpsync: { node: 'bpf_lfo', param: 'sync' }, + bpdepth: { node: 'bpf_lfo', param: 'depth' }, + bpdepthfrequency: { node: 'bpf_lfo', param: 'depth' }, + bpshape: { node: 'bpf_lfo', param: 'shape' }, + bpdc: { node: 'bpf_lfo', param: 'dcoffset' }, + bpskew: { node: 'bpf_lfo', param: 'skew' }, - vowel: { param: 'vowel.frequency', min: 200, max: 4000 }, + vowel: { node: 'vowel', param: 'frequency' }, // DISTORTION - coarse: { param: 'coarse.coarse', min: 1, max: 64 }, - crush: { param: 'crush.crush', min: 1, max: 16 }, - shape: { param: 'shape.shape', min: -1, max: 0.999 }, - shapevol: { param: 'shape.postgain', default: 1, min: 0, max: 1 }, - distort: { param: 'distort.distort', min: 0, max: 5 }, - distortvol: { param: 'distort.postgain', default: 1, min: 0, max: 1 }, + coarse: { node: 'coarse', param: 'coarse' }, + crush: { node: 'crush', param: 'crush' }, + shape: { node: 'shape', param: 'shape' }, + shapevol: { node: 'shape', param: 'postgain' }, + distort: { node: 'distort', param: 'distort' }, + distortvol: { node: 'distort', param: 'postgain' }, // COMPRESSOR - compressor: { param: 'compressor.threshold', default: -3, min: -100, max: 0 }, - compressorRatio: { param: 'compressor.ratio', default: 10, min: 1, max: 20 }, - compressorKnee: { param: 'compressor.knee', default: 10, min: 0, max: 40 }, - compressorAttack: { param: 'compressor.attack', default: 0.005, min: 0, max: 1 }, - compressorRelease: { param: 'compressor.release', default: 0.05, min: 0, max: 2 }, + compressor: { node: 'compressor', param: 'threshold' }, + compressorRatio: { node: 'compressor', param: 'ratio' }, + compressorKnee: { node: 'compressor', param: 'knee' }, + compressorAttack: { node: 'compressor', param: 'attack' }, + compressorRelease: { node: 'compressor', param: 'release' }, // PHASER - phaserrate: { param: 'phaser.rate', min: 0, max: 40 }, - phaserdepth: { param: 'phaser.depth', default: 0.75, min: 0, max: 1 }, - phasersweep: { param: 'phaser.sweep', min: 0, max: 5000 }, - phasercenter: { param: 'phaser.frequency', min: 20, max: 24000 }, + phaserrate: { node: 'phaser', param: 'rate' }, + phaserdepth: { node: 'phaser', param: 'depth' }, + phasersweep: { node: 'phaser', param: 'sweep' }, + phasercenter: { node: 'phaser', param: 'frequency' }, // ORBIT EFFECTS - delaytime: { param: 'delay.delayTime', min: 0, max: 4 }, - delayfeedback: { param: 'delay.feedback', default: 0.5, min: 0, max: 0.98 }, - delaysync: { param: 'delay.sync', default: 3 / 16, min: 0, max: 2 }, - dry: { param: 'dry.gain', min: 0, max: 1 }, - room: { param: 'room.wet', min: 0, max: 1 }, - roomfade: { param: 'room.fade', min: 0, max: 1 }, - roomlp: { param: 'room.lp', min: 20, max: 24000 }, - djf: { param: 'djf.value', min: 0, max: 1 }, - busgain: { param: 'bus.gain', default: 1, min: 0, max: 10 }, + delaytime: { node: 'delay', param: 'delayTime' }, + delayfeedback: { node: 'delay', param: 'feedback' }, + delaysync: { node: 'delay', param: 'sync' }, + dry: { node: 'dry', param: 'gain' }, + room: { node: 'room', param: 'wet' }, + roomfade: { node: 'room', param: 'fade' }, + roomlp: { node: 'room', param: 'lp' }, + djf: { node: 'djf', param: 'value' }, + busgain: { node: 'bus', param: 'gain' }, // SYNTHS - detune: { param: 'source.detune', min: 0, max: 1 }, - wt: { param: 'source.position', min: 0, max: 1 }, - warp: { param: 'source.warp', min: 0, max: 1 }, - freq: { param: 'source.frequency', min: 20, max: 24000 }, + s: { node: 'source', param: 'frequency' }, + detune: { node: 'source', param: 'detune' }, + wt: { node: 'source', param: 'position' }, + warp: { node: 'source', param: 'warp' }, + freq: { node: 'source', param: 'frequency' }, }; -export function getSuperdoughControlData() { - return CONTROL_DATA; +export function getSuperdoughControlTargets() { + return CONTROL_TARGETS; } diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 3686e13d..35c0bd04 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -374,7 +374,7 @@ export function registerSynthSounds() { const [attack, decay, sustain, release] = getADSRValues( [value.attack, value.decay, value.sustain, value.release], 'linear', - [0.001, 0.05, 0.6, 0.01], + [0.001, 0.05, 1, 0.01], ); const holdend = begin + value.duration; const end = holdend + release + 0.01; From 4169b764c3ce68fc638d945dc918cd77ae36d6a6 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 11 Dec 2025 15:28:48 -0600 Subject: [PATCH 114/194] Simpler control handling --- packages/core/pattern.mjs | 31 +++++++++++++++++++----------- packages/superdough/superdough.mjs | 22 ++++++++++----------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 7d0e4613..04328ccf 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3724,8 +3724,8 @@ const resolveConfigKey = (funcName, key) => { return aliasMap.get(normalized) ?? key; }; -addConfigAlias('lfo', 'target', 't'); -addConfigAlias('lfo', 'param', 'p'); +addConfigAlias('lfo', 'control', 'c'); +addConfigAlias('lfo', 'subControl', 'sc'); addConfigAlias('lfo', 'rate', 'r'); addConfigAlias('lfo', 'depth', 'dep', 'dr'); addConfigAlias('lfo', 'depthabs', 'da'); @@ -3734,7 +3734,8 @@ addConfigAlias('lfo', 'shape', 'sh'); addConfigAlias('lfo', 'skew', 'sk'); addConfigAlias('lfo', 'curve', 'c'); addConfigAlias('lfo', 'sync', 's'); -addConfigAlias('env', 'target', 't'); +addConfigAlias('env', 'control', 'c'); +addConfigAlias('env', 'subControl', 'sc'); addConfigAlias('env', 'attack', 'att', 'a'); addConfigAlias('env', 'decay', 'dec', 'd'); addConfigAlias('env', 'sustain', 'sus', 's'); @@ -3745,7 +3746,8 @@ addConfigAlias('env', 'acurve', 'ac'); addConfigAlias('env', 'dcurve', 'dc'); addConfigAlias('env', 'rcurve', 'rc'); addConfigAlias('bmod', 'orbit', 'o'); -addConfigAlias('bmod', 'target', 't'); +addConfigAlias('bmod', 'control', 'c'); +addConfigAlias('bmod', 'subControl', 'sc'); addConfigAlias('bmod', 'depth', 'dep', 'dr'); addConfigAlias('bmod', 'depthabs', 'da'); addConfigAlias('bmod', 'dc'); @@ -3761,27 +3763,31 @@ Pattern.prototype.modulate = function (type, config, idx) { } let output = this; let defaultValue = {}; - let defaultSet = 'target' in config; + let defaultSet = 'control' in config; for (const [rawKey, value] of Object.entries(config)) { const key = resolveConfigKey(type, rawKey); const valuePat = reify(value); output = output .fmap((v) => (c) => { if (!defaultSet) { - // default target to the control set just before this in the chain + // default control to the control set just before this in the chain // e.g. pat.gain(0.5).lfo({..}) will be a gain-LFO let control = getControlName(Object.keys(v).at(-1)); if (modulatorKeys.includes(control)) { control = `${control}${v[type].length - 1}`; } - defaultValue = { target: control }; + defaultValue = { control }; defaultSet = true; } v[type] ??= []; const t = v[type]; idx ??= t.length; t[idx] ??= defaultValue; - t[idx][key] = key === 'target' ? getControlName(c) : c; + if (key === 'control' || key === 'subControl') { + t[idx][key] = getControlName(c); + } else { + t[idx][key] = c; + } return v; }) .appLeft(valuePat); @@ -3795,7 +3801,8 @@ Pattern.prototype.modulate = function (type, config, idx) { * @name lfo * @memberof Pattern * @param {Object} config LFO configuration. - * @param {string | Pattern} [config.target] Node (and parameter if specified like `lpf.frequency`) to modulate. Aliases: t + * @param {string | Pattern} [config.control] Node to modulate. Aliases: t + * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p * @param {number | Pattern} [config.rate] Modulation rate. Aliases: rate, r * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da @@ -3817,7 +3824,8 @@ export const lfo = (config) => pure({}).lfo(config); * @name env * @memberof Pattern * @param {Object} config Envelope configuration. - * @param {string | Pattern} [config.target] Node (and parameter if specified like `lpf.frequency`) to modulate. Aliases: t + * @param {string | Pattern} [config.control] Node to modulate. Aliases: t + * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da * @param {number | Pattern} [config.attack] Time to reach depth. Aliases: att, a @@ -3844,7 +3852,8 @@ export const env = (config) => pure({}).env(config); * @memberof Pattern * @param {Object} config Bus modulation configuration. * @param {string | Pattern} [config.bus] Bus to get modulation signal from - * @param {string | Pattern} [config.target] Node (and parameter if specified like `lpf.frequency`) to modulate. Aliases: t + * @param {string | Pattern} [config.control] Node to modulate. Aliases: t + * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da * @param {number | Pattern} [config.ratio] Modulation ratio. Aliases: rat diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 7cbc58b9..9efc5b3e 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -411,14 +411,15 @@ function _getRangeForParam(paramName, targetParams, currentValue) { return { min: undefined, max: undefined }; } -function _getTargetParamsForControl(control, nodes, paramOverride) { - const targetInfo = _getControlData(control); +function _getTargetParamsForControl(control, nodes, subControl) { + const lookupKey = subControl ? `${control}_${subControl}` : control; + const targetInfo = _getControlData(lookupKey) ?? _getControlData(control); if (!targetInfo) { errorLogger(`Could not find control data for target '${control}'`, 'superdough'); return { targetParams: [], paramName: control }; } - const paramName = paramOverride ?? targetInfo.param; - const nodeKey = nodes[control] ? control : targetInfo.node; + const paramName = targetInfo.param; + const nodeKey = nodes[targetInfo.node] ? targetInfo.node : control; const targetNodes = nodes[nodeKey]; if (!targetNodes) { const keys = Object.keys(nodes); @@ -445,9 +446,8 @@ function _getTargetParamsForControl(control, nodes, paramOverride) { } function connectLFO(idx, params, nodeTracker, value) { - const { rate = 1, sync, cps, cycle, target = 'lfo', depth = 1, depthabs, param, p, ...filteredParams } = params; - const targetParam = param ?? p; - const { targetParams, paramName } = _getTargetParamsForControl(target, nodeTracker, targetParam); + const { rate = 1, sync, cps, cycle, control = 'lfo', subControl, depth = 1, depthabs, ...filteredParams } = params; + const { targetParams, paramName } = _getTargetParamsForControl(control, nodeTracker, subControl); const currentValue = targetParams[0].value; const { min, max } = _getRangeForParam(paramName, targetParams, currentValue); const depthValue = depthabs != null ? depthabs : depth * currentValue; @@ -466,8 +466,8 @@ function connectLFO(idx, params, nodeTracker, value) { } function connectEnvelope(idx, params, nodeTracker, value) { - const { target, acurve, dcurve, rcurve, depth = 1, depthabs, ...filteredParams } = params; - const { targetParams, paramName } = _getTargetParamsForControl(target, nodeTracker); + const { control, subControl, acurve, dcurve, rcurve, depth = 1, depthabs, ...filteredParams } = params; + const { targetParams, paramName } = _getTargetParamsForControl(control, nodeTracker, subControl); const currentValue = targetParams[0].value; const { min, max } = _getRangeForParam(paramName, targetParams, currentValue); const depthValue = depthabs != null ? depthabs : depth * currentValue; @@ -487,13 +487,13 @@ function connectEnvelope(idx, params, nodeTracker, value) { function connectBusModulator(params, nodeTracker, value) { const ac = getAudioContext(); - const { target, depth = 1, depthabs } = params; + const { control, subControl, depth = 1, depthabs } = params; const signal = controller.getBus(params.bus); const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 }); dc.start(params.begin); const shifted = dc.connect(gainNode(1)); signal.connect(shifted); - const { targetParams, paramName } = _getTargetParamsForControl(target, nodeTracker); + const { targetParams, paramName } = _getTargetParamsForControl(control, nodeTracker, subControl); const currentValue = targetParams[0].value; const { min, max } = _getRangeForParam(paramName, targetParams, currentValue); const depthValue = depthabs != null ? depthabs : depth * currentValue; From 415b6d5f1d41399ca208ded08ce23212662dabc7 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 11 Dec 2025 15:48:45 -0600 Subject: [PATCH 115/194] Fix name collision and move modulators into controls to avoid circular import test failure --- packages/core/controls.mjs | 162 ++++++++++++++++++++++++++++++++++++- packages/core/pattern.mjs | 160 ------------------------------------ 2 files changed, 161 insertions(+), 161 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 808babe6..c4ae8b8d 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -4,7 +4,8 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern, register, reify } from './pattern.mjs'; +import { logger } from './logger.mjs'; +import { Pattern, pure, register, reify } from './pattern.mjs'; export function createParam(names) { let isMulti = Array.isArray(names); @@ -2808,3 +2809,162 @@ export const scrub = register( }, false, ); + +const configAliases = new Map(); +const addConfigAlias = (funcName, canonical, ...aliases) => { + const lowerFunc = String(funcName).toLowerCase(); + const aliasMap = configAliases.get(lowerFunc) ?? new Map(); + const allKeys = new Set([canonical, ...aliases]); + for (const alias of allKeys) { + aliasMap.set(String(alias).toLowerCase(), canonical); + } + configAliases.set(lowerFunc, aliasMap); +}; + +const resolveConfigKey = (funcName, key) => { + const aliasMap = configAliases.get(String(funcName).toLowerCase()); + if (!aliasMap) return key; + const normalized = String(key).toLowerCase(); + return aliasMap.get(normalized) ?? key; +}; + +addConfigAlias('lfo', 'control', 'c'); +addConfigAlias('lfo', 'subControl', 'sc'); +addConfigAlias('lfo', 'rate', 'r'); +addConfigAlias('lfo', 'depth', 'dep', 'dr'); +addConfigAlias('lfo', 'depthabs', 'da'); +addConfigAlias('lfo', 'dcoffset', 'dc'); +addConfigAlias('lfo', 'shape', 'sh'); +addConfigAlias('lfo', 'skew', 'sk'); +addConfigAlias('lfo', 'curve'); +addConfigAlias('lfo', 'sync', 's'); +addConfigAlias('env', 'control', 'c'); +addConfigAlias('env', 'subControl', 'sc'); +addConfigAlias('env', 'attack', 'att', 'a'); +addConfigAlias('env', 'decay', 'dec', 'd'); +addConfigAlias('env', 'sustain', 'sus', 's'); +addConfigAlias('env', 'release', 'rel', 'r'); +addConfigAlias('env', 'depth', 'dep', 'dr'); +addConfigAlias('env', 'depthabs', 'da'); +addConfigAlias('env', 'acurve', 'ac'); +addConfigAlias('env', 'dcurve', 'dc'); +addConfigAlias('env', 'rcurve', 'rc'); +addConfigAlias('bmod', 'orbit', 'o'); +addConfigAlias('bmod', 'control', 'c'); +addConfigAlias('bmod', 'subControl', 'sc'); +addConfigAlias('bmod', 'depth', 'dep', 'dr'); +addConfigAlias('bmod', 'depthabs', 'da'); +addConfigAlias('bmod', 'dc'); + +Pattern.prototype.modulate = function (type, config, idx) { + if (config == null || typeof config !== 'object') { + return this; + } + const modulatorKeys = ['lfo', 'env', 'bmod']; + if (!modulatorKeys.includes(type)) { + logger(`[core] Modulation type ${type} not found. Please use one of 'lfo', 'env', 'bmod'`); + return this; + } + let output = this; + let defaultValue = {}; + let defaultSet = 'control' in config; + for (const [rawKey, value] of Object.entries(config)) { + const key = resolveConfigKey(type, rawKey); + const valuePat = reify(value); + output = output + .fmap((v) => (c) => { + if (!defaultSet) { + // default control to the control set just before this in the chain + // e.g. pat.gain(0.5).lfo({..}) will be a gain-LFO + let control = getControlName(Object.keys(v).at(-1)); + if (modulatorKeys.includes(control)) { + control = `${control}${v[control].length - 1}`; + } + defaultValue = { control }; + defaultSet = true; + } + v[type] ??= []; + const t = v[type]; + idx ??= t.length; + t[idx] ??= defaultValue; + if (key === 'control' || key === 'subControl') { + t[idx][key] = getControlName(c); + } else { + t[idx][key] = c; + } + return v; + }) + .appLeft(valuePat); + } + return output; +}; + +/** + * Configures an LFO. Can be called in sequence like pat.lfo(...).lfo(...) to set up multiple LFOs + * + * @name lfo + * @memberof Pattern + * @param {Object} config LFO configuration. + * @param {string | Pattern} [config.control] Node to modulate. Aliases: t + * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p + * @param {number | Pattern} [config.rate] Modulation rate. Aliases: rate, r + * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr + * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da + * @param {number | Pattern} [config.dcoffset] DC offset / bias for the waveform. Aliases: dc + * @param {number | Pattern} [config.shape] Waveform shape index. Aliases: sh + * @param {number | Pattern} [config.skew] Waveform skew amount. Aliases: sk + * @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: c + * @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s + * @returns Pattern + */ +Pattern.prototype.lfo = function (config, idx) { + return this.modulate('lfo', config, idx); +}; +export const lfo = (config) => pure({}).lfo(config); + +/** + * Configures an envelope. Can be called in sequence like pat.env(...).env(...) to set up multiple envelopes + * + * @name env + * @memberof Pattern + * @param {Object} config Envelope configuration. + * @param {string | Pattern} [config.control] Node to modulate. Aliases: t + * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p + * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr + * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da + * @param {number | Pattern} [config.attack] Time to reach depth. Aliases: att, a + * @param {number | Pattern} [config.decay] Time to reach sustain. Aliases: dec, d + * @param {number | Pattern} [config.sustain] Sustain depth. Aliases: sus, s + * @param {number | Pattern} [config.release] Time to return to nominal value. Aliases: rel, r + * @param {number | Pattern} [config.acurve] Snappiness of attack curve (-1 = relaxed, 1 = snappy). Aliases: ac + * @param {number | Pattern} [config.dcurve] Snappiness of decay curve (-1 = relaxed, 1 = snappy). Aliases: dc + * @param {number | Pattern} [config.rcurve] Snappiness of release curve (-1 = relaxed, 1 = snappy). Aliases: rc + * @returns Pattern + */ +Pattern.prototype.env = function (config, idx) { + return this.modulate('env', config, idx); +}; +export const env = (config) => pure({}).env(config); + +/** + * Modulates with the output from a given `bus`. + * Can be called in sequence like pat.bmod(...).bmod(...) to set up multiple modulators + * + * Send to an audio bus with `otherPat.bus(..)`. + * + * @name bmod + * @memberof Pattern + * @param {Object} config Bus modulation configuration. + * @param {string | Pattern} [config.bus] Bus to get modulation signal from + * @param {string | Pattern} [config.control] Node to modulate. Aliases: t + * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p + * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr + * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da + * @param {number | Pattern} [config.ratio] Modulation ratio. Aliases: rat + * @param {number | Pattern} [config.dc] DC offset prior to application + * @returns Pattern + */ +Pattern.prototype.bmod = function (config, idx) { + return this.modulate('bmod', config, idx); +}; +export const bmod = (config) => pure({}).bmod(config); diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 04328ccf..c2e5ca1b 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -26,7 +26,6 @@ import { } from './util.mjs'; import drawLine from './drawLine.mjs'; import { errorLogger, logger } from './logger.mjs'; -import { getControlName } from './controls.mjs'; let stringParser; @@ -3705,162 +3704,3 @@ Pattern.prototype.phases = function (list) { export const phases = (list) => { return _ensureListPattern(list).as('phases'); }; - -const configAliases = new Map(); -const addConfigAlias = (funcName, canonical, ...aliases) => { - const lowerFunc = String(funcName).toLowerCase(); - const aliasMap = configAliases.get(lowerFunc) ?? new Map(); - const allKeys = new Set([canonical, ...aliases]); - for (const alias of allKeys) { - aliasMap.set(String(alias).toLowerCase(), canonical); - } - configAliases.set(lowerFunc, aliasMap); -}; - -const resolveConfigKey = (funcName, key) => { - const aliasMap = configAliases.get(String(funcName).toLowerCase()); - if (!aliasMap) return key; - const normalized = String(key).toLowerCase(); - return aliasMap.get(normalized) ?? key; -}; - -addConfigAlias('lfo', 'control', 'c'); -addConfigAlias('lfo', 'subControl', 'sc'); -addConfigAlias('lfo', 'rate', 'r'); -addConfigAlias('lfo', 'depth', 'dep', 'dr'); -addConfigAlias('lfo', 'depthabs', 'da'); -addConfigAlias('lfo', 'dcoffset', 'dc'); -addConfigAlias('lfo', 'shape', 'sh'); -addConfigAlias('lfo', 'skew', 'sk'); -addConfigAlias('lfo', 'curve', 'c'); -addConfigAlias('lfo', 'sync', 's'); -addConfigAlias('env', 'control', 'c'); -addConfigAlias('env', 'subControl', 'sc'); -addConfigAlias('env', 'attack', 'att', 'a'); -addConfigAlias('env', 'decay', 'dec', 'd'); -addConfigAlias('env', 'sustain', 'sus', 's'); -addConfigAlias('env', 'release', 'rel', 'r'); -addConfigAlias('env', 'depth', 'dep', 'dr'); -addConfigAlias('env', 'depthabs', 'da'); -addConfigAlias('env', 'acurve', 'ac'); -addConfigAlias('env', 'dcurve', 'dc'); -addConfigAlias('env', 'rcurve', 'rc'); -addConfigAlias('bmod', 'orbit', 'o'); -addConfigAlias('bmod', 'control', 'c'); -addConfigAlias('bmod', 'subControl', 'sc'); -addConfigAlias('bmod', 'depth', 'dep', 'dr'); -addConfigAlias('bmod', 'depthabs', 'da'); -addConfigAlias('bmod', 'dc'); - -Pattern.prototype.modulate = function (type, config, idx) { - if (config == null || typeof config !== 'object') { - return this; - } - const modulatorKeys = ['lfo', 'env', 'bmod']; - if (!modulatorKeys.includes(type)) { - logger(`[core] Modulation type ${type} not found. Please use one of 'lfo', 'env', 'bmod'`); - return this; - } - let output = this; - let defaultValue = {}; - let defaultSet = 'control' in config; - for (const [rawKey, value] of Object.entries(config)) { - const key = resolveConfigKey(type, rawKey); - const valuePat = reify(value); - output = output - .fmap((v) => (c) => { - if (!defaultSet) { - // default control to the control set just before this in the chain - // e.g. pat.gain(0.5).lfo({..}) will be a gain-LFO - let control = getControlName(Object.keys(v).at(-1)); - if (modulatorKeys.includes(control)) { - control = `${control}${v[type].length - 1}`; - } - defaultValue = { control }; - defaultSet = true; - } - v[type] ??= []; - const t = v[type]; - idx ??= t.length; - t[idx] ??= defaultValue; - if (key === 'control' || key === 'subControl') { - t[idx][key] = getControlName(c); - } else { - t[idx][key] = c; - } - return v; - }) - .appLeft(valuePat); - } - return output; -}; - -/** - * Configures an LFO. Can be called in sequence like pat.lfo(...).lfo(...) to set up multiple LFOs - * - * @name lfo - * @memberof Pattern - * @param {Object} config LFO configuration. - * @param {string | Pattern} [config.control] Node to modulate. Aliases: t - * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p - * @param {number | Pattern} [config.rate] Modulation rate. Aliases: rate, r - * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr - * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da - * @param {number | Pattern} [config.dcoffset] DC offset / bias for the waveform. Aliases: dc - * @param {number | Pattern} [config.shape] Waveform shape index. Aliases: sh - * @param {number | Pattern} [config.skew] Waveform skew amount. Aliases: sk - * @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: c - * @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s - * @returns Pattern - */ -Pattern.prototype.lfo = function (config, idx) { - return this.modulate('lfo', config, idx); -}; -export const lfo = (config) => pure({}).lfo(config); - -/** - * Configures an envelope. Can be called in sequence like pat.env(...).env(...) to set up multiple envelopes - * - * @name env - * @memberof Pattern - * @param {Object} config Envelope configuration. - * @param {string | Pattern} [config.control] Node to modulate. Aliases: t - * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p - * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr - * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da - * @param {number | Pattern} [config.attack] Time to reach depth. Aliases: att, a - * @param {number | Pattern} [config.decay] Time to reach sustain. Aliases: dec, d - * @param {number | Pattern} [config.sustain] Sustain depth. Aliases: sus, s - * @param {number | Pattern} [config.release] Time to return to nominal value. Aliases: rel, r - * @param {number | Pattern} [config.acurve] Snappiness of attack curve (-1 = relaxed, 1 = snappy). Aliases: ac - * @param {number | Pattern} [config.dcurve] Snappiness of decay curve (-1 = relaxed, 1 = snappy). Aliases: dc - * @param {number | Pattern} [config.rcurve] Snappiness of release curve (-1 = relaxed, 1 = snappy). Aliases: rc - * @returns Pattern - */ -Pattern.prototype.env = function (config, idx) { - return this.modulate('env', config, idx); -}; -export const env = (config) => pure({}).env(config); - -/** - * Modulates with the output from a given `bus`. - * Can be called in sequence like pat.bmod(...).bmod(...) to set up multiple modulators - * - * Send to an audio bus with `otherPat.bus(..)`. - * - * @name bmod - * @memberof Pattern - * @param {Object} config Bus modulation configuration. - * @param {string | Pattern} [config.bus] Bus to get modulation signal from - * @param {string | Pattern} [config.control] Node to modulate. Aliases: t - * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p - * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr - * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da - * @param {number | Pattern} [config.ratio] Modulation ratio. Aliases: rat - * @param {number | Pattern} [config.dc] DC offset prior to application - * @returns Pattern - */ -Pattern.prototype.bmod = function (config, idx) { - return this.modulate('bmod', config, idx); -}; -export const bmod = (config) => pure({}).bmod(config); From c6e0f0533e927a878339a9828c32d21ac9115a7c Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Dec 2025 23:32:10 +0000 Subject: [PATCH 116/194] add delta --- packages/core/signal.mjs | 9 ++++ test/__snapshots__/examples.test.mjs.snap | 53 +++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 62fc26ad..9ac76895 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -882,3 +882,12 @@ export const whenKey = register('whenKey', function (input, func, pat) { export const keyDown = register('keyDown', function (pat) { return pat.fmap(_keyDown); }); + +/** + * A pattern that gives the duration of events that are combined with it. + * @example + * sound("bd sd [bd bd] sd*4 [- sd] [bd [bd bd]]").note(delta.withValue(x => 1/x + 20)) + */ +export const delta = new Pattern(function (state) { + return [new Hap(undefined, state.span, state.span.duration)]; +}); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 09d285b5..17dcb08b 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -2784,6 +2784,59 @@ exports[`runs examples > example "delaysync" example index 0 1`] = ` ] `; +exports[`runs examples > example "delta" example index 0 1`] = ` +[ + "[ 0/1 → 1/6 | s:bd note:26 ]", + "[ 1/6 → 1/3 | s:sd note:26 ]", + "[ 1/3 → 5/12 | s:bd note:32 ]", + "[ 5/12 → 1/2 | s:bd note:32 ]", + "[ 1/2 → 13/24 | s:sd note:44 ]", + "[ 13/24 → 7/12 | s:sd note:44 ]", + "[ 7/12 → 5/8 | s:sd note:44 ]", + "[ 5/8 → 2/3 | s:sd note:44 ]", + "[ 3/4 → 5/6 | s:sd note:32 ]", + "[ 5/6 → 11/12 | s:bd note:32 ]", + "[ 11/12 → 23/24 | s:bd note:44 ]", + "[ 23/24 → 1/1 | s:bd note:44 ]", + "[ 1/1 → 7/6 | s:bd note:26 ]", + "[ 7/6 → 4/3 | s:sd note:26 ]", + "[ 4/3 → 17/12 | s:bd note:32 ]", + "[ 17/12 → 3/2 | s:bd note:32 ]", + "[ 3/2 → 37/24 | s:sd note:44 ]", + "[ 37/24 → 19/12 | s:sd note:44 ]", + "[ 19/12 → 13/8 | s:sd note:44 ]", + "[ 13/8 → 5/3 | s:sd note:44 ]", + "[ 7/4 → 11/6 | s:sd note:32 ]", + "[ 11/6 → 23/12 | s:bd note:32 ]", + "[ 23/12 → 47/24 | s:bd note:44 ]", + "[ 47/24 → 2/1 | s:bd note:44 ]", + "[ 2/1 → 13/6 | s:bd note:26 ]", + "[ 13/6 → 7/3 | s:sd note:26 ]", + "[ 7/3 → 29/12 | s:bd note:32 ]", + "[ 29/12 → 5/2 | s:bd note:32 ]", + "[ 5/2 → 61/24 | s:sd note:44 ]", + "[ 61/24 → 31/12 | s:sd note:44 ]", + "[ 31/12 → 21/8 | s:sd note:44 ]", + "[ 21/8 → 8/3 | s:sd note:44 ]", + "[ 11/4 → 17/6 | s:sd note:32 ]", + "[ 17/6 → 35/12 | s:bd note:32 ]", + "[ 35/12 → 71/24 | s:bd note:44 ]", + "[ 71/24 → 3/1 | s:bd note:44 ]", + "[ 3/1 → 19/6 | s:bd note:26 ]", + "[ 19/6 → 10/3 | s:sd note:26 ]", + "[ 10/3 → 41/12 | s:bd note:32 ]", + "[ 41/12 → 7/2 | s:bd note:32 ]", + "[ 7/2 → 85/24 | s:sd note:44 ]", + "[ 85/24 → 43/12 | s:sd note:44 ]", + "[ 43/12 → 29/8 | s:sd note:44 ]", + "[ 29/8 → 11/3 | s:sd note:44 ]", + "[ 15/4 → 23/6 | s:sd note:32 ]", + "[ 23/6 → 47/12 | s:bd note:32 ]", + "[ 47/12 → 95/24 | s:bd note:44 ]", + "[ 95/24 → 4/1 | s:bd note:44 ]", +] +`; + exports[`runs examples > example "density" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:crackle density:0.01 ]", From 8e1fba04363cdcf39c946c8fb10a21554ef42cb7 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 12 Dec 2025 15:45:36 +0000 Subject: [PATCH 117/194] rename delta to loose, add inverse tight, and exponential tightx --- packages/core/signal.mjs | 41 ++++++++++++++++++++++++++--- packages/core/test/pattern.test.mjs | 14 ---------- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 9ac76895..ff94c3b8 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -884,10 +884,45 @@ export const keyDown = register('keyDown', function (pat) { }); /** - * A pattern that gives the duration of events that are combined with it. + * A pattern giving the 'looseness' of events, or in other words, the duration of pattern events, + * in cycles per event. `loose` doesn't have structure itself, but takes structure, and therefore + * event durations, from the pattern that it is combined with. + * For example `loose.struct("1 1 [1 1] 1")` would give the same as `"0.25 0.25 [0.125 0.125] 0.25"`. + * See also its inverse, `tight`. * @example - * sound("bd sd [bd bd] sd*4 [- sd] [bd [bd bd]]").note(delta.withValue(x => 1/x + 20)) + * // Shorter events are lower in pitch + * sound("saw saw [saw saw] saw") + * .note(loose.range(50, 100)) + * @example + * sound("bd sd [bd bd] sd*4 [- sd] [bd [bd bd]]") + * .note(tight.add(20)) */ -export const delta = new Pattern(function (state) { +export const loose = new Pattern(function (state) { return [new Hap(undefined, state.span, state.span.duration)]; }); + +/** + * A pattern giving the 'tightness' of events, or in other words, the duration of pattern events, + * in events per cycle. `tight` doesn't have structure itself, but takes structure, and therefore + * event durations, from the pattern that it is combined with. + * For example `tight.struct("1 1 [1 1] 1")` would give the same as `"4 4 [8 8] 4"`. + * See also its inverse, `loose`. + * @example + * // Shorter events are more distorted + * n("0 0*2 0 0*2 0 [0 0 0]@2").sound("bd") + * .distort(tight.div(2)) + */ +export const tight = new Pattern(function (state) { + return [new Hap(undefined, state.span, Fraction(1).div(state.span.duration))]; +}); + +/** + * Like `tight` but gives the tightness of events according to an exponential curve. In + * particular, where the event tightness doubles (i.e. where an event duration halves), the + * returned value increases by one. `tightx.struct("1 1 1 [1 [1 1]]")` would therefore be + * the same as `"3 3 3 [4 [5 5]]"`. + */ +export const tightx = new Pattern(function (state) { + const n = Fraction(1).div(state.span.duration); + return [new Hap(undefined, state.span, Math.log(n) / Math.log(2) + 1)]; +}); diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 408ff274..aaa14bd2 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -740,20 +740,6 @@ describe('Pattern', () => { ); }); }); - describe('signal()', () => { - it('Can make saw/saw2', () => { - expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual( - sequence(0, 1 / 4, 1 / 2, 3 / 4).firstCycle(), - ); - - expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle()); - }); - it('Can make isaw/isaw2', () => { - expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle()); - - expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle()); - }); - }); describe('_setContext()', () => { it('Can set the hap context', () => { expect( From cd071cb7c062909a850b2e60041f6c8057f47d5a Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 12 Dec 2025 15:46:04 +0000 Subject: [PATCH 118/194] separate out signal tests, add tests for tight, tightx, loose --- packages/core/test/signal.test.mjs | 61 ++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 packages/core/test/signal.test.mjs diff --git a/packages/core/test/signal.test.mjs b/packages/core/test/signal.test.mjs new file mode 100644 index 00000000..ee0ec9c7 --- /dev/null +++ b/packages/core/test/signal.test.mjs @@ -0,0 +1,61 @@ +/* +signal.test.mjs - +Copyright (C) 2022 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 Fraction from 'fraction.js'; + +import { describe, it, expect, vi } from 'vitest'; + +import { saw, saw2, isaw, isaw2, tight, tightx, loose } from '../signal.mjs'; +import { fastcat, sequence } from '../pattern.mjs'; + +const st = (begin, end) => new State(ts(begin, end)); +const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end)); +const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context); + +const third = Fraction(1, 3); +const twothirds = Fraction(2, 3); + +const sameFirst = (a, b) => { + return expect(a.sortHapsByPart().firstCycle()).toStrictEqual(b.sortHapsByPart().firstCycle()); +}; + +describe('signal()', () => { + it('Can make saw/saw2', () => { + expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual( + sequence(0, 1 / 4, 1 / 2, 3 / 4).firstCycle(), + ); + + expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle()); + }); + it('Can make isaw/isaw2', () => { + expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle()); + + expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle()); + }); +}); + +describe('loose', () => { + it('gives cycles per hap', () => { + sameFirst( + loose.struct(true, true, true, fastcat(true, true)), + sequence(0.25, 0.25, 0.25, fastcat(0.125, 0.125)).fmap(Fraction), + ); + }); +}); +describe('tight', () => { + it('gives haps per cycle', () => { + sameFirst(tight.struct(true, true, true, fastcat(true, true)), sequence(4, 4, 4, fastcat(8, 8)).fmap(Fraction)); + }); +}); + +describe('tightx', () => { + it('gives exponential haps per cycle', () => { + sameFirst( + tightx.struct(true, true, true, fastcat(true, fastcat(true, true))), + sequence(3, 3, 3, fastcat(4, fastcat(5, 5))), + ); + }); +}); From 99e4999e6a7cd98979aa9bb93864c21b06a84cce Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 12 Dec 2025 15:46:19 +0000 Subject: [PATCH 119/194] snapshot --- test/__snapshots__/examples.test.mjs.snap | 176 +++++++++++++++------- 1 file changed, 123 insertions(+), 53 deletions(-) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 17dcb08b..29237fdc 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -2784,59 +2784,6 @@ exports[`runs examples > example "delaysync" example index 0 1`] = ` ] `; -exports[`runs examples > example "delta" example index 0 1`] = ` -[ - "[ 0/1 → 1/6 | s:bd note:26 ]", - "[ 1/6 → 1/3 | s:sd note:26 ]", - "[ 1/3 → 5/12 | s:bd note:32 ]", - "[ 5/12 → 1/2 | s:bd note:32 ]", - "[ 1/2 → 13/24 | s:sd note:44 ]", - "[ 13/24 → 7/12 | s:sd note:44 ]", - "[ 7/12 → 5/8 | s:sd note:44 ]", - "[ 5/8 → 2/3 | s:sd note:44 ]", - "[ 3/4 → 5/6 | s:sd note:32 ]", - "[ 5/6 → 11/12 | s:bd note:32 ]", - "[ 11/12 → 23/24 | s:bd note:44 ]", - "[ 23/24 → 1/1 | s:bd note:44 ]", - "[ 1/1 → 7/6 | s:bd note:26 ]", - "[ 7/6 → 4/3 | s:sd note:26 ]", - "[ 4/3 → 17/12 | s:bd note:32 ]", - "[ 17/12 → 3/2 | s:bd note:32 ]", - "[ 3/2 → 37/24 | s:sd note:44 ]", - "[ 37/24 → 19/12 | s:sd note:44 ]", - "[ 19/12 → 13/8 | s:sd note:44 ]", - "[ 13/8 → 5/3 | s:sd note:44 ]", - "[ 7/4 → 11/6 | s:sd note:32 ]", - "[ 11/6 → 23/12 | s:bd note:32 ]", - "[ 23/12 → 47/24 | s:bd note:44 ]", - "[ 47/24 → 2/1 | s:bd note:44 ]", - "[ 2/1 → 13/6 | s:bd note:26 ]", - "[ 13/6 → 7/3 | s:sd note:26 ]", - "[ 7/3 → 29/12 | s:bd note:32 ]", - "[ 29/12 → 5/2 | s:bd note:32 ]", - "[ 5/2 → 61/24 | s:sd note:44 ]", - "[ 61/24 → 31/12 | s:sd note:44 ]", - "[ 31/12 → 21/8 | s:sd note:44 ]", - "[ 21/8 → 8/3 | s:sd note:44 ]", - "[ 11/4 → 17/6 | s:sd note:32 ]", - "[ 17/6 → 35/12 | s:bd note:32 ]", - "[ 35/12 → 71/24 | s:bd note:44 ]", - "[ 71/24 → 3/1 | s:bd note:44 ]", - "[ 3/1 → 19/6 | s:bd note:26 ]", - "[ 19/6 → 10/3 | s:sd note:26 ]", - "[ 10/3 → 41/12 | s:bd note:32 ]", - "[ 41/12 → 7/2 | s:bd note:32 ]", - "[ 7/2 → 85/24 | s:sd note:44 ]", - "[ 85/24 → 43/12 | s:sd note:44 ]", - "[ 43/12 → 29/8 | s:sd note:44 ]", - "[ 29/8 → 11/3 | s:sd note:44 ]", - "[ 15/4 → 23/6 | s:sd note:32 ]", - "[ 23/6 → 47/12 | s:bd note:32 ]", - "[ 47/12 → 95/24 | s:bd note:44 ]", - "[ 95/24 → 4/1 | s:bd note:44 ]", -] -`; - exports[`runs examples > example "density" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:crackle density:0.01 ]", @@ -6219,6 +6166,84 @@ exports[`runs examples > example "loopEnd" example index 0 1`] = ` ] `; +exports[`runs examples > example "loose" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:saw note:62.5 ]", + "[ 1/4 → 1/2 | s:saw note:62.5 ]", + "[ 1/2 → 5/8 | s:saw note:56.25 ]", + "[ 5/8 → 3/4 | s:saw note:56.25 ]", + "[ 3/4 → 1/1 | s:saw note:62.5 ]", + "[ 1/1 → 5/4 | s:saw note:62.5 ]", + "[ 5/4 → 3/2 | s:saw note:62.5 ]", + "[ 3/2 → 13/8 | s:saw note:56.25 ]", + "[ 13/8 → 7/4 | s:saw note:56.25 ]", + "[ 7/4 → 2/1 | s:saw note:62.5 ]", + "[ 2/1 → 9/4 | s:saw note:62.5 ]", + "[ 9/4 → 5/2 | s:saw note:62.5 ]", + "[ 5/2 → 21/8 | s:saw note:56.25 ]", + "[ 21/8 → 11/4 | s:saw note:56.25 ]", + "[ 11/4 → 3/1 | s:saw note:62.5 ]", + "[ 3/1 → 13/4 | s:saw note:62.5 ]", + "[ 13/4 → 7/2 | s:saw note:62.5 ]", + "[ 7/2 → 29/8 | s:saw note:56.25 ]", + "[ 29/8 → 15/4 | s:saw note:56.25 ]", + "[ 15/4 → 4/1 | s:saw note:62.5 ]", +] +`; + +exports[`runs examples > example "loose" example index 1 1`] = ` +[ + "[ 0/1 → 1/6 | s:bd note:26 ]", + "[ 1/6 → 1/3 | s:sd note:26 ]", + "[ 1/3 → 5/12 | s:bd note:32 ]", + "[ 5/12 → 1/2 | s:bd note:32 ]", + "[ 1/2 → 13/24 | s:sd note:44 ]", + "[ 13/24 → 7/12 | s:sd note:44 ]", + "[ 7/12 → 5/8 | s:sd note:44 ]", + "[ 5/8 → 2/3 | s:sd note:44 ]", + "[ 3/4 → 5/6 | s:sd note:32 ]", + "[ 5/6 → 11/12 | s:bd note:32 ]", + "[ 11/12 → 23/24 | s:bd note:44 ]", + "[ 23/24 → 1/1 | s:bd note:44 ]", + "[ 1/1 → 7/6 | s:bd note:26 ]", + "[ 7/6 → 4/3 | s:sd note:26 ]", + "[ 4/3 → 17/12 | s:bd note:32 ]", + "[ 17/12 → 3/2 | s:bd note:32 ]", + "[ 3/2 → 37/24 | s:sd note:44 ]", + "[ 37/24 → 19/12 | s:sd note:44 ]", + "[ 19/12 → 13/8 | s:sd note:44 ]", + "[ 13/8 → 5/3 | s:sd note:44 ]", + "[ 7/4 → 11/6 | s:sd note:32 ]", + "[ 11/6 → 23/12 | s:bd note:32 ]", + "[ 23/12 → 47/24 | s:bd note:44 ]", + "[ 47/24 → 2/1 | s:bd note:44 ]", + "[ 2/1 → 13/6 | s:bd note:26 ]", + "[ 13/6 → 7/3 | s:sd note:26 ]", + "[ 7/3 → 29/12 | s:bd note:32 ]", + "[ 29/12 → 5/2 | s:bd note:32 ]", + "[ 5/2 → 61/24 | s:sd note:44 ]", + "[ 61/24 → 31/12 | s:sd note:44 ]", + "[ 31/12 → 21/8 | s:sd note:44 ]", + "[ 21/8 → 8/3 | s:sd note:44 ]", + "[ 11/4 → 17/6 | s:sd note:32 ]", + "[ 17/6 → 35/12 | s:bd note:32 ]", + "[ 35/12 → 71/24 | s:bd note:44 ]", + "[ 71/24 → 3/1 | s:bd note:44 ]", + "[ 3/1 → 19/6 | s:bd note:26 ]", + "[ 19/6 → 10/3 | s:sd note:26 ]", + "[ 10/3 → 41/12 | s:bd note:32 ]", + "[ 41/12 → 7/2 | s:bd note:32 ]", + "[ 7/2 → 85/24 | s:sd note:44 ]", + "[ 85/24 → 43/12 | s:sd note:44 ]", + "[ 43/12 → 29/8 | s:sd note:44 ]", + "[ 29/8 → 11/3 | s:sd note:44 ]", + "[ 15/4 → 23/6 | s:sd note:32 ]", + "[ 23/6 → 47/12 | s:bd note:32 ]", + "[ 47/12 → 95/24 | s:bd note:44 ]", + "[ 95/24 → 4/1 | s:bd note:44 ]", +] +`; + exports[`runs examples > example "lpattack" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c2 s:sawtooth cutoff:300 lpattack:0.5 lpenv:4 ]", @@ -12041,6 +12066,51 @@ exports[`runs examples > example "take" example index 2 1`] = ` ] `; +exports[`runs examples > example "tight" example index 0 1`] = ` +[ + "[ 0/1 → 1/7 | n:0 s:bd distort:3.5 ]", + "[ 1/7 → 3/14 | n:0 s:bd distort:7 ]", + "[ 3/14 → 2/7 | n:0 s:bd distort:7 ]", + "[ 2/7 → 3/7 | n:0 s:bd distort:3.5 ]", + "[ 3/7 → 1/2 | n:0 s:bd distort:7 ]", + "[ 1/2 → 4/7 | n:0 s:bd distort:7 ]", + "[ 4/7 → 5/7 | n:0 s:bd distort:3.5 ]", + "[ 5/7 → 17/21 | n:0 s:bd distort:5.25 ]", + "[ 17/21 → 19/21 | n:0 s:bd distort:5.25 ]", + "[ 19/21 → 1/1 | n:0 s:bd distort:5.25 ]", + "[ 1/1 → 8/7 | n:0 s:bd distort:3.5 ]", + "[ 8/7 → 17/14 | n:0 s:bd distort:7 ]", + "[ 17/14 → 9/7 | n:0 s:bd distort:7 ]", + "[ 9/7 → 10/7 | n:0 s:bd distort:3.5 ]", + "[ 10/7 → 3/2 | n:0 s:bd distort:7 ]", + "[ 3/2 → 11/7 | n:0 s:bd distort:7 ]", + "[ 11/7 → 12/7 | n:0 s:bd distort:3.5 ]", + "[ 12/7 → 38/21 | n:0 s:bd distort:5.25 ]", + "[ 38/21 → 40/21 | n:0 s:bd distort:5.25 ]", + "[ 40/21 → 2/1 | n:0 s:bd distort:5.25 ]", + "[ 2/1 → 15/7 | n:0 s:bd distort:3.5 ]", + "[ 15/7 → 31/14 | n:0 s:bd distort:7 ]", + "[ 31/14 → 16/7 | n:0 s:bd distort:7 ]", + "[ 16/7 → 17/7 | n:0 s:bd distort:3.5 ]", + "[ 17/7 → 5/2 | n:0 s:bd distort:7 ]", + "[ 5/2 → 18/7 | n:0 s:bd distort:7 ]", + "[ 18/7 → 19/7 | n:0 s:bd distort:3.5 ]", + "[ 19/7 → 59/21 | n:0 s:bd distort:5.25 ]", + "[ 59/21 → 61/21 | n:0 s:bd distort:5.25 ]", + "[ 61/21 → 3/1 | n:0 s:bd distort:5.25 ]", + "[ 3/1 → 22/7 | n:0 s:bd distort:3.5 ]", + "[ 22/7 → 45/14 | n:0 s:bd distort:7 ]", + "[ 45/14 → 23/7 | n:0 s:bd distort:7 ]", + "[ 23/7 → 24/7 | n:0 s:bd distort:3.5 ]", + "[ 24/7 → 7/2 | n:0 s:bd distort:7 ]", + "[ 7/2 → 25/7 | n:0 s:bd distort:7 ]", + "[ 25/7 → 26/7 | n:0 s:bd distort:3.5 ]", + "[ 26/7 → 80/21 | n:0 s:bd distort:5.25 ]", + "[ 80/21 → 82/21 | n:0 s:bd distort:5.25 ]", + "[ 82/21 → 4/1 | n:0 s:bd distort:5.25 ]", +] +`; + exports[`runs examples > example "tour" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:e s:folkharp ]", From a53385aa116b5fedf03619acc78d43a61b24a483 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 12 Dec 2025 16:50:24 +0000 Subject: [PATCH 120/194] delint --- packages/core/test/signal.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/signal.test.mjs b/packages/core/test/signal.test.mjs index ee0ec9c7..6036b61d 100644 --- a/packages/core/test/signal.test.mjs +++ b/packages/core/test/signal.test.mjs @@ -9,7 +9,7 @@ import Fraction from 'fraction.js'; import { describe, it, expect, vi } from 'vitest'; import { saw, saw2, isaw, isaw2, tight, tightx, loose } from '../signal.mjs'; -import { fastcat, sequence } from '../pattern.mjs'; +import { fastcat, sequence, State, TimeSpan, Hap } from '../index.mjs'; const st = (begin, end) => new State(ts(begin, end)); const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end)); From c8b2e1ac9e529333f72cb4ac8e87fe80cdf82a6e Mon Sep 17 00:00:00 2001 From: 1d10t <1d10t@noreply.codeberg.org> Date: Fri, 12 Dec 2025 21:22:21 +0100 Subject: [PATCH 121/194] feat(superdough/audiocontext): make AudioContext injection optional - Modify `setDefaultAudioContext()` to accept an optional `existingAudioCtx` parameter. - If provided, it will use the existing context; otherwise, it creates a new one. - This allows for better integration with external audio systems that manage their own AudioContext. --- packages/superdough/audioContext.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/superdough/audioContext.mjs b/packages/superdough/audioContext.mjs index 71e01d57..9bae75bd 100644 --- a/packages/superdough/audioContext.mjs +++ b/packages/superdough/audioContext.mjs @@ -1,7 +1,7 @@ let audioContext; -export const setDefaultAudioContext = () => { - audioContext = new AudioContext(); +export const setDefaultAudioContext = (existingAudioCtx) => { + audioContext = existingAudioCtx ?? new AudioContext(); return audioContext; }; From 4e14718e93b7d9a5ebe7fb1892109745577b8bc7 Mon Sep 17 00:00:00 2001 From: 1d10t <1d10t@noreply.codeberg.org> Date: Fri, 12 Dec 2025 21:56:31 +0100 Subject: [PATCH 122/194] feat(webaudio): enable AudioContext sharing for webaudioRepl - Add support for optional `existingAudioCtx` parameter in `webaudioRepl()` function - When provided, sets it as the default AudioContext via `setDefaultAudioContext()` - Allows integration with external systems that manage their own Web Audio API context - Maintains backward compatibility with existing usage patterns --- packages/webaudio/webaudio.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 383e87f8..8fc9b860 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import * as strudel from '@strudel/core'; -import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough'; +import { superdough, setDefaultAudioContext, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; @@ -27,6 +27,9 @@ export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => { }; export function webaudioRepl(options = {}) { + if (options.existingAudioCtx) { + setDefaultAudioContext(options.existingAudioCtx); + } options = { getTime: () => getAudioContext().currentTime, defaultOutput: webaudioOutput, From adf9596d070eb33ee8c52726cbd10fef7dea0141 Mon Sep 17 00:00:00 2001 From: Sergey S Yaglov Date: Sat, 13 Dec 2025 00:28:13 +0300 Subject: [PATCH 123/194] codestyle(webaudio) --- packages/webaudio/webaudio.mjs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 8fc9b860..399e68ab 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -5,7 +5,14 @@ This program is free software: you can redistribute it and/or modify it under th */ import * as strudel from '@strudel/core'; -import { superdough, setDefaultAudioContext, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough'; +import { + superdough, + setDefaultAudioContext, + getAudioContext, + setLogger, + doughTrigger, + registerWorklet, +} from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; From 8eeebf1e4f31be65508260d791aed0fbd21ccdd5 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 13 Dec 2025 16:30:51 +0000 Subject: [PATCH 124/194] try renaming to per / perx / cyclesPer --- packages/core/signal.mjs | 39 ++-- packages/core/test/signal.test.mjs | 14 +- test/__snapshots__/examples.test.mjs.snap | 246 +++++++++++----------- 3 files changed, 151 insertions(+), 148 deletions(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index ff94c3b8..fa450c67 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -884,45 +884,48 @@ export const keyDown = register('keyDown', function (pat) { }); /** - * A pattern giving the 'looseness' of events, or in other words, the duration of pattern events, - * in cycles per event. `loose` doesn't have structure itself, but takes structure, and therefore + * A pattern measuring the duration of events, + * in cycles per event. `cyclesPer` doesn't have structure itself, but takes structure, and therefore * event durations, from the pattern that it is combined with. - * For example `loose.struct("1 1 [1 1] 1")` would give the same as `"0.25 0.25 [0.125 0.125] 0.25"`. - * See also its inverse, `tight`. + * For example `cyclesPer.struct("1 1 [1 1] 1")` would give the same as `"0.25 0.25 [0.125 0.125] 0.25"`. + * See also its reciprocal, `per`, also known as `perCycle`. * @example * // Shorter events are lower in pitch * sound("saw saw [saw saw] saw") - * .note(loose.range(50, 100)) + * .note(cyclesPer.range(50, 100)) * @example * sound("bd sd [bd bd] sd*4 [- sd] [bd [bd bd]]") - * .note(tight.add(20)) + * .note(cyclesPer.add(20)) */ -export const loose = new Pattern(function (state) { +export const cyclesPer = new Pattern(function (state) { return [new Hap(undefined, state.span, state.span.duration)]; }); /** - * A pattern giving the 'tightness' of events, or in other words, the duration of pattern events, - * in events per cycle. `tight` doesn't have structure itself, but takes structure, and therefore + * A pattern measuring the 'shortness' of events, or in other words, the duration of pattern events, + * in events per cycle. `per` doesn't have structure itself, but takes structure, and therefore * event durations, from the pattern that it is combined with. - * For example `tight.struct("1 1 [1 1] 1")` would give the same as `"4 4 [8 8] 4"`. - * See also its inverse, `loose`. + * For example `per.struct("1 1 [1 1] 1")` would give the same as `"4 4 [8 8] 4"`. + * See also its reciprocal, `cyclesPer`. + * @synonyms perCycle * @example * // Shorter events are more distorted * n("0 0*2 0 0*2 0 [0 0 0]@2").sound("bd") - * .distort(tight.div(2)) + * .distort(per.div(2)) */ -export const tight = new Pattern(function (state) { +export const per = new Pattern(function (state) { return [new Hap(undefined, state.span, Fraction(1).div(state.span.duration))]; }); +export const perCycle = per; + /** - * Like `tight` but gives the tightness of events according to an exponential curve. In - * particular, where the event tightness doubles (i.e. where an event duration halves), the - * returned value increases by one. `tightx.struct("1 1 1 [1 [1 1]]")` would therefore be - * the same as `"3 3 3 [4 [5 5]]"`. + * Like `per` but measures the shortness of events according to an exponential curve. In + * particular, where the event duration halves, the + * returned value increases by one. `perx.struct("1 1 [1 [1 1]] 1")` would therefore be + * the same as `"3 3 [4 [5 5]] 3"`. */ -export const tightx = new Pattern(function (state) { +export const perx = new Pattern(function (state) { const n = Fraction(1).div(state.span.duration); return [new Hap(undefined, state.span, Math.log(n) / Math.log(2) + 1)]; }); diff --git a/packages/core/test/signal.test.mjs b/packages/core/test/signal.test.mjs index 6036b61d..b02f8a08 100644 --- a/packages/core/test/signal.test.mjs +++ b/packages/core/test/signal.test.mjs @@ -8,7 +8,7 @@ import Fraction from 'fraction.js'; import { describe, it, expect, vi } from 'vitest'; -import { saw, saw2, isaw, isaw2, tight, tightx, loose } from '../signal.mjs'; +import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs'; import { fastcat, sequence, State, TimeSpan, Hap } from '../index.mjs'; const st = (begin, end) => new State(ts(begin, end)); @@ -37,24 +37,24 @@ describe('signal()', () => { }); }); -describe('loose', () => { +describe('cyclesPer', () => { it('gives cycles per hap', () => { sameFirst( - loose.struct(true, true, true, fastcat(true, true)), + cyclesPer.struct(true, true, true, fastcat(true, true)), sequence(0.25, 0.25, 0.25, fastcat(0.125, 0.125)).fmap(Fraction), ); }); }); -describe('tight', () => { +describe('per', () => { it('gives haps per cycle', () => { - sameFirst(tight.struct(true, true, true, fastcat(true, true)), sequence(4, 4, 4, fastcat(8, 8)).fmap(Fraction)); + sameFirst(per.struct(true, true, true, fastcat(true, true)), sequence(4, 4, 4, fastcat(8, 8)).fmap(Fraction)); }); }); -describe('tightx', () => { +describe('perx', () => { it('gives exponential haps per cycle', () => { sameFirst( - tightx.struct(true, true, true, fastcat(true, fastcat(true, true))), + perx.struct(true, true, true, fastcat(true, fastcat(true, true))), sequence(3, 3, 3, fastcat(4, fastcat(5, 5))), ); }); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 29237fdc..c4b50d36 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -2539,6 +2539,84 @@ exports[`runs examples > example "cut" example index 0 1`] = ` ] `; +exports[`runs examples > example "cyclesPer" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:saw note:62.5 ]", + "[ 1/4 → 1/2 | s:saw note:62.5 ]", + "[ 1/2 → 5/8 | s:saw note:56.25 ]", + "[ 5/8 → 3/4 | s:saw note:56.25 ]", + "[ 3/4 → 1/1 | s:saw note:62.5 ]", + "[ 1/1 → 5/4 | s:saw note:62.5 ]", + "[ 5/4 → 3/2 | s:saw note:62.5 ]", + "[ 3/2 → 13/8 | s:saw note:56.25 ]", + "[ 13/8 → 7/4 | s:saw note:56.25 ]", + "[ 7/4 → 2/1 | s:saw note:62.5 ]", + "[ 2/1 → 9/4 | s:saw note:62.5 ]", + "[ 9/4 → 5/2 | s:saw note:62.5 ]", + "[ 5/2 → 21/8 | s:saw note:56.25 ]", + "[ 21/8 → 11/4 | s:saw note:56.25 ]", + "[ 11/4 → 3/1 | s:saw note:62.5 ]", + "[ 3/1 → 13/4 | s:saw note:62.5 ]", + "[ 13/4 → 7/2 | s:saw note:62.5 ]", + "[ 7/2 → 29/8 | s:saw note:56.25 ]", + "[ 29/8 → 15/4 | s:saw note:56.25 ]", + "[ 15/4 → 4/1 | s:saw note:62.5 ]", +] +`; + +exports[`runs examples > example "cyclesPer" example index 1 1`] = ` +[ + "[ 0/1 → 1/6 | s:bd note:20.166666666666668 ]", + "[ 1/6 → 1/3 | s:sd note:20.166666666666668 ]", + "[ 1/3 → 5/12 | s:bd note:20.083333333333332 ]", + "[ 5/12 → 1/2 | s:bd note:20.083333333333332 ]", + "[ 1/2 → 13/24 | s:sd note:20.041666666666668 ]", + "[ 13/24 → 7/12 | s:sd note:20.041666666666668 ]", + "[ 7/12 → 5/8 | s:sd note:20.041666666666668 ]", + "[ 5/8 → 2/3 | s:sd note:20.041666666666668 ]", + "[ 3/4 → 5/6 | s:sd note:20.083333333333332 ]", + "[ 5/6 → 11/12 | s:bd note:20.083333333333332 ]", + "[ 11/12 → 23/24 | s:bd note:20.041666666666668 ]", + "[ 23/24 → 1/1 | s:bd note:20.041666666666668 ]", + "[ 1/1 → 7/6 | s:bd note:20.166666666666668 ]", + "[ 7/6 → 4/3 | s:sd note:20.166666666666668 ]", + "[ 4/3 → 17/12 | s:bd note:20.083333333333332 ]", + "[ 17/12 → 3/2 | s:bd note:20.083333333333332 ]", + "[ 3/2 → 37/24 | s:sd note:20.041666666666668 ]", + "[ 37/24 → 19/12 | s:sd note:20.041666666666668 ]", + "[ 19/12 → 13/8 | s:sd note:20.041666666666668 ]", + "[ 13/8 → 5/3 | s:sd note:20.041666666666668 ]", + "[ 7/4 → 11/6 | s:sd note:20.083333333333332 ]", + "[ 11/6 → 23/12 | s:bd note:20.083333333333332 ]", + "[ 23/12 → 47/24 | s:bd note:20.041666666666668 ]", + "[ 47/24 → 2/1 | s:bd note:20.041666666666668 ]", + "[ 2/1 → 13/6 | s:bd note:20.166666666666668 ]", + "[ 13/6 → 7/3 | s:sd note:20.166666666666668 ]", + "[ 7/3 → 29/12 | s:bd note:20.083333333333332 ]", + "[ 29/12 → 5/2 | s:bd note:20.083333333333332 ]", + "[ 5/2 → 61/24 | s:sd note:20.041666666666668 ]", + "[ 61/24 → 31/12 | s:sd note:20.041666666666668 ]", + "[ 31/12 → 21/8 | s:sd note:20.041666666666668 ]", + "[ 21/8 → 8/3 | s:sd note:20.041666666666668 ]", + "[ 11/4 → 17/6 | s:sd note:20.083333333333332 ]", + "[ 17/6 → 35/12 | s:bd note:20.083333333333332 ]", + "[ 35/12 → 71/24 | s:bd note:20.041666666666668 ]", + "[ 71/24 → 3/1 | s:bd note:20.041666666666668 ]", + "[ 3/1 → 19/6 | s:bd note:20.166666666666668 ]", + "[ 19/6 → 10/3 | s:sd note:20.166666666666668 ]", + "[ 10/3 → 41/12 | s:bd note:20.083333333333332 ]", + "[ 41/12 → 7/2 | s:bd note:20.083333333333332 ]", + "[ 7/2 → 85/24 | s:sd note:20.041666666666668 ]", + "[ 85/24 → 43/12 | s:sd note:20.041666666666668 ]", + "[ 43/12 → 29/8 | s:sd note:20.041666666666668 ]", + "[ 29/8 → 11/3 | s:sd note:20.041666666666668 ]", + "[ 15/4 → 23/6 | s:sd note:20.083333333333332 ]", + "[ 23/6 → 47/12 | s:bd note:20.083333333333332 ]", + "[ 47/12 → 95/24 | s:bd note:20.041666666666668 ]", + "[ 95/24 → 4/1 | s:bd note:20.041666666666668 ]", +] +`; + exports[`runs examples > example "decay" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c3 decay:0.1 sustain:0 ]", @@ -6166,84 +6244,6 @@ exports[`runs examples > example "loopEnd" example index 0 1`] = ` ] `; -exports[`runs examples > example "loose" example index 0 1`] = ` -[ - "[ 0/1 → 1/4 | s:saw note:62.5 ]", - "[ 1/4 → 1/2 | s:saw note:62.5 ]", - "[ 1/2 → 5/8 | s:saw note:56.25 ]", - "[ 5/8 → 3/4 | s:saw note:56.25 ]", - "[ 3/4 → 1/1 | s:saw note:62.5 ]", - "[ 1/1 → 5/4 | s:saw note:62.5 ]", - "[ 5/4 → 3/2 | s:saw note:62.5 ]", - "[ 3/2 → 13/8 | s:saw note:56.25 ]", - "[ 13/8 → 7/4 | s:saw note:56.25 ]", - "[ 7/4 → 2/1 | s:saw note:62.5 ]", - "[ 2/1 → 9/4 | s:saw note:62.5 ]", - "[ 9/4 → 5/2 | s:saw note:62.5 ]", - "[ 5/2 → 21/8 | s:saw note:56.25 ]", - "[ 21/8 → 11/4 | s:saw note:56.25 ]", - "[ 11/4 → 3/1 | s:saw note:62.5 ]", - "[ 3/1 → 13/4 | s:saw note:62.5 ]", - "[ 13/4 → 7/2 | s:saw note:62.5 ]", - "[ 7/2 → 29/8 | s:saw note:56.25 ]", - "[ 29/8 → 15/4 | s:saw note:56.25 ]", - "[ 15/4 → 4/1 | s:saw note:62.5 ]", -] -`; - -exports[`runs examples > example "loose" example index 1 1`] = ` -[ - "[ 0/1 → 1/6 | s:bd note:26 ]", - "[ 1/6 → 1/3 | s:sd note:26 ]", - "[ 1/3 → 5/12 | s:bd note:32 ]", - "[ 5/12 → 1/2 | s:bd note:32 ]", - "[ 1/2 → 13/24 | s:sd note:44 ]", - "[ 13/24 → 7/12 | s:sd note:44 ]", - "[ 7/12 → 5/8 | s:sd note:44 ]", - "[ 5/8 → 2/3 | s:sd note:44 ]", - "[ 3/4 → 5/6 | s:sd note:32 ]", - "[ 5/6 → 11/12 | s:bd note:32 ]", - "[ 11/12 → 23/24 | s:bd note:44 ]", - "[ 23/24 → 1/1 | s:bd note:44 ]", - "[ 1/1 → 7/6 | s:bd note:26 ]", - "[ 7/6 → 4/3 | s:sd note:26 ]", - "[ 4/3 → 17/12 | s:bd note:32 ]", - "[ 17/12 → 3/2 | s:bd note:32 ]", - "[ 3/2 → 37/24 | s:sd note:44 ]", - "[ 37/24 → 19/12 | s:sd note:44 ]", - "[ 19/12 → 13/8 | s:sd note:44 ]", - "[ 13/8 → 5/3 | s:sd note:44 ]", - "[ 7/4 → 11/6 | s:sd note:32 ]", - "[ 11/6 → 23/12 | s:bd note:32 ]", - "[ 23/12 → 47/24 | s:bd note:44 ]", - "[ 47/24 → 2/1 | s:bd note:44 ]", - "[ 2/1 → 13/6 | s:bd note:26 ]", - "[ 13/6 → 7/3 | s:sd note:26 ]", - "[ 7/3 → 29/12 | s:bd note:32 ]", - "[ 29/12 → 5/2 | s:bd note:32 ]", - "[ 5/2 → 61/24 | s:sd note:44 ]", - "[ 61/24 → 31/12 | s:sd note:44 ]", - "[ 31/12 → 21/8 | s:sd note:44 ]", - "[ 21/8 → 8/3 | s:sd note:44 ]", - "[ 11/4 → 17/6 | s:sd note:32 ]", - "[ 17/6 → 35/12 | s:bd note:32 ]", - "[ 35/12 → 71/24 | s:bd note:44 ]", - "[ 71/24 → 3/1 | s:bd note:44 ]", - "[ 3/1 → 19/6 | s:bd note:26 ]", - "[ 19/6 → 10/3 | s:sd note:26 ]", - "[ 10/3 → 41/12 | s:bd note:32 ]", - "[ 41/12 → 7/2 | s:bd note:32 ]", - "[ 7/2 → 85/24 | s:sd note:44 ]", - "[ 85/24 → 43/12 | s:sd note:44 ]", - "[ 43/12 → 29/8 | s:sd note:44 ]", - "[ 29/8 → 11/3 | s:sd note:44 ]", - "[ 15/4 → 23/6 | s:sd note:32 ]", - "[ 23/6 → 47/12 | s:bd note:32 ]", - "[ 47/12 → 95/24 | s:bd note:44 ]", - "[ 95/24 → 4/1 | s:bd note:44 ]", -] -`; - exports[`runs examples > example "lpattack" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c2 s:sawtooth cutoff:300 lpattack:0.5 lpenv:4 ]", @@ -7991,6 +7991,51 @@ exports[`runs examples > example "penv" example index 0 1`] = ` ] `; +exports[`runs examples > example "per" example index 0 1`] = ` +[ + "[ 0/1 → 1/7 | n:0 s:bd distort:3.5 ]", + "[ 1/7 → 3/14 | n:0 s:bd distort:7 ]", + "[ 3/14 → 2/7 | n:0 s:bd distort:7 ]", + "[ 2/7 → 3/7 | n:0 s:bd distort:3.5 ]", + "[ 3/7 → 1/2 | n:0 s:bd distort:7 ]", + "[ 1/2 → 4/7 | n:0 s:bd distort:7 ]", + "[ 4/7 → 5/7 | n:0 s:bd distort:3.5 ]", + "[ 5/7 → 17/21 | n:0 s:bd distort:5.25 ]", + "[ 17/21 → 19/21 | n:0 s:bd distort:5.25 ]", + "[ 19/21 → 1/1 | n:0 s:bd distort:5.25 ]", + "[ 1/1 → 8/7 | n:0 s:bd distort:3.5 ]", + "[ 8/7 → 17/14 | n:0 s:bd distort:7 ]", + "[ 17/14 → 9/7 | n:0 s:bd distort:7 ]", + "[ 9/7 → 10/7 | n:0 s:bd distort:3.5 ]", + "[ 10/7 → 3/2 | n:0 s:bd distort:7 ]", + "[ 3/2 → 11/7 | n:0 s:bd distort:7 ]", + "[ 11/7 → 12/7 | n:0 s:bd distort:3.5 ]", + "[ 12/7 → 38/21 | n:0 s:bd distort:5.25 ]", + "[ 38/21 → 40/21 | n:0 s:bd distort:5.25 ]", + "[ 40/21 → 2/1 | n:0 s:bd distort:5.25 ]", + "[ 2/1 → 15/7 | n:0 s:bd distort:3.5 ]", + "[ 15/7 → 31/14 | n:0 s:bd distort:7 ]", + "[ 31/14 → 16/7 | n:0 s:bd distort:7 ]", + "[ 16/7 → 17/7 | n:0 s:bd distort:3.5 ]", + "[ 17/7 → 5/2 | n:0 s:bd distort:7 ]", + "[ 5/2 → 18/7 | n:0 s:bd distort:7 ]", + "[ 18/7 → 19/7 | n:0 s:bd distort:3.5 ]", + "[ 19/7 → 59/21 | n:0 s:bd distort:5.25 ]", + "[ 59/21 → 61/21 | n:0 s:bd distort:5.25 ]", + "[ 61/21 → 3/1 | n:0 s:bd distort:5.25 ]", + "[ 3/1 → 22/7 | n:0 s:bd distort:3.5 ]", + "[ 22/7 → 45/14 | n:0 s:bd distort:7 ]", + "[ 45/14 → 23/7 | n:0 s:bd distort:7 ]", + "[ 23/7 → 24/7 | n:0 s:bd distort:3.5 ]", + "[ 24/7 → 7/2 | n:0 s:bd distort:7 ]", + "[ 7/2 → 25/7 | n:0 s:bd distort:7 ]", + "[ 25/7 → 26/7 | n:0 s:bd distort:3.5 ]", + "[ 26/7 → 80/21 | n:0 s:bd distort:5.25 ]", + "[ 80/21 → 82/21 | n:0 s:bd distort:5.25 ]", + "[ 82/21 → 4/1 | n:0 s:bd distort:5.25 ]", +] +`; + exports[`runs examples > example "perlin" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh cutoff:500 ]", @@ -12066,51 +12111,6 @@ exports[`runs examples > example "take" example index 2 1`] = ` ] `; -exports[`runs examples > example "tight" example index 0 1`] = ` -[ - "[ 0/1 → 1/7 | n:0 s:bd distort:3.5 ]", - "[ 1/7 → 3/14 | n:0 s:bd distort:7 ]", - "[ 3/14 → 2/7 | n:0 s:bd distort:7 ]", - "[ 2/7 → 3/7 | n:0 s:bd distort:3.5 ]", - "[ 3/7 → 1/2 | n:0 s:bd distort:7 ]", - "[ 1/2 → 4/7 | n:0 s:bd distort:7 ]", - "[ 4/7 → 5/7 | n:0 s:bd distort:3.5 ]", - "[ 5/7 → 17/21 | n:0 s:bd distort:5.25 ]", - "[ 17/21 → 19/21 | n:0 s:bd distort:5.25 ]", - "[ 19/21 → 1/1 | n:0 s:bd distort:5.25 ]", - "[ 1/1 → 8/7 | n:0 s:bd distort:3.5 ]", - "[ 8/7 → 17/14 | n:0 s:bd distort:7 ]", - "[ 17/14 → 9/7 | n:0 s:bd distort:7 ]", - "[ 9/7 → 10/7 | n:0 s:bd distort:3.5 ]", - "[ 10/7 → 3/2 | n:0 s:bd distort:7 ]", - "[ 3/2 → 11/7 | n:0 s:bd distort:7 ]", - "[ 11/7 → 12/7 | n:0 s:bd distort:3.5 ]", - "[ 12/7 → 38/21 | n:0 s:bd distort:5.25 ]", - "[ 38/21 → 40/21 | n:0 s:bd distort:5.25 ]", - "[ 40/21 → 2/1 | n:0 s:bd distort:5.25 ]", - "[ 2/1 → 15/7 | n:0 s:bd distort:3.5 ]", - "[ 15/7 → 31/14 | n:0 s:bd distort:7 ]", - "[ 31/14 → 16/7 | n:0 s:bd distort:7 ]", - "[ 16/7 → 17/7 | n:0 s:bd distort:3.5 ]", - "[ 17/7 → 5/2 | n:0 s:bd distort:7 ]", - "[ 5/2 → 18/7 | n:0 s:bd distort:7 ]", - "[ 18/7 → 19/7 | n:0 s:bd distort:3.5 ]", - "[ 19/7 → 59/21 | n:0 s:bd distort:5.25 ]", - "[ 59/21 → 61/21 | n:0 s:bd distort:5.25 ]", - "[ 61/21 → 3/1 | n:0 s:bd distort:5.25 ]", - "[ 3/1 → 22/7 | n:0 s:bd distort:3.5 ]", - "[ 22/7 → 45/14 | n:0 s:bd distort:7 ]", - "[ 45/14 → 23/7 | n:0 s:bd distort:7 ]", - "[ 23/7 → 24/7 | n:0 s:bd distort:3.5 ]", - "[ 24/7 → 7/2 | n:0 s:bd distort:7 ]", - "[ 7/2 → 25/7 | n:0 s:bd distort:7 ]", - "[ 25/7 → 26/7 | n:0 s:bd distort:3.5 ]", - "[ 26/7 → 80/21 | n:0 s:bd distort:5.25 ]", - "[ 80/21 → 82/21 | n:0 s:bd distort:5.25 ]", - "[ 82/21 → 4/1 | n:0 s:bd distort:5.25 ]", -] -`; - exports[`runs examples > example "tour" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:e s:folkharp ]", From 8baecf78b9d62cb7fabee425d1f709fc6b5a5a8b Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Dec 2025 11:37:37 -0600 Subject: [PATCH 125/194] Add note --- packages/superdough/superdoughdata.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/superdough/superdoughdata.mjs b/packages/superdough/superdoughdata.mjs index cc161b0e..a273021d 100644 --- a/packages/superdough/superdoughdata.mjs +++ b/packages/superdough/superdoughdata.mjs @@ -94,6 +94,7 @@ const CONTROL_TARGETS = { wt: { node: 'source', param: 'position' }, warp: { node: 'source', param: 'warp' }, freq: { node: 'source', param: 'frequency' }, + note: { node: 'source', param: 'frequency' }, }; export function getSuperdoughControlTargets() { From dc5f6827f9a72f7f760f8e29d4c2b6948c463647 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Dec 2025 14:50:27 -0600 Subject: [PATCH 126/194] Change names, cleanup --- packages/core/controls.mjs | 80 +++++++++++++++++------------- packages/superdough/superdough.mjs | 2 +- 2 files changed, 47 insertions(+), 35 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index c4ae8b8d..9052d64c 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2810,51 +2810,63 @@ export const scrub = register( false, ); -const configAliases = new Map(); -const addConfigAlias = (funcName, canonical, ...aliases) => { +const subControlAliases = new Map(); +const registerSubControl = (funcName, main, ...aliases) => { const lowerFunc = String(funcName).toLowerCase(); - const aliasMap = configAliases.get(lowerFunc) ?? new Map(); - const allKeys = new Set([canonical, ...aliases]); + const aliasMap = subControlAliases.get(lowerFunc) ?? new Map(); + const allKeys = new Set([main, ...aliases]); for (const alias of allKeys) { - aliasMap.set(String(alias).toLowerCase(), canonical); + aliasMap.set(String(alias).toLowerCase(), main); + } + subControlAliases.set(lowerFunc, aliasMap); +}; + +const registerSubControls = (funcName, aliasGroups = []) => { + for (const [main, ...aliases] of aliasGroups) { + registerSubControl(funcName, main, ...aliases); } - configAliases.set(lowerFunc, aliasMap); }; const resolveConfigKey = (funcName, key) => { - const aliasMap = configAliases.get(String(funcName).toLowerCase()); + const aliasMap = subControlAliases.get(String(funcName).toLowerCase()); if (!aliasMap) return key; const normalized = String(key).toLowerCase(); return aliasMap.get(normalized) ?? key; }; -addConfigAlias('lfo', 'control', 'c'); -addConfigAlias('lfo', 'subControl', 'sc'); -addConfigAlias('lfo', 'rate', 'r'); -addConfigAlias('lfo', 'depth', 'dep', 'dr'); -addConfigAlias('lfo', 'depthabs', 'da'); -addConfigAlias('lfo', 'dcoffset', 'dc'); -addConfigAlias('lfo', 'shape', 'sh'); -addConfigAlias('lfo', 'skew', 'sk'); -addConfigAlias('lfo', 'curve'); -addConfigAlias('lfo', 'sync', 's'); -addConfigAlias('env', 'control', 'c'); -addConfigAlias('env', 'subControl', 'sc'); -addConfigAlias('env', 'attack', 'att', 'a'); -addConfigAlias('env', 'decay', 'dec', 'd'); -addConfigAlias('env', 'sustain', 'sus', 's'); -addConfigAlias('env', 'release', 'rel', 'r'); -addConfigAlias('env', 'depth', 'dep', 'dr'); -addConfigAlias('env', 'depthabs', 'da'); -addConfigAlias('env', 'acurve', 'ac'); -addConfigAlias('env', 'dcurve', 'dc'); -addConfigAlias('env', 'rcurve', 'rc'); -addConfigAlias('bmod', 'orbit', 'o'); -addConfigAlias('bmod', 'control', 'c'); -addConfigAlias('bmod', 'subControl', 'sc'); -addConfigAlias('bmod', 'depth', 'dep', 'dr'); -addConfigAlias('bmod', 'depthabs', 'da'); -addConfigAlias('bmod', 'dc'); +registerSubControls('lfo', [ + ['control', 'c'], + ['subControl', 'sc'], + ['rate', 'r'], + ['depth', 'dep', 'dr'], + ['depthabs', 'da'], + ['dcoffset', 'dc'], + ['shape', 'sh'], + ['skew', 'sk'], + ['curve'], + ['sync', 's'], +]); +registerSubControls('env', [ + ['control', 'c'], + ['subControl', 'sc'], + ['attack', 'att', 'a'], + ['decay', 'dec', 'd'], + ['sustain', 'sus', 's'], + ['release', 'rel', 'r'], + ['depth', 'dep', 'dr'], + ['depthabs', 'da'], + ['acurve', 'ac'], + ['dcurve', 'dc'], + ['rcurve', 'rc'], +]); +registerSubControls('bmod', [ + ['bus', 'b'], + ['control', 'c'], + ['subControl', 'sc'], + ['depth', 'dep', 'dr'], + ['depthabs', 'da'], + ['dc'], +]); Pattern.prototype.modulate = function (type, config, idx) { if (config == null || typeof config !== 'object') { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 9efc5b3e..86f54f13 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -485,7 +485,7 @@ function connectEnvelope(idx, params, nodeTracker, value) { return envNode; } -function connectBusModulator(params, nodeTracker, value) { +function connectBusModulator(params, nodeTracker) { const ac = getAudioContext(); const { control, subControl, depth = 1, depthabs } = params; const signal = controller.getBus(params.bus); From ef2ee0969ade3aa331616c1b37e5daf069edcd33 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Dec 2025 14:59:14 -0600 Subject: [PATCH 127/194] Restore old lfo to avoid unifying right now --- packages/superdough/helpers.mjs | 24 +++++++++++++++++++++++- packages/superdough/superdough.mjs | 10 ++++------ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 19064954..3b704cae 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -109,7 +109,29 @@ export function getEnvelope(audioContext, properties = {}) { return getWorklet(audioContext, 'envelope-processor', properties); } -export function getLfo(audioContext, properties = {}) { +export function getLfo(audioContext, begin, end, properties = {}) { + const { shape = 0, ...props } = properties; + const { dcoffset = -0.5, depth = 1 } = properties; + const lfoprops = { + frequency: 1, + depth, + skew: 0.5, + phaseoffset: 0, + time: begin, + begin, + end, + shape: getModulationShapeInput(shape), + dcoffset, + min: dcoffset * depth, + max: dcoffset * depth + depth, + curve: 1, + ...props, + }; + + return getWorklet(audioContext, 'lfo-processor', lfoprops); +} + +export function getCustomLfo(audioContext, properties = {}) { // Extract some params we need for deriving other params const { shape = 0, ...props } = properties; const lfoprops = { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 6fcc9cec..04e383c4 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -443,7 +443,7 @@ function _getTargetParamsForControl(control, nodes, subControl) { return { targetParams: audioParams, paramName }; } -function connectLFO(idx, params, nodeTracker, value) { +function connectLFO(idx, params, nodeTracker) { const { rate = 1, sync, cps, cycle, control = 'lfo', subControl, depth = 1, depthabs, ...filteredParams } = params; const { targetParams, paramName } = _getTargetParamsForControl(control, nodeTracker, subControl); const currentValue = targetParams[0].value; @@ -457,13 +457,13 @@ function connectLFO(idx, params, nodeTracker, value) { min, max, }; - const lfoNode = getLfo(getAudioContext(), modParams); + const lfoNode = getCustomLfo(getAudioContext(), modParams); nodeTracker[`lfo${idx}`] = [lfoNode]; targetParams.forEach((t) => lfoNode.connect(t)); return lfoNode; } -function connectEnvelope(idx, params, nodeTracker, value) { +function connectEnvelope(idx, params, nodeTracker) { const { control, subControl, acurve, dcurve, rcurve, depth = 1, depthabs, ...filteredParams } = params; const { targetParams, paramName } = _getTargetParamsForControl(control, nodeTracker, subControl); const currentValue = targetParams[0].value; @@ -997,7 +997,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) end: endWithRelease, }, nodes, - value, ); audioNodes.push(lfo); } @@ -1012,14 +1011,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) end: endWithRelease, }, nodes, - value, ); audioNodes.push(env); } } if (value.bmod) { for (const p of value.bmod) { - const { toCleanup } = connectBusModulator({ ...p, begin: t, end: endWithRelease }, nodes, value); + const { toCleanup } = connectBusModulator({ ...p, begin: t, end: endWithRelease }, nodes); audioNodes.push(...toCleanup); } } From 11e3da0552d34c230dcb0682392afe55cf928c84 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Dec 2025 15:08:48 -0600 Subject: [PATCH 128/194] Parity with other LFO (sync and dcoffset) --- packages/superdough/helpers.mjs | 3 ++- packages/superdough/superdough.mjs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 3b704cae..b13f4049 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -132,10 +132,11 @@ export function getLfo(audioContext, begin, end, properties = {}) { } export function getCustomLfo(audioContext, properties = {}) { - // Extract some params we need for deriving other params + // Default / process certain params const { shape = 0, ...props } = properties; const lfoprops = { shape: getModulationShapeInput(shape), + dcoffset: -0.5, ...props, }; return getWorklet(audioContext, 'lfo-processor', lfoprops); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 04e383c4..ba84d691 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -451,7 +451,7 @@ function connectLFO(idx, params, nodeTracker) { const depthValue = depthabs != null ? depthabs : depth * currentValue; const modParams = { ...filteredParams, - frequency: sync !== undefined ? sync / cps : rate, + frequency: sync !== undefined ? sync * cps : rate, time: cycle / cps, depth: depthValue, min, From 4b08dd8447e24216ae1caa4a317362f7f0103867 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Dec 2025 15:20:51 -0600 Subject: [PATCH 129/194] Unify lfo functions --- packages/superdough/helpers.mjs | 53 ++++++++++++++++-------------- packages/superdough/superdough.mjs | 2 +- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index b13f4049..7b9becdf 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -109,36 +109,39 @@ export function getEnvelope(audioContext, properties = {}) { return getWorklet(audioContext, 'envelope-processor', properties); } -export function getLfo(audioContext, begin, end, properties = {}) { - const { shape = 0, ...props } = properties; - const { dcoffset = -0.5, depth = 1 } = properties; +export function getLfo(audioContext, properties = {}) { + const { + shape = 0, + begin = 0, + end = 0, + time, + depth = 1, + dcoffset = -0.5, + frequency = 1, + skew = 0.5, + phaseoffset = 0, + curve = 1, + min, + max, + ...props + } = properties; + const lfoprops = { - frequency: 1, - depth, - skew: 0.5, - phaseoffset: 0, - time: begin, begin, end, - shape: getModulationShapeInput(shape), + time: time ?? begin, + depth, dcoffset, - min: dcoffset * depth, - max: dcoffset * depth + depth, - curve: 1, - ...props, - }; - - return getWorklet(audioContext, 'lfo-processor', lfoprops); -} - -export function getCustomLfo(audioContext, properties = {}) { - // Default / process certain params - const { shape = 0, ...props } = properties; - const lfoprops = { + frequency, + skew, + phaseoffset, + curve, shape: getModulationShapeInput(shape), - dcoffset: -0.5, + min: min ?? dcoffset * depth, + max: max ?? dcoffset * depth + depth, ...props, }; + return getWorklet(audioContext, 'lfo-processor', lfoprops); } @@ -177,7 +180,9 @@ export function getParamLfo(audioContext, param, start, end, lfoValues) { } let lfo; if (depth) { - lfo = getLfo(audioContext, start, end, { + lfo = getLfo(audioContext, { + begin: start, + end, depth, dcoffset, ...getLfoInputs, diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index ba84d691..5ea04f89 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -457,7 +457,7 @@ function connectLFO(idx, params, nodeTracker) { min, max, }; - const lfoNode = getCustomLfo(getAudioContext(), modParams); + const lfoNode = getLfo(getAudioContext(), modParams); nodeTracker[`lfo${idx}`] = [lfoNode]; targetParams.forEach((t) => lfoNode.connect(t)); return lfoNode; From bae5571df0547d1901a3b035b02d13ff7f86e9b3 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Dec 2025 15:32:27 -0600 Subject: [PATCH 130/194] Cleanup --- packages/core/controls.mjs | 28 ++++++++++++-------------- packages/superdough/superdoughdata.mjs | 1 + 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index d8fdd51d..0cb78f05 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2814,27 +2814,25 @@ export const scrub = register( ); const subControlAliases = new Map(); -const registerSubControl = (funcName, main, ...aliases) => { - const lowerFunc = String(funcName).toLowerCase(); - const aliasMap = subControlAliases.get(lowerFunc) ?? new Map(); - const allKeys = new Set([main, ...aliases]); +const registerSubControl = (control, subControl, ...aliases) => { + const aliasMap = subControlAliases.get(control) ?? new Map(); + const allKeys = new Set([subControl, ...aliases]); for (const alias of allKeys) { - aliasMap.set(String(alias).toLowerCase(), main); + aliasMap.set(String(alias).toLowerCase(), subControl); } - subControlAliases.set(lowerFunc, aliasMap); + subControlAliases.set(control, aliasMap); }; -const registerSubControls = (funcName, aliasGroups = []) => { - for (const [main, ...aliases] of aliasGroups) { - registerSubControl(funcName, main, ...aliases); +const registerSubControls = (control, subControlAliases = []) => { + for (const [subControl, ...aliases] of subControlAliases) { + registerSubControl(control, subControl, ...aliases); } }; -const resolveConfigKey = (funcName, key) => { - const aliasMap = subControlAliases.get(String(funcName).toLowerCase()); - if (!aliasMap) return key; - const normalized = String(key).toLowerCase(); - return aliasMap.get(normalized) ?? key; +const getMainSubcontrolName = (control, subKey) => { + const aliasMap = subControlAliases.get(control); + if (!aliasMap) return subKey; + return aliasMap.get(String(subKey).toLowerCase()) ?? subKey; }; registerSubControls('lfo', [ @@ -2884,7 +2882,7 @@ Pattern.prototype.modulate = function (type, config, idx) { let defaultValue = {}; let defaultSet = 'control' in config; for (const [rawKey, value] of Object.entries(config)) { - const key = resolveConfigKey(type, rawKey); + const key = getMainSubcontrolName(type, rawKey); const valuePat = reify(value); output = output .fmap((v) => (c) => { diff --git a/packages/superdough/superdoughdata.mjs b/packages/superdough/superdoughdata.mjs index a273021d..d3b3d40b 100644 --- a/packages/superdough/superdoughdata.mjs +++ b/packages/superdough/superdoughdata.mjs @@ -4,6 +4,7 @@ Copyright (C) 2025 Strudel contributors - see . */ +// Mapping from control name to webaudio node and parameter const CONTROL_TARGETS = { stretch: { node: 'stretch', param: 'pitchFactor' }, gain: { node: 'gain', param: 'gain' }, From 29f7f5c1f83d00d680fa2916178cc21e3de43b47 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Dec 2025 16:06:28 -0600 Subject: [PATCH 131/194] Allow lfos to modulate other lfos without clamping --- packages/superdough/superdough.mjs | 15 ++++++++------- packages/superdough/superdoughdata.mjs | 10 ++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 5ea04f89..97781264 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -401,10 +401,11 @@ function _getControlData(control) { return controlTargets[_stripIndex(control)]; } -function _getRangeForParam(paramName, targetParams, currentValue) { - if (paramName === 'frequency') { - const liveValue = targetParams?.[0]?.value ?? currentValue ?? 0; - return { min: 20 - liveValue, max: 24000 - liveValue }; +function _getRangeForParam(paramName, currentValue, control) { + // For our normal oscillators / filters we want to clamp the frequency to a reasonable range + // For LFOs we allow it to be modulated freely + if (paramName === 'frequency' || control?.startsWith?.('lfo')) { + return { min: 20 - currentValue, max: 24000 - currentValue }; } return { min: undefined, max: undefined }; } @@ -447,7 +448,7 @@ function connectLFO(idx, params, nodeTracker) { const { rate = 1, sync, cps, cycle, control = 'lfo', subControl, depth = 1, depthabs, ...filteredParams } = params; const { targetParams, paramName } = _getTargetParamsForControl(control, nodeTracker, subControl); const currentValue = targetParams[0].value; - const { min, max } = _getRangeForParam(paramName, targetParams, currentValue); + const { min, max } = _getRangeForParam(paramName, currentValue, control); const depthValue = depthabs != null ? depthabs : depth * currentValue; const modParams = { ...filteredParams, @@ -467,7 +468,7 @@ function connectEnvelope(idx, params, nodeTracker) { const { control, subControl, acurve, dcurve, rcurve, depth = 1, depthabs, ...filteredParams } = params; const { targetParams, paramName } = _getTargetParamsForControl(control, nodeTracker, subControl); const currentValue = targetParams[0].value; - const { min, max } = _getRangeForParam(paramName, targetParams, currentValue); + const { min, max } = _getRangeForParam(paramName, currentValue, control); const depthValue = depthabs != null ? depthabs : depth * currentValue; const envNode = getEnvelope(getAudioContext(), { ...filteredParams, @@ -493,7 +494,7 @@ function connectBusModulator(params, nodeTracker) { signal.connect(shifted); const { targetParams, paramName } = _getTargetParamsForControl(control, nodeTracker, subControl); const currentValue = targetParams[0].value; - const { min, max } = _getRangeForParam(paramName, targetParams, currentValue); + const { min, max } = _getRangeForParam(paramName, currentValue, control); const depthValue = depthabs != null ? depthabs : depth * currentValue; const maxAbsDepth = Math.min(Math.abs(min), Math.abs(max)); const boundedDepth = Math.min(Math.abs(depthValue), maxAbsDepth) || Math.abs(depthValue); diff --git a/packages/superdough/superdoughdata.mjs b/packages/superdough/superdoughdata.mjs index d3b3d40b..6b577889 100644 --- a/packages/superdough/superdoughdata.mjs +++ b/packages/superdough/superdoughdata.mjs @@ -19,8 +19,18 @@ const CONTROL_TARGETS = { // MODULATORS lfo: { node: 'lfo', param: 'frequency' }, + lfo_rate: { node: 'lfo', param: 'frequency' }, + lfo_sync: { node: 'lfo', param: 'frequency' }, + lfo_depth: { node: 'lfo', param: 'depth' }, + lfo_depthabs: { node: 'lfo', param: 'depth' }, env: { node: 'env', param: 'depth' }, + env_attack: { node: 'env', param: 'attack' }, + env_decay: { node: 'env', param: 'decay' }, + env_sustain: { node: 'env', param: 'sustain' }, + env_release: { node: 'env', param: 'release' }, bmod: { node: 'bmod', param: 'depth' }, + bmod_depth: { node: 'bmod', param: 'depth' }, + bmod_depthabs: { node: 'bmod', param: 'depth' }, // LPF cutoff: { node: 'lpf', param: 'frequency' }, From dec037bed52f8ffa76bc36c57a14276b6981c137 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Dec 2025 16:23:47 -0600 Subject: [PATCH 132/194] Bus fix --- packages/superdough/superdoughdata.mjs | 1 + packages/superdough/synth.mjs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/superdough/superdoughdata.mjs b/packages/superdough/superdoughdata.mjs index 6b577889..cab82975 100644 --- a/packages/superdough/superdoughdata.mjs +++ b/packages/superdough/superdoughdata.mjs @@ -74,6 +74,7 @@ const CONTROL_TARGETS = { shapevol: { node: 'shape', param: 'postgain' }, distort: { node: 'distort', param: 'distort' }, distortvol: { node: 'distort', param: 'postgain' }, + distorttype: { node: 'distort', param: 'distort' }, // COMPRESSOR compressor: { node: 'compressor', param: 'threshold' }, diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 39868589..40e62f86 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -380,7 +380,7 @@ export function registerSynthSounds() { const holdend = begin + value.duration; const end = holdend + release + 0.01; const bus = getSuperdoughAudioController().getBus(value.n ?? 0); - const envGain = bus.connect(gainNode(1)); + const envGain = bus.connect(gainNode(0)); getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); const timeoutNode = webAudioTimeout( ac, From c598059dc77ee46eccf334467efc60adac2a3a60 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Dec 2025 16:26:50 -0600 Subject: [PATCH 133/194] Clean up old code --- packages/superdough/superdough.mjs | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 97781264..9312ccda 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -381,19 +381,6 @@ function _getNodeParam(node, name) { return undefined; } -function _getNodeParams(node) { - const params = new Set(); - // Worklet case - if (node?.parameters) { - node.parameters.forEach((_v, k) => params.add(k)); - } - // Guesses based on common parameters - ['gain', 'frequency', 'detune', 'Q', 'pan', 'playbackRate', 'delayTime'].forEach((k) => { - if (node?.[k] instanceof AudioParam) params.add(k); - }); - return Array.from(params); -} - const controlTargets = getSuperdoughControlTargets(); const _stripIndex = (control) => control?.replace(/\d+$/, ''); @@ -431,14 +418,6 @@ function _getTargetParamsForControl(control, nodes, subControl) { const audioParams = []; targetNodes.forEach((targetNode) => { const targetParam = _getNodeParam(targetNode, paramName); - if (!targetParam) { - const available = _getNodeParams(targetNode); - errorLogger( - `Could not connect to parameter '${paramName}' on '${nodeKey}'. Available parameters: ${available.join(', ')}`, - 'superdough', - ); - return; - } audioParams.push(targetParam); }); return { targetParams: audioParams, paramName }; From 63de46ae96ecc8069285ad64f77489fb230547f8 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Dec 2025 16:44:36 -0600 Subject: [PATCH 134/194] Update docstrings --- packages/core/controls.mjs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 0cb78f05..826174d8 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2924,8 +2924,8 @@ Pattern.prototype.modulate = function (type, config, idx) { * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da * @param {number | Pattern} [config.dcoffset] DC offset / bias for the waveform. Aliases: dc - * @param {number | Pattern} [config.shape] Waveform shape index. Aliases: sh - * @param {number | Pattern} [config.skew] Waveform skew amount. Aliases: sk + * @param {number | Pattern} [config.shape] Shape index. Aliases: sh + * @param {number | Pattern} [config.skew] Skew amount. Aliases: sk * @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: c * @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s * @returns Pattern @@ -2973,7 +2973,6 @@ export const env = (config) => pure({}).env(config); * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da - * @param {number | Pattern} [config.ratio] Modulation ratio. Aliases: rat * @param {number | Pattern} [config.dc] DC offset prior to application * @returns Pattern */ From 18b3c66eb83948c0f1008ebddba75862b558f6cf Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Dec 2025 17:15:06 -0600 Subject: [PATCH 135/194] Clean up error messages --- packages/superdough/superdough.mjs | 13 ++++++++----- packages/superdough/worklets.mjs | 16 +++++++++++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 9312ccda..8f79e5b2 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -401,7 +401,7 @@ function _getTargetParamsForControl(control, nodes, subControl) { const lookupKey = subControl ? `${control}_${subControl}` : control; const targetInfo = _getControlData(lookupKey) ?? _getControlData(control); if (!targetInfo) { - errorLogger(`Could not find control data for target '${control}'`, 'superdough'); + errorLogger(new Error(`Could not find control data for target '${control}'`), 'superdough'); return { targetParams: [], paramName: control }; } const paramName = targetInfo.param; @@ -410,7 +410,7 @@ function _getTargetParamsForControl(control, nodes, subControl) { if (!targetNodes) { const keys = Object.keys(nodes); errorLogger( - `Could not connect to target '${nodeKey}' — it does not exist. Available targets: ${keys.join(', ')}`, + new Error(`Could not connect to target '${nodeKey}' — it does not exist. Available targets: ${keys.join(', ')}`), 'superdough', ); return { targetParams: [], paramName }; @@ -426,6 +426,7 @@ function _getTargetParamsForControl(control, nodes, subControl) { function connectLFO(idx, params, nodeTracker) { const { rate = 1, sync, cps, cycle, control = 'lfo', subControl, depth = 1, depthabs, ...filteredParams } = params; const { targetParams, paramName } = _getTargetParamsForControl(control, nodeTracker, subControl); + if (!targetParams.length) return; const currentValue = targetParams[0].value; const { min, max } = _getRangeForParam(paramName, currentValue, control); const depthValue = depthabs != null ? depthabs : depth * currentValue; @@ -446,6 +447,7 @@ function connectLFO(idx, params, nodeTracker) { function connectEnvelope(idx, params, nodeTracker) { const { control, subControl, acurve, dcurve, rcurve, depth = 1, depthabs, ...filteredParams } = params; const { targetParams, paramName } = _getTargetParamsForControl(control, nodeTracker, subControl); + if (!targetParams.length) return; const currentValue = targetParams[0].value; const { min, max } = _getRangeForParam(paramName, currentValue, control); const depthValue = depthabs != null ? depthabs : depth * currentValue; @@ -466,12 +468,13 @@ function connectEnvelope(idx, params, nodeTracker) { function connectBusModulator(params, nodeTracker) { const ac = getAudioContext(); const { control, subControl, depth = 1, depthabs } = params; + const { targetParams, paramName } = _getTargetParamsForControl(control, nodeTracker, subControl); + if (!targetParams.length) return { toCleanup: [] }; const signal = controller.getBus(params.bus); const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 }); dc.start(params.begin); const shifted = dc.connect(gainNode(1)); signal.connect(shifted); - const { targetParams, paramName } = _getTargetParamsForControl(control, nodeTracker, subControl); const currentValue = targetParams[0].value; const { min, max } = _getRangeForParam(paramName, currentValue, control); const depthValue = depthabs != null ? depthabs : depth * currentValue; @@ -978,7 +981,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) }, nodes, ); - audioNodes.push(lfo); + lfo && audioNodes.push(lfo); } } if (value.env) { @@ -992,7 +995,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) }, nodes, ); - audioNodes.push(env); + env && audioNodes.push(env); } } if (value.bmod) { diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 321c8b42..4523e9e8 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1013,9 +1013,15 @@ class EnvelopeProcessor extends AudioWorkletProcessor { } process(_inputs, outputs, params) { + const begin = params['begin'][0]; + const end = params['end'][0]; + if (currentTime >= end) { + return false; + } + if (currentTime <= begin) { + return true; + } const out = outputs[0][0]; - if (!out) return true; - const begin = pv(params.begin, 0); const retrigger = pv(params.retrigger, 0) >= 0.5; // convert to bool if (begin !== this.beginTime && (this.state === 0 || retrigger)) { // triggered @@ -1034,8 +1040,8 @@ class EnvelopeProcessor extends AudioWorkletProcessor { const dCurve = pv(params.decayCurve, i); const rCurve = pv(params.releaseCurve, i); const depth = pv(params.depth, i); - const clampMin = pv(params.min, i); - const clampMax = pv(params.max, i); + const min = pv(params.min, i); + const max = pv(params.max, i); const states = [ { time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle { time: attack, start: this.attackStart, target: 1, curve: aCurve }, @@ -1049,7 +1055,7 @@ class EnvelopeProcessor extends AudioWorkletProcessor { this.state = (this.state + 1) % states.length; time = states[this.state].time; } - const clamped = clamp(this.val * depth, Math.min(clampMin, clampMax), Math.max(clampMin, clampMax)); + const clamped = clamp(this.val * depth, min, max); out[i] = clamped; } return true; From 4a7344595d8e72c14acb1ace5c629a1aa81e4a00 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Dec 2025 17:28:42 -0600 Subject: [PATCH 136/194] Make legacy the default --- packages/core/bench/signal.bench.mjs | 10 +- packages/core/signal.mjs | 19 +- test/__snapshots__/examples.test.mjs.snap | 1785 +++++++++++---------- vitest.setup.mjs | 4 +- website/src/repl/tunes.mjs | 20 +- 5 files changed, 926 insertions(+), 912 deletions(-) diff --git a/packages/core/bench/signal.bench.mjs b/packages/core/bench/signal.bench.mjs index d92840ef..bd651ccd 100644 --- a/packages/core/bench/signal.bench.mjs +++ b/packages/core/bench/signal.bench.mjs @@ -1,6 +1,6 @@ import { describe, bench } from 'vitest'; -import { calculateSteps, rand, useOldRandom } from '../index.mjs'; +import { calculateSteps, rand, useRNG } from '../index.mjs'; const testingResolution = 128; @@ -11,13 +11,13 @@ describe('old random', () => { bench( '+tactus', () => { - useOldRandom(); + useRNG('legacy'); _generateRandomPattern(); }, { time: 1000, teardown() { - useOldRandom(false); + useRNG('legacy'); }, }, ); @@ -26,13 +26,13 @@ describe('old random', () => { bench( '-tactus', () => { - useOldRandom(); + useRNG('precise'); _generateRandomPattern(); }, { time: 1000, teardown() { - useOldRandom(false); + useRNG('legacy'); }, }, ); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index e50ba22d..e497fb1b 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -231,7 +231,8 @@ const timeToRands = (t, n, seed = 0) => { return out; }; -// Deprecated: Old random signals. Configuration `useOldRandom` may be used for legacy songs +// Old random signals. Currently the default, but can also be chosen via +// `useRNG('legacy')` // stretch 300 cycles over the range of [0,2**29 == 536870912) then apply the xorshift algorithm const __xorwise = (x) => { @@ -257,23 +258,25 @@ const __timeToRands = (t, n) => __timeToRandsPrime(__timeToIntSeed(t), n); // End old random -let useOldRandomBool = false; +let RNG_MODE = 'legacy'; export const getRandsAtTime = (t, n = 1, seed = 0) => { - return useOldRandomBool ? __timeToRands(t + seed, n) : timeToRands(t, n, seed); + return RNG_MODE === 'legacy' ? __timeToRands(t + seed, n) : timeToRands(t, n, seed); }; /** - * Whether to use the old random number generator or not. Can be used to support legacy projects + * Sets which random number generator to use. Historically Strudel would + * use `useRNG('legacy')`, which remains the default. To use a new more statistically + * precise RNG, try `useRNG('precise')`. * - * @name useOldRandom - * @param {boolean} b - Whether to use old RNG + * @name useRNG + * @param {string} mod - Mode. One of 'legacy', 'precise' * @example - * useOldRandom(true) + * useRNG('legacy') * // Repeats every 300 cycles * $: n(irand(50)).seg(16).scale("C:minor").ribbon(88, 32) * $: n(irand(50)).seg(16).scale("C:minor").ribbon(388, 32) */ -export const useOldRandom = (b = true) => (useOldRandomBool = b); +export const useRNG = (mode = 'legacy') => (RNG_MODE = mode); /** * A discrete pattern of numbers from 0 to n-1 diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index b26823a9..48131587 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -677,17 +677,17 @@ exports[`runs examples > example "almostAlways" example index 0 1`] = ` "[ 1/2 → 5/8 | s:hh speed:0.5 ]", "[ 5/8 → 3/4 | s:hh speed:0.5 ]", "[ 3/4 → 7/8 | s:hh speed:0.5 ]", - "[ 7/8 → 1/1 | s:hh ]", + "[ 7/8 → 1/1 | s:hh speed:0.5 ]", "[ 1/1 → 9/8 | s:hh speed:0.5 ]", "[ 9/8 → 5/4 | s:hh speed:0.5 ]", - "[ 5/4 → 11/8 | s:hh ]", + "[ 5/4 → 11/8 | s:hh speed:0.5 ]", "[ 11/8 → 3/2 | s:hh speed:0.5 ]", "[ 3/2 → 13/8 | s:hh speed:0.5 ]", "[ 13/8 → 7/4 | s:hh speed:0.5 ]", "[ 7/4 → 15/8 | s:hh speed:0.5 ]", "[ 15/8 → 2/1 | s:hh speed:0.5 ]", - "[ 2/1 → 17/8 | s:hh speed:0.5 ]", - "[ 17/8 → 9/4 | s:hh speed:0.5 ]", + "[ 2/1 → 17/8 | s:hh ]", + "[ 17/8 → 9/4 | s:hh ]", "[ 9/4 → 19/8 | s:hh speed:0.5 ]", "[ 19/8 → 5/2 | s:hh speed:0.5 ]", "[ 5/2 → 21/8 | s:hh speed:0.5 ]", @@ -699,7 +699,7 @@ exports[`runs examples > example "almostAlways" example index 0 1`] = ` "[ 13/4 → 27/8 | s:hh ]", "[ 27/8 → 7/2 | s:hh speed:0.5 ]", "[ 7/2 → 29/8 | s:hh speed:0.5 ]", - "[ 29/8 → 15/4 | s:hh speed:0.5 ]", + "[ 29/8 → 15/4 | s:hh ]", "[ 15/4 → 31/8 | s:hh speed:0.5 ]", "[ 31/8 → 4/1 | s:hh speed:0.5 ]", ] @@ -707,7 +707,7 @@ exports[`runs examples > example "almostAlways" example index 0 1`] = ` exports[`runs examples > example "almostNever" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:hh ]", + "[ 0/1 → 1/8 | s:hh speed:0.5 ]", "[ 1/8 → 1/4 | s:hh ]", "[ 1/4 → 3/8 | s:hh ]", "[ 3/8 → 1/2 | s:hh ]", @@ -716,9 +716,9 @@ exports[`runs examples > example "almostNever" example index 0 1`] = ` "[ 3/4 → 7/8 | s:hh ]", "[ 7/8 → 1/1 | s:hh ]", "[ 1/1 → 9/8 | s:hh ]", - "[ 9/8 → 5/4 | s:hh speed:0.5 ]", + "[ 9/8 → 5/4 | s:hh ]", "[ 5/4 → 11/8 | s:hh ]", - "[ 11/8 → 3/2 | s:hh speed:0.5 ]", + "[ 11/8 → 3/2 | s:hh ]", "[ 3/2 → 13/8 | s:hh ]", "[ 13/8 → 7/4 | s:hh ]", "[ 7/4 → 15/8 | s:hh ]", @@ -730,11 +730,11 @@ exports[`runs examples > example "almostNever" example index 0 1`] = ` "[ 5/2 → 21/8 | s:hh ]", "[ 21/8 → 11/4 | s:hh ]", "[ 11/4 → 23/8 | s:hh ]", - "[ 23/8 → 3/1 | s:hh ]", + "[ 23/8 → 3/1 | s:hh speed:0.5 ]", "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", "[ 13/4 → 27/8 | s:hh ]", - "[ 27/8 → 7/2 | s:hh speed:0.5 ]", + "[ 27/8 → 7/2 | s:hh ]", "[ 7/2 → 29/8 | s:hh ]", "[ 29/8 → 15/4 | s:hh ]", "[ 15/4 → 31/8 | s:hh ]", @@ -1078,70 +1078,70 @@ exports[`runs examples > example "begin" example index 0 1`] = ` exports[`runs examples > example "berlin" example index 0 1`] = ` [ - "[ 0/1 → 1/16 | note:G3 ]", - "[ 1/16 → 1/8 | note:G3 ]", - "[ 1/8 → 3/16 | note:A3 ]", - "[ 3/16 → 1/4 | note:A3 ]", - "[ 1/4 → 5/16 | note:E3 ]", - "[ 5/16 → 3/8 | note:F3 ]", - "[ 3/8 → 7/16 | note:G3 ]", - "[ 7/16 → 1/2 | note:A3 ]", - "[ 1/2 → 9/16 | note:Bb3 ]", - "[ 9/16 → 5/8 | note:C4 ]", - "[ 5/8 → 11/16 | note:D4 ]", + "[ 0/1 → 1/16 | note:D3 ]", + "[ 1/16 → 1/8 | note:E3 ]", + "[ 1/8 → 3/16 | note:F3 ]", + "[ 3/16 → 1/4 | note:G3 ]", + "[ 1/4 → 5/16 | note:A3 ]", + "[ 5/16 → 3/8 | note:C4 ]", + "[ 3/8 → 7/16 | note:D4 ]", + "[ 7/16 → 1/2 | note:F4 ]", + "[ 1/2 → 9/16 | note:D4 ]", + "[ 9/16 → 5/8 | note:E4 ]", + "[ 5/8 → 11/16 | note:E4 ]", "[ 11/16 → 3/4 | note:E4 ]", - "[ 3/4 → 13/16 | note:Bb3 ]", - "[ 13/16 → 7/8 | note:Bb3 ]", - "[ 7/8 → 15/16 | note:Bb3 ]", - "[ 15/16 → 1/1 | note:C4 ]", + "[ 3/4 → 13/16 | note:F3 ]", + "[ 13/16 → 7/8 | note:F3 ]", + "[ 7/8 → 15/16 | note:F3 ]", + "[ 15/16 → 1/1 | note:F3 ]", "[ 1/1 → 17/16 | note:E3 ]", - "[ 17/16 → 9/8 | note:G3 ]", - "[ 9/8 → 19/16 | note:A3 ]", - "[ 19/16 → 5/4 | note:C4 ]", - "[ 5/4 → 21/16 | note:C4 ]", - "[ 21/16 → 11/8 | note:D4 ]", - "[ 11/8 → 23/16 | note:D4 ]", - "[ 23/16 → 3/2 | note:E4 ]", - "[ 3/2 → 25/16 | note:F3 ]", - "[ 25/16 → 13/8 | note:A3 ]", + "[ 17/16 → 9/8 | note:E3 ]", + "[ 9/8 → 19/16 | note:E3 ]", + "[ 19/16 → 5/4 | note:F3 ]", + "[ 5/4 → 21/16 | note:E3 ]", + "[ 21/16 → 11/8 | note:F3 ]", + "[ 11/8 → 23/16 | note:G3 ]", + "[ 23/16 → 3/2 | note:A3 ]", + "[ 3/2 → 25/16 | note:A3 ]", + "[ 25/16 → 13/8 | note:Bb3 ]", "[ 13/8 → 27/16 | note:Bb3 ]", - "[ 27/16 → 7/4 | note:D4 ]", - "[ 7/4 → 29/16 | note:C4 ]", - "[ 29/16 → 15/8 | note:D4 ]", - "[ 15/8 → 31/16 | note:E4 ]", - "[ 31/16 → 2/1 | note:F4 ]", - "[ 2/1 → 33/16 | note:Bb3 ]", - "[ 33/16 → 17/8 | note:C4 ]", - "[ 17/8 → 35/16 | note:D4 ]", - "[ 35/16 → 9/4 | note:E4 ]", + "[ 27/16 → 7/4 | note:Bb3 ]", + "[ 7/4 → 29/16 | note:F3 ]", + "[ 29/16 → 15/8 | note:G3 ]", + "[ 15/8 → 31/16 | note:Bb3 ]", + "[ 31/16 → 2/1 | note:C4 ]", + "[ 2/1 → 33/16 | note:C4 ]", + "[ 33/16 → 17/8 | note:D4 ]", + "[ 17/8 → 35/16 | note:F4 ]", + "[ 35/16 → 9/4 | note:G4 ]", "[ 9/4 → 37/16 | note:Bb3 ]", - "[ 37/16 → 19/8 | note:C4 ]", - "[ 19/8 → 39/16 | note:D4 ]", - "[ 39/16 → 5/2 | note:F4 ]", - "[ 5/2 → 41/16 | note:C4 ]", - "[ 41/16 → 21/8 | note:D4 ]", - "[ 21/8 → 43/16 | note:F4 ]", - "[ 43/16 → 11/4 | note:A4 ]", - "[ 11/4 → 45/16 | note:D4 ]", - "[ 45/16 → 23/8 | note:F4 ]", - "[ 23/8 → 47/16 | note:G4 ]", - "[ 47/16 → 3/1 | note:A4 ]", - "[ 3/1 → 49/16 | note:C4 ]", - "[ 49/16 → 25/8 | note:C4 ]", - "[ 25/8 → 51/16 | note:C4 ]", - "[ 51/16 → 13/4 | note:D4 ]", - "[ 13/4 → 53/16 | note:F3 ]", - "[ 53/16 → 27/8 | note:F3 ]", - "[ 27/8 → 55/16 | note:F3 ]", - "[ 55/16 → 7/2 | note:F3 ]", - "[ 7/2 → 57/16 | note:E3 ]", - "[ 57/16 → 29/8 | note:E3 ]", - "[ 29/8 → 59/16 | note:F3 ]", - "[ 59/16 → 15/4 | note:F3 ]", + "[ 37/16 → 19/8 | note:Bb3 ]", + "[ 19/8 → 39/16 | note:Bb3 ]", + "[ 39/16 → 5/2 | note:C4 ]", + "[ 5/2 → 41/16 | note:F3 ]", + "[ 41/16 → 21/8 | note:F3 ]", + "[ 21/8 → 43/16 | note:G3 ]", + "[ 43/16 → 11/4 | note:A3 ]", + "[ 11/4 → 45/16 | note:A3 ]", + "[ 45/16 → 23/8 | note:A3 ]", + "[ 23/8 → 47/16 | note:A3 ]", + "[ 47/16 → 3/1 | note:A3 ]", + "[ 3/1 → 49/16 | note:E3 ]", + "[ 49/16 → 25/8 | note:G3 ]", + "[ 25/8 → 51/16 | note:Bb3 ]", + "[ 51/16 → 13/4 | note:C4 ]", + "[ 13/4 → 53/16 | note:D4 ]", + "[ 53/16 → 27/8 | note:E4 ]", + "[ 27/8 → 55/16 | note:G4 ]", + "[ 55/16 → 7/2 | note:A4 ]", + "[ 7/2 → 57/16 | note:Bb3 ]", + "[ 57/16 → 29/8 | note:Bb3 ]", + "[ 29/8 → 59/16 | note:C4 ]", + "[ 59/16 → 15/4 | note:C4 ]", "[ 15/4 → 61/16 | note:F3 ]", - "[ 61/16 → 31/8 | note:F3 ]", - "[ 31/8 → 63/16 | note:F3 ]", - "[ 63/16 → 4/1 | note:G3 ]", + "[ 61/16 → 31/8 | note:G3 ]", + "[ 31/8 → 63/16 | note:G3 ]", + "[ 63/16 → 4/1 | note:A3 ]", ] `; @@ -1525,50 +1525,50 @@ exports[`runs examples > example "brand" example index 0 1`] = ` "[ 0/1 → 1/10 | s:hh pan:true ]", "[ 1/10 → 1/5 | s:hh pan:true ]", "[ 1/5 → 3/10 | s:hh pan:false ]", - "[ 3/10 → 2/5 | s:hh pan:true ]", + "[ 3/10 → 2/5 | s:hh pan:false ]", "[ 2/5 → 1/2 | s:hh pan:false ]", "[ 1/2 → 3/5 | s:hh pan:true ]", - "[ 3/5 → 7/10 | s:hh pan:true ]", - "[ 7/10 → 4/5 | s:hh pan:true ]", + "[ 3/5 → 7/10 | s:hh pan:false ]", + "[ 7/10 → 4/5 | s:hh pan:false ]", "[ 4/5 → 9/10 | s:hh pan:true ]", "[ 9/10 → 1/1 | s:hh pan:false ]", - "[ 1/1 → 11/10 | s:hh pan:true ]", - "[ 11/10 → 6/5 | s:hh pan:true ]", - "[ 6/5 → 13/10 | s:hh pan:false ]", + "[ 1/1 → 11/10 | s:hh pan:false ]", + "[ 11/10 → 6/5 | s:hh pan:false ]", + "[ 6/5 → 13/10 | s:hh pan:true ]", "[ 13/10 → 7/5 | s:hh pan:true ]", - "[ 7/5 → 3/2 | s:hh pan:false ]", - "[ 3/2 → 8/5 | s:hh pan:true ]", - "[ 8/5 → 17/10 | s:hh pan:false ]", + "[ 7/5 → 3/2 | s:hh pan:true ]", + "[ 3/2 → 8/5 | s:hh pan:false ]", + "[ 8/5 → 17/10 | s:hh pan:true ]", "[ 17/10 → 9/5 | s:hh pan:true ]", - "[ 9/5 → 19/10 | s:hh pan:false ]", + "[ 9/5 → 19/10 | s:hh pan:true ]", "[ 19/10 → 2/1 | s:hh pan:true ]", "[ 2/1 → 21/10 | s:hh pan:false ]", "[ 21/10 → 11/5 | s:hh pan:true ]", "[ 11/5 → 23/10 | s:hh pan:false ]", - "[ 23/10 → 12/5 | s:hh pan:true ]", + "[ 23/10 → 12/5 | s:hh pan:false ]", "[ 12/5 → 5/2 | s:hh pan:false ]", - "[ 5/2 → 13/5 | s:hh pan:false ]", - "[ 13/5 → 27/10 | s:hh pan:false ]", + "[ 5/2 → 13/5 | s:hh pan:true ]", + "[ 13/5 → 27/10 | s:hh pan:true ]", "[ 27/10 → 14/5 | s:hh pan:true ]", "[ 14/5 → 29/10 | s:hh pan:false ]", - "[ 29/10 → 3/1 | s:hh pan:false ]", - "[ 3/1 → 31/10 | s:hh pan:false ]", + "[ 29/10 → 3/1 | s:hh pan:true ]", + "[ 3/1 → 31/10 | s:hh pan:true ]", "[ 31/10 → 16/5 | s:hh pan:true ]", "[ 16/5 → 33/10 | s:hh pan:true ]", "[ 33/10 → 17/5 | s:hh pan:false ]", - "[ 17/5 → 7/2 | s:hh pan:true ]", + "[ 17/5 → 7/2 | s:hh pan:false ]", "[ 7/2 → 18/5 | s:hh pan:true ]", - "[ 18/5 → 37/10 | s:hh pan:true ]", - "[ 37/10 → 19/5 | s:hh pan:true ]", - "[ 19/5 → 39/10 | s:hh pan:false ]", + "[ 18/5 → 37/10 | s:hh pan:false ]", + "[ 37/10 → 19/5 | s:hh pan:false ]", + "[ 19/5 → 39/10 | s:hh pan:true ]", "[ 39/10 → 4/1 | s:hh pan:true ]", ] `; exports[`runs examples > example "brandBy" example index 0 1`] = ` [ - "[ 0/1 → 1/10 | s:hh pan:false ]", - "[ 1/10 → 1/5 | s:hh pan:false ]", + "[ 0/1 → 1/10 | s:hh pan:true ]", + "[ 1/10 → 1/5 | s:hh pan:true ]", "[ 1/5 → 3/10 | s:hh pan:false ]", "[ 3/10 → 2/5 | s:hh pan:false ]", "[ 2/5 → 1/2 | s:hh pan:false ]", @@ -1577,18 +1577,18 @@ exports[`runs examples > example "brandBy" example index 0 1`] = ` "[ 7/10 → 4/5 | s:hh pan:false ]", "[ 4/5 → 9/10 | s:hh pan:false ]", "[ 9/10 → 1/1 | s:hh pan:false ]", - "[ 1/1 → 11/10 | s:hh pan:true ]", + "[ 1/1 → 11/10 | s:hh pan:false ]", "[ 11/10 → 6/5 | s:hh pan:false ]", "[ 6/5 → 13/10 | s:hh pan:false ]", - "[ 13/10 → 7/5 | s:hh pan:true ]", + "[ 13/10 → 7/5 | s:hh pan:false ]", "[ 7/5 → 3/2 | s:hh pan:false ]", - "[ 3/2 → 8/5 | s:hh pan:true ]", + "[ 3/2 → 8/5 | s:hh pan:false ]", "[ 8/5 → 17/10 | s:hh pan:false ]", "[ 17/10 → 9/5 | s:hh pan:false ]", "[ 9/5 → 19/10 | s:hh pan:false ]", - "[ 19/10 → 2/1 | s:hh pan:false ]", + "[ 19/10 → 2/1 | s:hh pan:true ]", "[ 2/1 → 21/10 | s:hh pan:false ]", - "[ 21/10 → 11/5 | s:hh pan:true ]", + "[ 21/10 → 11/5 | s:hh pan:false ]", "[ 11/5 → 23/10 | s:hh pan:false ]", "[ 23/10 → 12/5 | s:hh pan:false ]", "[ 12/5 → 5/2 | s:hh pan:false ]", @@ -1599,14 +1599,14 @@ exports[`runs examples > example "brandBy" example index 0 1`] = ` "[ 29/10 → 3/1 | s:hh pan:false ]", "[ 3/1 → 31/10 | s:hh pan:false ]", "[ 31/10 → 16/5 | s:hh pan:true ]", - "[ 16/5 → 33/10 | s:hh pan:false ]", + "[ 16/5 → 33/10 | s:hh pan:true ]", "[ 33/10 → 17/5 | s:hh pan:false ]", "[ 17/5 → 7/2 | s:hh pan:false ]", "[ 7/2 → 18/5 | s:hh pan:false ]", "[ 18/5 → 37/10 | s:hh pan:false ]", "[ 37/10 → 19/5 | s:hh pan:false ]", - "[ 19/5 → 39/10 | s:hh pan:false ]", - "[ 39/10 → 4/1 | s:hh pan:true ]", + "[ 19/5 → 39/10 | s:hh pan:true ]", + "[ 39/10 → 4/1 | s:hh pan:false ]", ] `; @@ -1737,100 +1737,100 @@ exports[`runs examples > example "channels" example index 0 1`] = ` exports[`runs examples > example "choose" example index 0 1`] = ` [ - "[ 0/1 → 1/5 | note:c2 s:triangle ]", - "[ 1/5 → 2/5 | note:g2 s:triangle ]", + "[ 0/1 → 1/5 | note:c2 s:sine ]", + "[ 1/5 → 2/5 | note:g2 s:bd n:6 ]", "[ 2/5 → 3/5 | note:g2 s:triangle ]", - "[ 3/5 → 4/5 | note:d2 s:sine ]", - "[ 4/5 → 1/1 | note:f1 s:triangle ]", - "[ 1/1 → 6/5 | note:c2 s:sine ]", + "[ 3/5 → 4/5 | note:d2 s:bd n:6 ]", + "[ 4/5 → 1/1 | note:f1 s:sine ]", + "[ 1/1 → 6/5 | note:c2 s:triangle ]", "[ 6/5 → 7/5 | note:g2 s:triangle ]", - "[ 7/5 → 8/5 | note:g2 s:bd n:6 ]", + "[ 7/5 → 8/5 | note:g2 s:sine ]", "[ 8/5 → 9/5 | note:d2 s:triangle ]", - "[ 9/5 → 2/1 | note:f1 s:triangle ]", - "[ 2/1 → 11/5 | note:c2 s:triangle ]", + "[ 9/5 → 2/1 | note:f1 s:sine ]", + "[ 2/1 → 11/5 | note:c2 s:bd n:6 ]", "[ 11/5 → 12/5 | note:g2 s:bd n:6 ]", "[ 12/5 → 13/5 | note:g2 s:bd n:6 ]", - "[ 13/5 → 14/5 | note:d2 s:bd n:6 ]", - "[ 14/5 → 3/1 | note:f1 s:bd n:6 ]", - "[ 3/1 → 16/5 | note:c2 s:triangle ]", - "[ 16/5 → 17/5 | note:g2 s:triangle ]", - "[ 17/5 → 18/5 | note:g2 s:sine ]", + "[ 13/5 → 14/5 | note:d2 s:sine ]", + "[ 14/5 → 3/1 | note:f1 s:triangle ]", + "[ 3/1 → 16/5 | note:c2 s:sine ]", + "[ 16/5 → 17/5 | note:g2 s:sine ]", + "[ 17/5 → 18/5 | note:g2 s:triangle ]", "[ 18/5 → 19/5 | note:d2 s:triangle ]", - "[ 19/5 → 4/1 | note:f1 s:bd n:6 ]", + "[ 19/5 → 4/1 | note:f1 s:sine ]", ] `; exports[`runs examples > example "chooseCycles" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:hh ]", - "[ 1/8 → 1/4 | s:bd ]", - "[ 1/4 → 3/8 | s:hh ]", - "[ 3/8 → 1/2 | s:hh ]", + "[ 0/1 → 1/8 | s:bd ]", + "[ 1/8 → 1/4 | s:hh ]", + "[ 1/4 → 3/8 | s:sd ]", + "[ 3/8 → 1/2 | s:bd ]", "[ 1/2 → 5/8 | s:bd ]", - "[ 5/8 → 3/4 | s:sd ]", - "[ 3/4 → 7/8 | s:bd ]", - "[ 7/8 → 1/1 | s:sd ]", - "[ 1/1 → 9/8 | s:hh ]", + "[ 5/8 → 3/4 | s:bd ]", + "[ 3/4 → 7/8 | s:hh ]", + "[ 7/8 → 1/1 | s:bd ]", + "[ 1/1 → 9/8 | s:sd ]", "[ 9/8 → 5/4 | s:hh ]", - "[ 5/4 → 11/8 | s:sd ]", - "[ 11/8 → 3/2 | s:sd ]", - "[ 3/2 → 13/8 | s:sd ]", - "[ 13/8 → 7/4 | s:bd ]", - "[ 7/4 → 15/8 | s:bd ]", + "[ 5/4 → 11/8 | s:bd ]", + "[ 11/8 → 3/2 | s:hh ]", + "[ 3/2 → 13/8 | s:bd ]", + "[ 13/8 → 7/4 | s:sd ]", + "[ 7/4 → 15/8 | s:hh ]", "[ 15/8 → 2/1 | s:bd ]", "[ 2/1 → 17/8 | s:bd ]", "[ 17/8 → 9/4 | s:sd ]", "[ 9/4 → 19/8 | s:sd ]", - "[ 19/8 → 5/2 | s:sd ]", - "[ 5/2 → 21/8 | s:bd ]", - "[ 21/8 → 11/4 | s:hh ]", + "[ 19/8 → 5/2 | s:bd ]", + "[ 5/2 → 21/8 | s:hh ]", + "[ 21/8 → 11/4 | s:bd ]", "[ 11/4 → 23/8 | s:sd ]", - "[ 23/8 → 3/1 | s:sd ]", - "[ 3/1 → 25/8 | s:hh ]", + "[ 23/8 → 3/1 | s:bd ]", + "[ 3/1 → 25/8 | s:sd ]", "[ 25/8 → 13/4 | s:sd ]", - "[ 13/4 → 27/8 | s:hh ]", + "[ 13/4 → 27/8 | s:bd ]", "[ 27/8 → 7/2 | s:bd ]", - "[ 7/2 → 29/8 | s:sd ]", + "[ 7/2 → 29/8 | s:bd ]", "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:bd ]", - "[ 31/8 → 4/1 | s:sd ]", + "[ 15/4 → 31/8 | s:hh ]", + "[ 31/8 → 4/1 | s:bd ]", ] `; exports[`runs examples > example "chooseCycles" example index 1 1`] = ` [ - "[ 0/1 → 1/8 | s:hh ]", - "[ 1/8 → 1/4 | s:bd ]", - "[ 1/4 → 3/8 | s:hh ]", - "[ 3/8 → 1/2 | s:hh ]", + "[ 0/1 → 1/8 | s:bd ]", + "[ 1/8 → 1/4 | s:hh ]", + "[ 1/4 → 3/8 | s:sd ]", + "[ 3/8 → 1/2 | s:bd ]", "[ 1/2 → 5/8 | s:bd ]", - "[ 5/8 → 3/4 | s:sd ]", - "[ 3/4 → 7/8 | s:bd ]", - "[ 7/8 → 1/1 | s:sd ]", - "[ 1/1 → 9/8 | s:hh ]", + "[ 5/8 → 3/4 | s:bd ]", + "[ 3/4 → 7/8 | s:hh ]", + "[ 7/8 → 1/1 | s:bd ]", + "[ 1/1 → 9/8 | s:sd ]", "[ 9/8 → 5/4 | s:hh ]", - "[ 5/4 → 11/8 | s:sd ]", - "[ 11/8 → 3/2 | s:sd ]", - "[ 3/2 → 13/8 | s:sd ]", - "[ 13/8 → 7/4 | s:bd ]", - "[ 7/4 → 15/8 | s:bd ]", + "[ 5/4 → 11/8 | s:bd ]", + "[ 11/8 → 3/2 | s:hh ]", + "[ 3/2 → 13/8 | s:bd ]", + "[ 13/8 → 7/4 | s:sd ]", + "[ 7/4 → 15/8 | s:hh ]", "[ 15/8 → 2/1 | s:bd ]", "[ 2/1 → 17/8 | s:bd ]", "[ 17/8 → 9/4 | s:sd ]", "[ 9/4 → 19/8 | s:sd ]", - "[ 19/8 → 5/2 | s:sd ]", - "[ 5/2 → 21/8 | s:bd ]", - "[ 21/8 → 11/4 | s:hh ]", + "[ 19/8 → 5/2 | s:bd ]", + "[ 5/2 → 21/8 | s:hh ]", + "[ 21/8 → 11/4 | s:bd ]", "[ 11/4 → 23/8 | s:sd ]", - "[ 23/8 → 3/1 | s:sd ]", - "[ 3/1 → 25/8 | s:hh ]", + "[ 23/8 → 3/1 | s:bd ]", + "[ 3/1 → 25/8 | s:sd ]", "[ 25/8 → 13/4 | s:sd ]", - "[ 13/4 → 27/8 | s:hh ]", + "[ 13/4 → 27/8 | s:bd ]", "[ 27/8 → 7/2 | s:bd ]", - "[ 7/2 → 29/8 | s:sd ]", + "[ 7/2 → 29/8 | s:bd ]", "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:bd ]", - "[ 31/8 → 4/1 | s:sd ]", + "[ 15/4 → 31/8 | s:hh ]", + "[ 31/8 → 4/1 | s:bd ]", ] `; @@ -2494,57 +2494,81 @@ exports[`runs examples > example "decay" example index 0 1`] = ` exports[`runs examples > example "degrade" example index 0 1`] = ` [ "[ 1/8 → 1/4 | s:hh ]", - "[ 3/8 → 1/2 | s:hh ]", - "[ 5/8 → 3/4 | s:hh ]", - "[ 7/8 → 1/1 | s:hh ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 9/8 → 5/4 | s:hh ]", "[ 5/4 → 11/8 | s:hh ]", - "[ 13/8 → 7/4 | s:hh ]", + "[ 3/2 → 13/8 | s:hh ]", "[ 15/8 → 2/1 | s:hh ]", "[ 2/1 → 17/8 | s:hh ]", + "[ 17/8 → 9/4 | s:hh ]", "[ 19/8 → 5/2 | s:hh ]", - "[ 5/2 → 21/8 | s:hh ]", - "[ 23/8 → 3/1 | s:hh ]", - "[ 3/1 → 25/8 | s:hh ]", - "[ 25/8 → 13/4 | s:hh ]", + "[ 11/4 → 23/8 | s:hh ]", "[ 13/4 → 27/8 | s:hh ]", "[ 29/8 → 15/4 | s:hh ]", "[ 15/4 → 31/8 | s:hh ]", + "[ 31/8 → 4/1 | s:hh ]", ] `; exports[`runs examples > example "degrade" example index 1 1`] = ` [ + "[ 1/8 → 1/4 | s:hh ]", "[ 1/4 → 3/8 | s:hh ]", - "[ 3/8 → 1/2 | s:hh ]", - "[ 5/8 → 3/4 | s:hh ]", - "[ 7/8 → 1/1 | s:hh ]", + "[ 3/4 → 7/8 | s:hh ]", "[ 1/1 → 9/8 | s:hh ]", "[ 9/8 → 5/4 | s:hh ]", - "[ 5/4 → 11/8 | s:hh ]", - "[ 11/8 → 3/2 | s:hh ]", - "[ 3/2 → 13/8 | s:hh ]", + "[ 13/8 → 7/4 | s:hh ]", + "[ 7/4 → 15/8 | s:hh ]", "[ 17/8 → 9/4 | s:hh ]", "[ 9/4 → 19/8 | s:hh ]", - "[ 19/8 → 5/2 | s:hh ]", + "[ 5/2 → 21/8 | s:hh ]", "[ 11/4 → 23/8 | s:hh ]", - "[ 23/8 → 3/1 | s:hh ]", "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", - "[ 7/2 → 29/8 | s:hh ]", - "[ 31/8 → 4/1 | s:hh ]", + "[ 15/4 → 31/8 | s:hh ]", ] `; exports[`runs examples > example "degradeBy" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:hh ]", "[ 1/8 → 1/4 | s:hh ]", + "[ 1/4 → 3/8 | s:hh ]", "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:hh ]", - "[ 5/8 → 3/4 | s:hh ]", - "[ 3/4 → 7/8 | s:hh ]", "[ 7/8 → 1/1 | s:hh ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 9/8 → 5/4 | s:hh ]", "[ 5/4 → 11/8 | s:hh ]", + "[ 3/2 → 13/8 | s:hh ]", + "[ 13/8 → 7/4 | s:hh ]", + "[ 7/4 → 15/8 | s:hh ]", + "[ 15/8 → 2/1 | s:hh ]", + "[ 2/1 → 17/8 | s:hh ]", + "[ 17/8 → 9/4 | s:hh ]", + "[ 9/4 → 19/8 | s:hh ]", + "[ 19/8 → 5/2 | s:hh ]", + "[ 5/2 → 21/8 | s:hh ]", + "[ 11/4 → 23/8 | s:hh ]", + "[ 3/1 → 25/8 | s:hh ]", + "[ 25/8 → 13/4 | s:hh ]", + "[ 13/4 → 27/8 | s:hh ]", + "[ 27/8 → 7/2 | s:hh ]", + "[ 7/2 → 29/8 | s:hh ]", + "[ 29/8 → 15/4 | s:hh ]", + "[ 15/4 → 31/8 | s:hh ]", + "[ 31/8 → 4/1 | s:hh ]", +] +`; + +exports[`runs examples > example "degradeBy" example index 1 1`] = ` +[ + "[ 1/8 → 1/4 | s:hh ]", + "[ 1/4 → 3/8 | s:hh ]", + "[ 3/8 → 1/2 | s:hh ]", + "[ 3/4 → 7/8 | s:hh ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 9/8 → 5/4 | s:hh ]", + "[ 11/8 → 3/2 | s:hh ]", "[ 13/8 → 7/4 | s:hh ]", "[ 7/4 → 15/8 | s:hh ]", "[ 15/8 → 2/1 | s:hh ]", @@ -2555,79 +2579,45 @@ exports[`runs examples > example "degradeBy" example index 0 1`] = ` "[ 5/2 → 21/8 | s:hh ]", "[ 21/8 → 11/4 | s:hh ]", "[ 11/4 → 23/8 | s:hh ]", - "[ 23/8 → 3/1 | s:hh ]", "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", - "[ 13/4 → 27/8 | s:hh ]", + "[ 27/8 → 7/2 | s:hh ]", "[ 7/2 → 29/8 | s:hh ]", "[ 29/8 → 15/4 | s:hh ]", "[ 15/4 → 31/8 | s:hh ]", ] `; -exports[`runs examples > example "degradeBy" example index 1 1`] = ` -[ - "[ 0/1 → 1/8 | s:hh ]", - "[ 1/4 → 3/8 | s:hh ]", - "[ 3/8 → 1/2 | s:hh ]", - "[ 5/8 → 3/4 | s:hh ]", - "[ 3/4 → 7/8 | s:hh ]", - "[ 7/8 → 1/1 | s:hh ]", - "[ 1/1 → 9/8 | s:hh ]", - "[ 9/8 → 5/4 | s:hh ]", - "[ 5/4 → 11/8 | s:hh ]", - "[ 11/8 → 3/2 | s:hh ]", - "[ 3/2 → 13/8 | s:hh ]", - "[ 15/8 → 2/1 | s:hh ]", - "[ 17/8 → 9/4 | s:hh ]", - "[ 9/4 → 19/8 | s:hh ]", - "[ 19/8 → 5/2 | s:hh ]", - "[ 21/8 → 11/4 | s:hh ]", - "[ 11/4 → 23/8 | s:hh ]", - "[ 23/8 → 3/1 | s:hh ]", - "[ 3/1 → 25/8 | s:hh ]", - "[ 25/8 → 13/4 | s:hh ]", - "[ 13/4 → 27/8 | s:hh ]", - "[ 7/2 → 29/8 | s:hh ]", - "[ 29/8 → 15/4 | s:hh ]", - "[ 31/8 → 4/1 | s:hh ]", -] -`; - exports[`runs examples > example "degradeBy" example index 2 1`] = ` [ - "[ 1/16 → 1/8 | s:bd ]", "[ 1/8 → 3/16 | s:bd ]", - "[ 3/16 → 1/4 | s:bd ]", + "[ 1/4 → 5/16 | s:bd ]", "[ 5/16 → 3/8 | s:bd ]", - "[ 3/8 → 7/16 | s:bd ]", "[ 1/2 → 9/16 | s:bd ]", - "[ 13/16 → 7/8 | s:bd ]", - "[ 7/8 → 15/16 | s:bd ]", - "[ 17/16 → 9/8 | s:bd ]", + "[ 9/16 → 5/8 | s:bd ]", + "[ 11/16 → 3/4 | s:bd ]", + "[ 15/16 → 1/1 | s:bd ]", "[ 9/8 → 19/16 | s:bd ]", - "[ 19/16 → 5/4 | s:bd ]", + "[ 5/4 → 21/16 | s:bd ]", "[ 21/16 → 11/8 | s:bd ]", - "[ 11/8 → 23/16 | s:bd ]", "[ 3/2 → 25/16 | s:bd ]", - "[ 29/16 → 15/8 | s:bd ]", - "[ 15/8 → 31/16 | s:bd ]", - "[ 33/16 → 17/8 | s:bd ]", + "[ 25/16 → 13/8 | s:bd ]", + "[ 27/16 → 7/4 | s:bd ]", + "[ 31/16 → 2/1 | s:bd ]", "[ 17/8 → 35/16 | s:bd ]", - "[ 35/16 → 9/4 | s:bd ]", + "[ 9/4 → 37/16 | s:bd ]", "[ 37/16 → 19/8 | s:bd ]", - "[ 19/8 → 39/16 | s:bd ]", "[ 5/2 → 41/16 | s:bd ]", - "[ 45/16 → 23/8 | s:bd ]", - "[ 23/8 → 47/16 | s:bd ]", - "[ 49/16 → 25/8 | s:bd ]", + "[ 41/16 → 21/8 | s:bd ]", + "[ 43/16 → 11/4 | s:bd ]", + "[ 47/16 → 3/1 | s:bd ]", "[ 25/8 → 51/16 | s:bd ]", - "[ 51/16 → 13/4 | s:bd ]", + "[ 13/4 → 53/16 | s:bd ]", "[ 53/16 → 27/8 | s:bd ]", - "[ 27/8 → 55/16 | s:bd ]", "[ 7/2 → 57/16 | s:bd ]", - "[ 61/16 → 31/8 | s:bd ]", - "[ 31/8 → 63/16 | s:bd ]", + "[ 57/16 → 29/8 | s:bd ]", + "[ 59/16 → 15/4 | s:bd ]", + "[ 63/16 → 4/1 | s:bd ]", ] `; @@ -2950,14 +2940,14 @@ exports[`runs examples > example "distorttype" example index 0 1`] = ` exports[`runs examples > example "distorttype" example index 1 1`] = ` [ - "[ (0/1 → 1/2) ⇝ 1/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:3.71662513515912 distorttype:fold ]", - "[ 0/1 ⇜ (1/2 → 1/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:3.71662513515912 distorttype:fold ]", - "[ (1/1 → 3/2) ⇝ 2/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1.8276505067478865 distorttype:chebyshev ]", - "[ 1/1 ⇜ (3/2 → 2/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1.8276505067478865 distorttype:chebyshev ]", - "[ (2/1 → 5/2) ⇝ 3/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:5.068953953683376 distorttype:scurve ]", - "[ 2/1 ⇜ (5/2 → 3/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:5.068953953683376 distorttype:scurve ]", - "[ (3/1 → 7/2) ⇝ 4/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:5.303743980126455 distorttype:diode ]", - "[ 3/1 ⇜ (7/2 → 4/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:5.303743980126455 distorttype:diode ]", + "[ (0/1 → 1/2) ⇝ 1/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1 distorttype:fold ]", + "[ 0/1 ⇜ (1/2 → 1/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1 distorttype:fold ]", + "[ (1/1 → 3/2) ⇝ 2/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:4.6367951557040215 distorttype:chebyshev ]", + "[ 1/1 ⇜ (3/2 → 2/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:4.6367951557040215 distorttype:chebyshev ]", + "[ (2/1 → 5/2) ⇝ 3/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:7.716689839959145 distorttype:scurve ]", + "[ 2/1 ⇜ (5/2 → 3/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:7.716689839959145 distorttype:scurve ]", + "[ (3/1 → 7/2) ⇝ 4/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:2.5210237745195627 distorttype:diode ]", + "[ 3/1 ⇜ (7/2 → 4/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:2.5210237745195627 distorttype:diode ]", ] `; @@ -2984,38 +2974,38 @@ exports[`runs examples > example "distortvol" example index 0 1`] = ` exports[`runs examples > example "djf" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | note:C4 s:supersaw djf:0.5 ]", + "[ 0/1 → 1/8 | note:D3 s:supersaw djf:0.5 ]", "[ 1/8 → 1/4 | note:G4 s:supersaw djf:0.5 ]", - "[ 1/4 → 3/8 | note:F3 s:supersaw djf:0.5 ]", - "[ 3/8 → 1/2 | note:C5 s:supersaw djf:0.5 ]", - "[ 1/2 → 5/8 | note:Bb3 s:supersaw djf:0.5 ]", - "[ 5/8 → 3/4 | note:A4 s:supersaw djf:0.5 ]", - "[ 3/4 → 7/8 | note:A3 s:supersaw djf:0.5 ]", - "[ 7/8 → 1/1 | note:D5 s:supersaw djf:0.5 ]", - "[ 1/1 → 9/8 | note:Eb3 s:supersaw djf:0.3 ]", - "[ 9/8 → 5/4 | note:D3 s:supersaw djf:0.3 ]", - "[ 5/4 → 11/8 | note:Eb5 s:supersaw djf:0.3 ]", - "[ 11/8 → 3/2 | note:D3 s:supersaw djf:0.3 ]", - "[ 3/2 → 13/8 | note:F3 s:supersaw djf:0.3 ]", - "[ 13/8 → 7/4 | note:G4 s:supersaw djf:0.3 ]", - "[ 7/4 → 15/8 | note:Bb3 s:supersaw djf:0.3 ]", - "[ 15/8 → 2/1 | note:G4 s:supersaw djf:0.3 ]", - "[ 2/1 → 17/8 | note:F4 s:supersaw djf:0.2 ]", - "[ 17/8 → 9/4 | note:D4 s:supersaw djf:0.2 ]", - "[ 9/4 → 19/8 | note:G3 s:supersaw djf:0.2 ]", + "[ 1/4 → 3/8 | note:Bb3 s:supersaw djf:0.5 ]", + "[ 3/8 → 1/2 | note:C4 s:supersaw djf:0.5 ]", + "[ 1/2 → 5/8 | note:A3 s:supersaw djf:0.5 ]", + "[ 5/8 → 3/4 | note:F3 s:supersaw djf:0.5 ]", + "[ 3/4 → 7/8 | note:G3 s:supersaw djf:0.5 ]", + "[ 7/8 → 1/1 | note:C4 s:supersaw djf:0.5 ]", + "[ 1/1 → 9/8 | note:Eb4 s:supersaw djf:0.3 ]", + "[ 9/8 → 5/4 | note:G4 s:supersaw djf:0.3 ]", + "[ 5/4 → 11/8 | note:A4 s:supersaw djf:0.3 ]", + "[ 11/8 → 3/2 | note:F3 s:supersaw djf:0.3 ]", + "[ 3/2 → 13/8 | note:F4 s:supersaw djf:0.3 ]", + "[ 13/8 → 7/4 | note:D4 s:supersaw djf:0.3 ]", + "[ 7/4 → 15/8 | note:G3 s:supersaw djf:0.3 ]", + "[ 15/8 → 2/1 | note:F4 s:supersaw djf:0.3 ]", + "[ 2/1 → 17/8 | note:Eb5 s:supersaw djf:0.2 ]", + "[ 17/8 → 9/4 | note:D5 s:supersaw djf:0.2 ]", + "[ 9/4 → 19/8 | note:Bb3 s:supersaw djf:0.2 ]", "[ 19/8 → 5/2 | note:C5 s:supersaw djf:0.2 ]", - "[ 5/2 → 21/8 | note:A4 s:supersaw djf:0.2 ]", - "[ 21/8 → 11/4 | note:Bb3 s:supersaw djf:0.2 ]", - "[ 11/4 → 23/8 | note:Bb3 s:supersaw djf:0.2 ]", - "[ 23/8 → 3/1 | note:C5 s:supersaw djf:0.2 ]", - "[ 3/1 → 25/8 | note:F4 s:supersaw djf:0.75 ]", - "[ 25/8 → 13/4 | note:F4 s:supersaw djf:0.75 ]", - "[ 13/4 → 27/8 | note:D5 s:supersaw djf:0.75 ]", - "[ 27/8 → 7/2 | note:D3 s:supersaw djf:0.75 ]", - "[ 7/2 → 29/8 | note:G3 s:supersaw djf:0.75 ]", - "[ 29/8 → 15/4 | note:F4 s:supersaw djf:0.75 ]", - "[ 15/4 → 31/8 | note:F4 s:supersaw djf:0.75 ]", - "[ 31/8 → 4/1 | note:F3 s:supersaw djf:0.75 ]", + "[ 5/2 → 21/8 | note:D4 s:supersaw djf:0.2 ]", + "[ 21/8 → 11/4 | note:F3 s:supersaw djf:0.2 ]", + "[ 11/4 → 23/8 | note:G4 s:supersaw djf:0.2 ]", + "[ 23/8 → 3/1 | note:D3 s:supersaw djf:0.2 ]", + "[ 3/1 → 25/8 | note:G3 s:supersaw djf:0.75 ]", + "[ 25/8 → 13/4 | note:Bb3 s:supersaw djf:0.75 ]", + "[ 13/4 → 27/8 | note:Eb5 s:supersaw djf:0.75 ]", + "[ 27/8 → 7/2 | note:C4 s:supersaw djf:0.75 ]", + "[ 7/2 → 29/8 | note:C4 s:supersaw djf:0.75 ]", + "[ 29/8 → 15/4 | note:Eb5 s:supersaw djf:0.75 ]", + "[ 15/4 → 31/8 | note:Bb4 s:supersaw djf:0.75 ]", + "[ 31/8 → 4/1 | note:A4 s:supersaw djf:0.75 ]", ] `; @@ -5173,71 +5163,71 @@ exports[`runs examples > example "invert" example index 0 1`] = ` exports[`runs examples > example "irand" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:F3 ]", - "[ 1/4 → 3/8 | note:D3 ]", - "[ 3/8 → 1/2 | note:Bb3 ]", + "[ 0/1 → 1/4 | note:C3 ]", + "[ 1/4 → 3/8 | note:Eb3 ]", + "[ 3/8 → 1/2 | note:F3 ]", "[ 1/2 → 3/4 | note:Eb3 ]", - "[ 3/4 → 5/6 | note:Eb3 ]", - "[ 5/6 → 11/12 | note:Ab3 ]", - "[ 11/12 → 1/1 | note:D3 ]", - "[ 1/1 → 5/4 | note:C3 ]", - "[ 5/4 → 11/8 | note:C4 ]", - "[ 11/8 → 3/2 | note:C3 ]", - "[ 3/2 → 7/4 | note:D3 ]", - "[ 7/4 → 11/6 | note:Eb3 ]", - "[ 11/6 → 23/12 | note:C3 ]", - "[ 23/12 → 2/1 | note:Ab3 ]", - "[ 2/1 → 9/4 | note:G3 ]", - "[ 9/4 → 19/8 | note:D3 ]", + "[ 3/4 → 5/6 | note:D3 ]", + "[ 5/6 → 11/12 | note:C3 ]", + "[ 11/12 → 1/1 | note:C4 ]", + "[ 1/1 → 5/4 | note:G3 ]", + "[ 5/4 → 11/8 | note:Ab3 ]", + "[ 11/8 → 3/2 | note:D3 ]", + "[ 3/2 → 7/4 | note:G3 ]", + "[ 7/4 → 11/6 | note:D3 ]", + "[ 11/6 → 23/12 | note:Bb3 ]", + "[ 23/12 → 2/1 | note:Eb3 ]", + "[ 2/1 → 9/4 | note:C4 ]", + "[ 9/4 → 19/8 | note:Eb3 ]", "[ 19/8 → 5/2 | note:Bb3 ]", - "[ 5/2 → 11/4 | note:Ab3 ]", - "[ 11/4 → 17/6 | note:Eb3 ]", - "[ 17/6 → 35/12 | note:Ab3 ]", - "[ 35/12 → 3/1 | note:D3 ]", - "[ 3/1 → 13/4 | note:G3 ]", + "[ 5/2 → 11/4 | note:F3 ]", + "[ 11/4 → 17/6 | note:Ab3 ]", + "[ 17/6 → 35/12 | note:D3 ]", + "[ 35/12 → 3/1 | note:F3 ]", + "[ 3/1 → 13/4 | note:D3 ]", "[ 13/4 → 27/8 | note:C4 ]", - "[ 27/8 → 7/2 | note:C3 ]", - "[ 7/2 → 15/4 | note:D3 ]", - "[ 15/4 → 23/6 | note:G3 ]", + "[ 27/8 → 7/2 | note:F3 ]", + "[ 7/2 → 15/4 | note:F3 ]", + "[ 15/4 → 23/6 | note:Bb3 ]", "[ 23/6 → 47/12 | note:Ab3 ]", - "[ 47/12 → 4/1 | note:Ab3 ]", + "[ 47/12 → 4/1 | note:C3 ]", ] `; exports[`runs examples > example "irbegin" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", "[ 1/8 → 1/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.0625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", - "[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.5 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", "[ 19/8 → 5/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", - "[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.75 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", ] `; @@ -5264,38 +5254,38 @@ exports[`runs examples > example "iresponse" example index 0 1`] = ` exports[`runs examples > example "irspeed" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", "[ 1/8 → 1/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.0625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", - "[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.5 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", "[ 19/8 → 5/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", - "[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.75 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", ] `; @@ -6985,34 +6975,34 @@ exports[`runs examples > example "often" example index 0 1`] = ` "[ 0/1 → 1/8 | s:hh speed:0.5 ]", "[ 1/8 → 1/4 | s:hh speed:0.5 ]", "[ 1/4 → 3/8 | s:hh speed:0.5 ]", - "[ 3/8 → 1/2 | s:hh ]", + "[ 3/8 → 1/2 | s:hh speed:0.5 ]", "[ 1/2 → 5/8 | s:hh speed:0.5 ]", "[ 5/8 → 3/4 | s:hh speed:0.5 ]", "[ 3/4 → 7/8 | s:hh speed:0.5 ]", - "[ 7/8 → 1/1 | s:hh ]", + "[ 7/8 → 1/1 | s:hh speed:0.5 ]", "[ 1/1 → 9/8 | s:hh speed:0.5 ]", "[ 9/8 → 5/4 | s:hh speed:0.5 ]", - "[ 5/4 → 11/8 | s:hh ]", + "[ 5/4 → 11/8 | s:hh speed:0.5 ]", "[ 11/8 → 3/2 | s:hh speed:0.5 ]", "[ 3/2 → 13/8 | s:hh speed:0.5 ]", "[ 13/8 → 7/4 | s:hh speed:0.5 ]", "[ 7/4 → 15/8 | s:hh speed:0.5 ]", "[ 15/8 → 2/1 | s:hh speed:0.5 ]", - "[ 2/1 → 17/8 | s:hh speed:0.5 ]", - "[ 17/8 → 9/4 | s:hh speed:0.5 ]", + "[ 2/1 → 17/8 | s:hh ]", + "[ 17/8 → 9/4 | s:hh ]", "[ 9/4 → 19/8 | s:hh speed:0.5 ]", "[ 19/8 → 5/2 | s:hh ]", "[ 5/2 → 21/8 | s:hh speed:0.5 ]", "[ 21/8 → 11/4 | s:hh speed:0.5 ]", "[ 11/4 → 23/8 | s:hh speed:0.5 ]", - "[ 23/8 → 3/1 | s:hh ]", + "[ 23/8 → 3/1 | s:hh speed:0.5 ]", "[ 3/1 → 25/8 | s:hh speed:0.5 ]", "[ 25/8 → 13/4 | s:hh speed:0.5 ]", "[ 13/4 → 27/8 | s:hh ]", "[ 27/8 → 7/2 | s:hh speed:0.5 ]", "[ 7/2 → 29/8 | s:hh speed:0.5 ]", - "[ 29/8 → 15/4 | s:hh speed:0.5 ]", - "[ 15/4 → 31/8 | s:hh speed:0.5 ]", + "[ 29/8 → 15/4 | s:hh ]", + "[ 15/4 → 31/8 | s:hh ]", "[ 31/8 → 4/1 | s:hh speed:0.5 ]", ] `; @@ -7283,54 +7273,54 @@ exports[`runs examples > example "penv" example index 0 1`] = ` exports[`runs examples > example "perlin" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:hh cutoff:3410.6697876704857 ]", - "[ 0/1 → 1/4 | s:bd cutoff:3410.6697876704857 ]", - "[ 1/8 → 1/4 | s:hh cutoff:3378.181624527514 ]", - "[ 1/4 → 3/8 | s:hh cutoff:3201.164370596416 ]", - "[ 1/4 → 1/2 | s:bd cutoff:3201.164370596416 ]", - "[ 3/8 → 1/2 | s:hh cutoff:2853.676907017785 ]", - "[ 1/2 → 5/8 | s:hh cutoff:2398.7190938787535 ]", - "[ 1/2 → 3/4 | s:bd cutoff:2398.7190938787535 ]", - "[ 5/8 → 3/4 | s:hh cutoff:1943.7612807397215 ]", - "[ 3/4 → 7/8 | s:hh cutoff:1596.2738171610908 ]", - "[ 3/4 → 1/1 | s:bd cutoff:1596.2738171610908 ]", - "[ 7/8 → 1/1 | s:hh cutoff:1419.2565632299932 ]", - "[ 1/1 → 9/8 | s:hh cutoff:1386.7684000870213 ]", - "[ 1/1 → 5/4 | s:bd cutoff:1386.7684000870213 ]", - "[ 9/8 → 5/4 | s:hh cutoff:1442.5150435813734 ]", - "[ 5/4 → 11/8 | s:hh cutoff:1746.2600630772158 ]", - "[ 5/4 → 3/2 | s:bd cutoff:1746.2600630772158 ]", - "[ 11/8 → 3/2 | s:hh cutoff:2342.5159876004573 ]", - "[ 3/2 → 13/8 | s:hh cutoff:3123.1809609453194 ]", - "[ 3/2 → 7/4 | s:bd cutoff:3123.1809609453194 ]", - "[ 13/8 → 7/4 | s:hh cutoff:3903.8459342901815 ]", - "[ 7/4 → 15/8 | s:hh cutoff:4500.101858813423 ]", - "[ 7/4 → 2/1 | s:bd cutoff:4500.101858813423 ]", - "[ 15/8 → 2/1 | s:hh cutoff:4803.846878309266 ]", - "[ 2/1 → 17/8 | s:hh cutoff:4859.5935218036175 ]", - "[ 2/1 → 9/4 | s:bd cutoff:4859.5935218036175 ]", - "[ 17/8 → 9/4 | s:hh cutoff:4863.631636751641 ]", - "[ 9/4 → 19/8 | s:hh cutoff:4885.633989301141 ]", - "[ 9/4 → 5/2 | s:bd cutoff:4885.633989301141 ]", - "[ 19/8 → 5/2 | s:hh cutoff:4928.824929790842 ]", - "[ 5/2 → 21/8 | s:hh cutoff:4985.37389311241 ]", - "[ 5/2 → 11/4 | s:bd cutoff:4985.37389311241 ]", - "[ 21/8 → 11/4 | s:hh cutoff:5041.922856433977 ]", - "[ 11/4 → 23/8 | s:hh cutoff:5085.113796923679 ]", - "[ 11/4 → 3/1 | s:bd cutoff:5085.113796923679 ]", - "[ 23/8 → 3/1 | s:hh cutoff:5107.116149473179 ]", - "[ 3/1 → 25/8 | s:hh cutoff:5111.154264421202 ]", - "[ 3/1 → 13/4 | s:bd cutoff:5111.154264421202 ]", - "[ 25/8 → 13/4 | s:hh cutoff:5054.286698772394 ]", - "[ 13/4 → 27/8 | s:hh cutoff:4744.434145256264 ]", - "[ 13/4 → 7/2 | s:bd cutoff:4744.434145256264 ]", - "[ 27/8 → 7/2 | s:hh cutoff:4136.189041947908 ]", - "[ 7/2 → 29/8 | s:hh cutoff:3339.826896379236 ]", - "[ 7/2 → 15/4 | s:bd cutoff:3339.826896379236 ]", - "[ 29/8 → 15/4 | s:hh cutoff:2543.464750810564 ]", - "[ 15/4 → 31/8 | s:hh cutoff:1935.2196475022083 ]", - "[ 15/4 → 4/1 | s:bd cutoff:1935.2196475022083 ]", - "[ 31/8 → 4/1 | s:hh cutoff:1625.3670939860783 ]", + "[ 0/1 → 1/8 | s:hh cutoff:500 ]", + "[ 0/1 → 1/4 | s:bd cutoff:500 ]", + "[ 1/8 → 1/4 | s:hh cutoff:562.5486401770559 ]", + "[ 1/4 → 3/8 | s:hh cutoff:903.3554895067937 ]", + "[ 1/4 → 1/2 | s:bd cutoff:903.3554895067937 ]", + "[ 3/8 → 1/2 | s:hh cutoff:1572.364329119182 ]", + "[ 1/2 → 5/8 | s:hh cutoff:2448.2831191271544 ]", + "[ 1/2 → 3/4 | s:bd cutoff:2448.2831191271544 ]", + "[ 5/8 → 3/4 | s:hh cutoff:3324.2019091351267 ]", + "[ 3/4 → 7/8 | s:hh cutoff:3993.210748747515 ]", + "[ 3/4 → 1/1 | s:bd cutoff:3993.210748747515 ]", + "[ 7/8 → 1/1 | s:hh cutoff:4334.017598077253 ]", + "[ 1/1 → 9/8 | s:hh cutoff:4396.566238254309 ]", + "[ 1/1 → 5/4 | s:bd cutoff:4396.566238254309 ]", + "[ 9/8 → 5/4 | s:hh cutoff:4449.536839055554 ]", + "[ 5/4 → 11/8 | s:hh cutoff:4738.156120227359 ]", + "[ 5/4 → 3/2 | s:bd cutoff:4738.156120227359 ]", + "[ 11/8 → 3/2 | s:hh cutoff:5304.71999875931 ]", + "[ 3/2 → 13/8 | s:hh cutoff:6046.5098191052675 ]", + "[ 3/2 → 7/4 | s:bd cutoff:6046.5098191052675 ]", + "[ 13/8 → 7/4 | s:hh cutoff:6788.299639451225 ]", + "[ 7/4 → 15/8 | s:hh cutoff:7354.863517983176 ]", + "[ 7/4 → 2/1 | s:bd cutoff:7354.863517983176 ]", + "[ 15/8 → 2/1 | s:hh cutoff:7643.482799154981 ]", + "[ 2/1 → 17/8 | s:hh cutoff:7696.453399956226 ]", + "[ 2/1 → 9/4 | s:bd cutoff:7696.453399956226 ]", + "[ 17/8 → 9/4 | s:hh cutoff:7607.093996059746 ]", + "[ 9/4 → 19/8 | s:hh cutoff:7120.204164182724 ]", + "[ 9/4 → 5/2 | s:bd cutoff:7120.204164182724 ]", + "[ 19/8 → 5/2 | s:hh cutoff:6164.432289046601 ]", + "[ 5/2 → 21/8 | s:hh cutoff:4913.0608648993075 ]", + "[ 5/2 → 11/4 | s:bd cutoff:4913.0608648993075 ]", + "[ 21/8 → 11/4 | s:hh cutoff:3661.6894407520135 ]", + "[ 11/4 → 23/8 | s:hh cutoff:2705.9175656158914 ]", + "[ 11/4 → 3/1 | s:bd cutoff:2705.9175656158914 ]", + "[ 23/8 → 3/1 | s:hh cutoff:2219.0277337388693 ]", + "[ 3/1 → 25/8 | s:hh cutoff:2129.6683298423886 ]", + "[ 3/1 → 13/4 | s:bd cutoff:2129.6683298423886 ]", + "[ 25/8 → 13/4 | s:hh cutoff:2113.2537022102156 ]", + "[ 13/4 → 27/8 | s:hh cutoff:2023.8158261763601 ]", + "[ 13/4 → 7/2 | s:bd cutoff:2023.8158261763601 ]", + "[ 27/8 → 7/2 | s:hh cutoff:1848.2479648482126 ]", + "[ 7/2 → 29/8 | s:hh cutoff:1618.380764964968 ]", + "[ 7/2 → 15/4 | s:bd cutoff:1618.380764964968 ]", + "[ 29/8 → 15/4 | s:hh cutoff:1388.5135650817233 ]", + "[ 15/4 → 31/8 | s:hh cutoff:1212.9457037535758 ]", + "[ 15/4 → 4/1 | s:bd cutoff:1212.9457037535758 ]", + "[ 31/8 → 4/1 | s:hh cutoff:1123.5078277197204 ]", ] `; @@ -8152,54 +8142,54 @@ exports[`runs examples > example "queryArc" example index 0 1`] = `[]`; exports[`runs examples > example "rand" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:hh cutoff:3410.6697876704857 ]", - "[ 0/1 → 1/4 | s:bd cutoff:3410.6697876704857 ]", - "[ 1/8 → 1/4 | s:hh cutoff:5409.978067153133 ]", - "[ 1/4 → 3/8 | s:hh cutoff:1470.4432229045779 ]", - "[ 1/4 → 1/2 | s:bd cutoff:1470.4432229045779 ]", - "[ 3/8 → 1/2 | s:hh cutoff:6906.840303097852 ]", - "[ 1/2 → 5/8 | s:hh cutoff:2951.713348273188 ]", - "[ 1/2 → 3/4 | s:bd cutoff:2951.713348273188 ]", - "[ 5/8 → 3/4 | s:hh cutoff:5732.849565800279 ]", - "[ 3/4 → 7/8 | s:hh cutoff:2621.97931134142 ]", - "[ 3/4 → 1/1 | s:bd cutoff:2621.97931134142 ]", - "[ 7/8 → 1/1 | s:hh cutoff:7323.85477644857 ]", - "[ 1/1 → 9/8 | s:hh cutoff:1386.7684000870213 ]", - "[ 1/1 → 5/4 | s:bd cutoff:1386.7684000870213 ]", - "[ 9/8 → 5/4 | s:hh cutoff:968.4956459095702 ]", - "[ 5/4 → 11/8 | s:hh cutoff:7674.6532345423475 ]", - "[ 5/4 → 3/2 | s:bd cutoff:7674.6532345423475 ]", - "[ 11/8 → 3/2 | s:hh cutoff:863.651579245925 ]", - "[ 3/2 → 13/8 | s:hh cutoff:1476.3008129084483 ]", - "[ 3/2 → 7/4 | s:bd cutoff:1476.3008129084483 ]", - "[ 13/8 → 7/4 | s:hh cutoff:5232.728436938487 ]", - "[ 7/4 → 15/8 | s:hh cutoff:3057.402160600759 ]", - "[ 7/4 → 2/1 | s:bd cutoff:3057.402160600759 ]", - "[ 15/8 → 2/1 | s:hh cutoff:5307.644687825814 ]", - "[ 2/1 → 17/8 | s:hh cutoff:4859.5935218036175 ]", - "[ 2/1 → 9/4 | s:bd cutoff:4859.5935218036175 ]", - "[ 17/8 → 9/4 | s:hh cutoff:3798.551473999396 ]", - "[ 9/4 → 19/8 | s:hh cutoff:2020.6333762034774 ]", - "[ 9/4 → 5/2 | s:bd cutoff:2020.6333762034774 ]", - "[ 19/8 → 5/2 | s:hh cutoff:6903.368854080327 ]", - "[ 5/2 → 21/8 | s:hh cutoff:5992.341757635586 ]", - "[ 5/2 → 11/4 | s:bd cutoff:5992.341757635586 ]", - "[ 21/8 → 11/4 | s:hh cutoff:3192.4760919064283 ]", - "[ 11/4 → 23/8 | s:hh cutoff:3188.529685838148 ]", - "[ 11/4 → 3/1 | s:bd cutoff:3188.529685838148 ]", - "[ 23/8 → 3/1 | s:hh cutoff:6837.369324173778 ]", - "[ 3/1 → 25/8 | s:hh cutoff:5111.154264421202 ]", - "[ 3/1 → 13/4 | s:bd cutoff:5111.154264421202 ]", - "[ 25/8 → 13/4 | s:hh cutoff:4812.422384973615 ]", - "[ 13/4 → 27/8 | s:hh cutoff:7517.148802988231 ]", - "[ 13/4 → 7/2 | s:bd cutoff:7517.148802988231 ]", - "[ 27/8 → 7/2 | s:hh cutoff:821.3959728600457 ]", - "[ 7/2 → 29/8 | s:hh cutoff:2354.0139601100236 ]", - "[ 7/2 → 15/4 | s:bd cutoff:2354.0139601100236 ]", - "[ 29/8 → 15/4 | s:hh cutoff:4901.923676487058 ]", - "[ 15/4 → 31/8 | s:hh cutoff:4723.308931104839 ]", - "[ 15/4 → 4/1 | s:bd cutoff:4723.308931104839 ]", - "[ 31/8 → 4/1 | s:hh cutoff:1649.9183619162068 ]", + "[ 0/1 → 1/8 | s:hh cutoff:500 ]", + "[ 0/1 → 1/4 | s:bd cutoff:500 ]", + "[ 1/8 → 1/4 | s:hh cutoff:5639.116775244474 ]", + "[ 1/4 → 3/8 | s:hh cutoff:3273.1976890936494 ]", + "[ 1/4 → 1/2 | s:bd cutoff:3273.1976890936494 ]", + "[ 3/8 → 1/2 | s:hh cutoff:3510.4438681155443 ]", + "[ 1/2 → 5/8 | s:hh cutoff:2453.604621812701 ]", + "[ 1/2 → 3/4 | s:bd cutoff:2453.604621812701 ]", + "[ 5/8 → 3/4 | s:hh cutoff:1517.2690078616142 ]", + "[ 3/4 → 7/8 | s:hh cutoff:1968.6986012384295 ]", + "[ 3/4 → 1/1 | s:bd cutoff:1968.6986012384295 ]", + "[ 7/8 → 1/1 | s:hh cutoff:3482.2331061586738 ]", + "[ 1/1 → 9/8 | s:hh cutoff:4396.566238254309 ]", + "[ 1/1 → 5/4 | s:bd cutoff:4396.566238254309 ]", + "[ 9/8 → 5/4 | s:hh cutoff:5543.671359308064 ]", + "[ 5/4 → 11/8 | s:hh cutoff:5965.461523272097 ]", + "[ 5/4 → 3/2 | s:bd cutoff:5965.461523272097 ]", + "[ 11/8 → 3/2 | s:hh cutoff:1871.028139255941 ]", + "[ 3/2 → 13/8 | s:hh cutoff:5063.060561195016 ]", + "[ 3/2 → 7/4 | s:bd cutoff:5063.060561195016 ]", + "[ 13/8 → 7/4 | s:hh cutoff:4225.660336203873 ]", + "[ 7/4 → 15/8 | s:hh cutoff:2035.5342207476497 ]", + "[ 7/4 → 2/1 | s:bd cutoff:2035.5342207476497 ]", + "[ 15/8 → 2/1 | s:hh cutoff:4959.178843535483 ]", + "[ 2/1 → 17/8 | s:hh cutoff:7696.453399956226 ]", + "[ 2/1 → 9/4 | s:bd cutoff:7696.453399956226 ]", + "[ 17/8 → 9/4 | s:hh cutoff:7275.427204556763 ]", + "[ 9/4 → 19/8 | s:hh cutoff:3091.1188358440995 ]", + "[ 9/4 → 5/2 | s:bd cutoff:3091.1188358440995 ]", + "[ 19/8 → 5/2 | s:hh cutoff:6773.7998040392995 ]", + "[ 5/2 → 21/8 | s:hh cutoff:3939.620556309819 ]", + "[ 5/2 → 11/4 | s:bd cutoff:3939.620556309819 ]", + "[ 21/8 → 11/4 | s:hh cutoff:1701.4511320739985 ]", + "[ 11/4 → 23/8 | s:hh cutoff:5249.245778657496 ]", + "[ 11/4 → 3/1 | s:bd cutoff:5249.245778657496 ]", + "[ 23/8 → 3/1 | s:hh cutoff:621.9062879681587 ]", + "[ 3/1 → 25/8 | s:hh cutoff:2129.6683298423886 ]", + "[ 3/1 → 13/4 | s:bd cutoff:2129.6683298423886 ]", + "[ 25/8 → 13/4 | s:hh cutoff:3074.329087510705 ]", + "[ 13/4 → 27/8 | s:hh cutoff:7942.738036625087 ]", + "[ 13/4 → 7/2 | s:bd cutoff:7942.738036625087 ]", + "[ 27/8 → 7/2 | s:hh cutoff:3565.1885084807873 ]", + "[ 7/2 → 29/8 | s:hh cutoff:3574.6161099523306 ]", + "[ 7/2 → 15/4 | s:bd cutoff:3574.6161099523306 ]", + "[ 29/8 → 15/4 | s:hh cutoff:7543.614340946078 ]", + "[ 15/4 → 31/8 | s:hh cutoff:6591.254916973412 ]", + "[ 15/4 → 4/1 | s:bd cutoff:6591.254916973412 ]", + "[ 31/8 → 4/1 | s:hh cutoff:6021.249040029943 ]", ] `; @@ -8364,38 +8354,38 @@ exports[`runs examples > example "rangex" example index 0 1`] = ` exports[`runs examples > example "rarely" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:hh ]", + "[ 0/1 → 1/8 | s:hh speed:0.5 ]", "[ 1/8 → 1/4 | s:hh ]", - "[ 1/4 → 3/8 | s:hh speed:0.5 ]", + "[ 1/4 → 3/8 | s:hh ]", "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:hh ]", - "[ 5/8 → 3/4 | s:hh ]", - "[ 3/4 → 7/8 | s:hh ]", + "[ 5/8 → 3/4 | s:hh speed:0.5 ]", + "[ 3/4 → 7/8 | s:hh speed:0.5 ]", "[ 7/8 → 1/1 | s:hh ]", - "[ 1/1 → 9/8 | s:hh speed:0.5 ]", - "[ 9/8 → 5/4 | s:hh speed:0.5 ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 9/8 → 5/4 | s:hh ]", "[ 5/4 → 11/8 | s:hh ]", "[ 11/8 → 3/2 | s:hh speed:0.5 ]", - "[ 3/2 → 13/8 | s:hh speed:0.5 ]", + "[ 3/2 → 13/8 | s:hh ]", "[ 13/8 → 7/4 | s:hh ]", - "[ 7/4 → 15/8 | s:hh ]", + "[ 7/4 → 15/8 | s:hh speed:0.5 ]", "[ 15/8 → 2/1 | s:hh ]", "[ 2/1 → 17/8 | s:hh ]", "[ 17/8 → 9/4 | s:hh ]", - "[ 9/4 → 19/8 | s:hh speed:0.5 ]", + "[ 9/4 → 19/8 | s:hh ]", "[ 19/8 → 5/2 | s:hh ]", "[ 5/2 → 21/8 | s:hh ]", - "[ 21/8 → 11/4 | s:hh ]", + "[ 21/8 → 11/4 | s:hh speed:0.5 ]", "[ 11/4 → 23/8 | s:hh ]", - "[ 23/8 → 3/1 | s:hh ]", - "[ 3/1 → 25/8 | s:hh ]", + "[ 23/8 → 3/1 | s:hh speed:0.5 ]", + "[ 3/1 → 25/8 | s:hh speed:0.5 ]", "[ 25/8 → 13/4 | s:hh ]", "[ 13/4 → 27/8 | s:hh ]", - "[ 27/8 → 7/2 | s:hh speed:0.5 ]", - "[ 7/2 → 29/8 | s:hh speed:0.5 ]", + "[ 27/8 → 7/2 | s:hh ]", + "[ 7/2 → 29/8 | s:hh ]", "[ 29/8 → 15/4 | s:hh ]", "[ 15/4 → 31/8 | s:hh ]", - "[ 31/8 → 4/1 | s:hh speed:0.5 ]", + "[ 31/8 → 4/1 | s:hh ]", ] `; @@ -8439,22 +8429,22 @@ exports[`runs examples > example "release" example index 0 1`] = ` exports[`runs examples > example "repeatCycles" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:38 s:gm_acoustic_guitar_nylon ]", - "[ 1/4 → 1/2 | note:35 s:gm_acoustic_guitar_nylon ]", + "[ 0/1 → 1/4 | note:34 s:gm_acoustic_guitar_nylon ]", + "[ 1/4 → 1/2 | note:38 s:gm_acoustic_guitar_nylon ]", "[ 1/2 → 3/4 | note:37 s:gm_acoustic_guitar_nylon ]", - "[ 3/4 → 1/1 | note:37 s:gm_acoustic_guitar_nylon ]", - "[ 1/1 → 5/4 | note:38 s:gm_acoustic_guitar_nylon ]", - "[ 5/4 → 3/2 | note:35 s:gm_acoustic_guitar_nylon ]", + "[ 3/4 → 1/1 | note:36 s:gm_acoustic_guitar_nylon ]", + "[ 1/1 → 5/4 | note:34 s:gm_acoustic_guitar_nylon ]", + "[ 5/4 → 3/2 | note:38 s:gm_acoustic_guitar_nylon ]", "[ 3/2 → 7/4 | note:37 s:gm_acoustic_guitar_nylon ]", - "[ 7/4 → 2/1 | note:37 s:gm_acoustic_guitar_nylon ]", - "[ 2/1 → 9/4 | note:35 s:gm_acoustic_guitar_nylon ]", - "[ 9/4 → 5/2 | note:45 s:gm_acoustic_guitar_nylon ]", - "[ 5/2 → 11/4 | note:35 s:gm_acoustic_guitar_nylon ]", - "[ 11/4 → 3/1 | note:38 s:gm_acoustic_guitar_nylon ]", - "[ 3/1 → 13/4 | note:35 s:gm_acoustic_guitar_nylon ]", - "[ 13/4 → 7/2 | note:45 s:gm_acoustic_guitar_nylon ]", - "[ 7/2 → 15/4 | note:35 s:gm_acoustic_guitar_nylon ]", - "[ 15/4 → 4/1 | note:38 s:gm_acoustic_guitar_nylon ]", + "[ 7/4 → 2/1 | note:36 s:gm_acoustic_guitar_nylon ]", + "[ 2/1 → 9/4 | note:40 s:gm_acoustic_guitar_nylon ]", + "[ 9/4 → 5/2 | note:42 s:gm_acoustic_guitar_nylon ]", + "[ 5/2 → 11/4 | note:41 s:gm_acoustic_guitar_nylon ]", + "[ 11/4 → 3/1 | note:36 s:gm_acoustic_guitar_nylon ]", + "[ 3/1 → 13/4 | note:40 s:gm_acoustic_guitar_nylon ]", + "[ 13/4 → 7/2 | note:42 s:gm_acoustic_guitar_nylon ]", + "[ 7/2 → 15/4 | note:41 s:gm_acoustic_guitar_nylon ]", + "[ 15/4 → 4/1 | note:36 s:gm_acoustic_guitar_nylon ]", ] `; @@ -8619,43 +8609,67 @@ exports[`runs examples > example "ribbon" example index 0 1`] = ` exports[`runs examples > example "ribbon" example index 1 1`] = ` [ - "[ 0/1 → 1/4 | note:D3 ]", - "[ 1/4 → 1/2 | note:A3 ]", + "[ 0/1 → 1/4 | note:A3 ]", + "[ 1/4 → 1/2 | note:C3 ]", "[ 1/2 → 3/4 | note:D3 ]", - "[ 3/4 → 1/1 | note:E3 ]", - "[ 1/1 → 5/4 | note:C3 ]", - "[ 5/4 → 3/2 | note:D3 ]", - "[ 3/2 → 7/4 | note:D3 ]", - "[ 7/4 → 2/1 | note:C4 ]", - "[ 2/1 → 9/4 | note:D3 ]", - "[ 9/4 → 5/2 | note:A3 ]", + "[ 3/4 → 1/1 | note:G3 ]", + "[ 1/1 → 5/4 | note:E3 ]", + "[ 5/4 → 3/2 | note:C4 ]", + "[ 3/2 → 7/4 | note:A3 ]", + "[ 7/4 → 2/1 | note:E4 ]", + "[ 2/1 → 9/4 | note:A3 ]", + "[ 9/4 → 5/2 | note:C3 ]", "[ 5/2 → 11/4 | note:D3 ]", - "[ 11/4 → 3/1 | note:E3 ]", - "[ 3/1 → 13/4 | note:C3 ]", - "[ 13/4 → 7/2 | note:D3 ]", - "[ 7/2 → 15/4 | note:D3 ]", - "[ 15/4 → 4/1 | note:C4 ]", + "[ 11/4 → 3/1 | note:G3 ]", + "[ 3/1 → 13/4 | note:E3 ]", + "[ 13/4 → 7/2 | note:C4 ]", + "[ 7/2 → 15/4 | note:A3 ]", + "[ 15/4 → 4/1 | note:E4 ]", ] `; exports[`runs examples > example "ribbon" example index 2 1`] = ` [ "[ 1/16 → 1/8 | s:bd ]", + "[ 1/8 → 3/16 | s:bd ]", "[ 3/16 → 1/4 | s:bd ]", + "[ 1/4 → 5/16 | s:bd ]", + "[ 3/8 → 7/16 | s:bd ]", "[ 9/16 → 5/8 | s:bd ]", + "[ 5/8 → 11/16 | s:bd ]", "[ 11/16 → 3/4 | s:bd ]", + "[ 3/4 → 13/16 | s:bd ]", + "[ 7/8 → 15/16 | s:bd ]", "[ 17/16 → 9/8 | s:bd ]", + "[ 9/8 → 19/16 | s:bd ]", "[ 19/16 → 5/4 | s:bd ]", + "[ 5/4 → 21/16 | s:bd ]", + "[ 11/8 → 23/16 | s:bd ]", "[ 25/16 → 13/8 | s:bd ]", + "[ 13/8 → 27/16 | s:bd ]", "[ 27/16 → 7/4 | s:bd ]", + "[ 7/4 → 29/16 | s:bd ]", + "[ 15/8 → 31/16 | s:bd ]", "[ 33/16 → 17/8 | s:bd ]", + "[ 17/8 → 35/16 | s:bd ]", "[ 35/16 → 9/4 | s:bd ]", + "[ 9/4 → 37/16 | s:bd ]", + "[ 19/8 → 39/16 | s:bd ]", "[ 41/16 → 21/8 | s:bd ]", + "[ 21/8 → 43/16 | s:bd ]", "[ 43/16 → 11/4 | s:bd ]", + "[ 11/4 → 45/16 | s:bd ]", + "[ 23/8 → 47/16 | s:bd ]", "[ 49/16 → 25/8 | s:bd ]", + "[ 25/8 → 51/16 | s:bd ]", "[ 51/16 → 13/4 | s:bd ]", + "[ 13/4 → 53/16 | s:bd ]", + "[ 27/8 → 55/16 | s:bd ]", "[ 57/16 → 29/8 | s:bd ]", + "[ 29/8 → 59/16 | s:bd ]", "[ 59/16 → 15/4 | s:bd ]", + "[ 15/4 → 61/16 | s:bd ]", + "[ 31/8 → 63/16 | s:bd ]", ] `; @@ -9186,38 +9200,38 @@ exports[`runs examples > example "scale" example index 1 1`] = ` exports[`runs examples > example "scale" example index 2 1`] = ` [ - "[ 0/1 → 1/8 | note:C4 s:piano ]", - "[ 1/8 → 1/4 | note:G4 s:piano ]", - "[ 1/4 → 3/8 | note:F3 s:piano ]", - "[ 3/8 → 1/2 | note:D5 s:piano ]", + "[ 0/1 → 1/8 | note:C3 s:piano ]", + "[ 1/8 → 1/4 | note:A4 s:piano ]", + "[ 1/4 → 3/8 | note:C4 s:piano ]", + "[ 3/8 → 1/2 | note:C4 s:piano ]", "[ 1/2 → 5/8 | note:A3 s:piano ]", - "[ 5/8 → 3/4 | note:A4 s:piano ]", - "[ 3/4 → 7/8 | note:A3 s:piano ]", - "[ 7/8 → 1/1 | note:D5 s:piano ]", - "[ 1/1 → 9/8 | note:F3 s:piano ]", - "[ 9/8 → 5/4 | note:D3 s:piano ]", - "[ 5/4 → 11/8 | note:F5 s:piano ]", - "[ 11/8 → 3/2 | note:D3 s:piano ]", - "[ 3/2 → 13/8 | note:F3 s:piano ]", - "[ 13/8 → 7/4 | note:G4 s:piano ]", - "[ 7/4 → 15/8 | note:C4 s:piano ]", + "[ 5/8 → 3/4 | note:F3 s:piano ]", + "[ 3/4 → 7/8 | note:G3 s:piano ]", + "[ 7/8 → 1/1 | note:C4 s:piano ]", + "[ 1/1 → 9/8 | note:F4 s:piano ]", + "[ 9/8 → 5/4 | note:A4 s:piano ]", + "[ 5/4 → 11/8 | note:A4 s:piano ]", + "[ 11/8 → 3/2 | note:G3 s:piano ]", + "[ 3/2 → 13/8 | note:G4 s:piano ]", + "[ 13/8 → 7/4 | note:D4 s:piano ]", + "[ 7/4 → 15/8 | note:G3 s:piano ]", "[ 15/8 → 2/1 | note:G4 s:piano ]", - "[ 2/1 → 17/8 | note:F4 s:piano ]", - "[ 17/8 → 9/4 | note:D4 s:piano ]", - "[ 9/4 → 19/8 | note:G3 s:piano ]", + "[ 2/1 → 17/8 | note:F5 s:piano ]", + "[ 17/8 → 9/4 | note:D5 s:piano ]", + "[ 9/4 → 19/8 | note:C4 s:piano ]", "[ 19/8 → 5/2 | note:D5 s:piano ]", - "[ 5/2 → 21/8 | note:A4 s:piano ]", - "[ 21/8 → 11/4 | note:C4 s:piano ]", - "[ 11/4 → 23/8 | note:C4 s:piano ]", - "[ 23/8 → 3/1 | note:D5 s:piano ]", - "[ 3/1 → 25/8 | note:G4 s:piano ]", - "[ 25/8 → 13/4 | note:F4 s:piano ]", + "[ 5/2 → 21/8 | note:D4 s:piano ]", + "[ 21/8 → 11/4 | note:F3 s:piano ]", + "[ 11/4 → 23/8 | note:G4 s:piano ]", + "[ 23/8 → 3/1 | note:D3 s:piano ]", + "[ 3/1 → 25/8 | note:G3 s:piano ]", + "[ 25/8 → 13/4 | note:C4 s:piano ]", "[ 13/4 → 27/8 | note:F5 s:piano ]", - "[ 27/8 → 7/2 | note:D3 s:piano ]", - "[ 7/2 → 29/8 | note:G3 s:piano ]", - "[ 29/8 → 15/4 | note:G4 s:piano ]", - "[ 15/4 → 31/8 | note:F4 s:piano ]", - "[ 31/8 → 4/1 | note:F3 s:piano ]", + "[ 27/8 → 7/2 | note:C4 s:piano ]", + "[ 7/2 → 29/8 | note:C4 s:piano ]", + "[ 29/8 → 15/4 | note:F5 s:piano ]", + "[ 15/4 → 31/8 | note:C5 s:piano ]", + "[ 31/8 → 4/1 | note:A4 s:piano ]", ] `; @@ -9256,70 +9270,70 @@ exports[`runs examples > example "scale" example index 3 1`] = ` exports[`runs examples > example "scale" example index 4 1`] = ` [ - "[ 0/1 → 1/16 | note:Gb2 ]", - "[ 1/16 → 1/8 | note:Gb2 ]", - "[ 1/8 → 3/16 | note:Gb2 ]", - "[ 3/16 → 1/4 | note:Gb2 ]", - "[ 1/4 → 5/16 | note:Gb2 ]", - "[ 5/16 → 3/8 | note:Gb2 ]", - "[ 3/8 → 7/16 | note:Gb2 ]", - "[ 7/16 → 1/2 | note:Gb2 ]", - "[ 1/2 → 9/16 | note:Gb2 ]", - "[ 9/16 → 5/8 | note:Gb2 ]", - "[ 5/8 → 11/16 | note:Gb2 ]", - "[ 11/16 → 3/4 | note:Gb2 ]", - "[ 3/4 → 13/16 | note:Gb2 ]", - "[ 13/16 → 7/8 | note:Gb2 ]", - "[ 7/8 → 15/16 | note:Gb2 ]", - "[ 15/16 → 1/1 | note:Gb2 ]", - "[ 1/1 → 17/16 | note:Bb1 ]", - "[ 17/16 → 9/8 | note:Bb1 ]", - "[ 9/8 → 19/16 | note:Bb1 ]", - "[ 19/16 → 5/4 | note:Bb1 ]", - "[ 5/4 → 21/16 | note:Bb1 ]", - "[ 21/16 → 11/8 | note:Bb1 ]", - "[ 11/8 → 23/16 | note:Bb1 ]", - "[ 23/16 → 3/2 | note:Bb1 ]", - "[ 3/2 → 25/16 | note:Bb1 ]", - "[ 25/16 → 13/8 | note:Bb1 ]", - "[ 13/8 → 27/16 | note:Bb1 ]", - "[ 27/16 → 7/4 | note:Bb1 ]", - "[ 7/4 → 29/16 | note:Bb1 ]", - "[ 29/16 → 15/8 | note:Bb1 ]", - "[ 15/8 → 31/16 | note:Bb1 ]", - "[ 31/16 → 2/1 | note:Bb1 ]", - "[ 2/1 → 33/16 | note:Db3 ]", - "[ 33/16 → 17/8 | note:Db3 ]", - "[ 17/8 → 35/16 | note:Db3 ]", - "[ 35/16 → 9/4 | note:Db3 ]", - "[ 9/4 → 37/16 | note:Db3 ]", - "[ 37/16 → 19/8 | note:Db3 ]", - "[ 19/8 → 39/16 | note:Db3 ]", - "[ 39/16 → 5/2 | note:Db3 ]", - "[ 5/2 → 41/16 | note:Db3 ]", - "[ 41/16 → 21/8 | note:Db3 ]", - "[ 21/8 → 43/16 | note:Db3 ]", - "[ 43/16 → 11/4 | note:Db3 ]", - "[ 11/4 → 45/16 | note:Db3 ]", - "[ 45/16 → 23/8 | note:Db3 ]", - "[ 23/8 → 47/16 | note:Db3 ]", - "[ 47/16 → 3/1 | note:Db3 ]", - "[ 3/1 → 49/16 | note:Eb3 ]", - "[ 49/16 → 25/8 | note:Eb3 ]", - "[ 25/8 → 51/16 | note:Eb3 ]", - "[ 51/16 → 13/4 | note:Eb3 ]", - "[ 13/4 → 53/16 | note:Eb3 ]", - "[ 53/16 → 27/8 | note:Eb3 ]", - "[ 27/8 → 55/16 | note:Eb3 ]", - "[ 55/16 → 7/2 | note:Eb3 ]", - "[ 7/2 → 57/16 | note:Eb3 ]", - "[ 57/16 → 29/8 | note:Eb3 ]", - "[ 29/8 → 59/16 | note:Eb3 ]", - "[ 59/16 → 15/4 | note:Eb3 ]", - "[ 15/4 → 61/16 | note:Eb3 ]", - "[ 61/16 → 31/8 | note:Eb3 ]", - "[ 31/8 → 63/16 | note:Eb3 ]", - "[ 63/16 → 4/1 | note:Eb3 ]", + "[ 0/1 → 1/16 | note:Gb1 ]", + "[ 1/16 → 1/8 | note:Gb1 ]", + "[ 1/8 → 3/16 | note:Gb1 ]", + "[ 3/16 → 1/4 | note:Gb1 ]", + "[ 1/4 → 5/16 | note:Gb1 ]", + "[ 5/16 → 3/8 | note:Gb1 ]", + "[ 3/8 → 7/16 | note:Gb1 ]", + "[ 7/16 → 1/2 | note:Gb1 ]", + "[ 1/2 → 9/16 | note:Gb1 ]", + "[ 9/16 → 5/8 | note:Gb1 ]", + "[ 5/8 → 11/16 | note:Gb1 ]", + "[ 11/16 → 3/4 | note:Gb1 ]", + "[ 3/4 → 13/16 | note:Gb1 ]", + "[ 13/16 → 7/8 | note:Gb1 ]", + "[ 7/8 → 15/16 | note:Gb1 ]", + "[ 15/16 → 1/1 | note:Gb1 ]", + "[ 1/1 → 17/16 | note:Cb3 ]", + "[ 17/16 → 9/8 | note:Cb3 ]", + "[ 9/8 → 19/16 | note:Cb3 ]", + "[ 19/16 → 5/4 | note:Cb3 ]", + "[ 5/4 → 21/16 | note:Cb3 ]", + "[ 21/16 → 11/8 | note:Cb3 ]", + "[ 11/8 → 23/16 | note:Cb3 ]", + "[ 23/16 → 3/2 | note:Cb3 ]", + "[ 3/2 → 25/16 | note:Cb3 ]", + "[ 25/16 → 13/8 | note:Cb3 ]", + "[ 13/8 → 27/16 | note:Cb3 ]", + "[ 27/16 → 7/4 | note:Cb3 ]", + "[ 7/4 → 29/16 | note:Cb3 ]", + "[ 29/16 → 15/8 | note:Cb3 ]", + "[ 15/8 → 31/16 | note:Cb3 ]", + "[ 31/16 → 2/1 | note:Cb3 ]", + "[ 2/1 → 33/16 | note:Eb4 ]", + "[ 33/16 → 17/8 | note:Eb4 ]", + "[ 17/8 → 35/16 | note:Eb4 ]", + "[ 35/16 → 9/4 | note:Eb4 ]", + "[ 9/4 → 37/16 | note:Eb4 ]", + "[ 37/16 → 19/8 | note:Eb4 ]", + "[ 19/8 → 39/16 | note:Eb4 ]", + "[ 39/16 → 5/2 | note:Eb4 ]", + "[ 5/2 → 41/16 | note:Eb4 ]", + "[ 41/16 → 21/8 | note:Eb4 ]", + "[ 21/8 → 43/16 | note:Eb4 ]", + "[ 43/16 → 11/4 | note:Eb4 ]", + "[ 11/4 → 45/16 | note:Eb4 ]", + "[ 45/16 → 23/8 | note:Eb4 ]", + "[ 23/8 → 47/16 | note:Eb4 ]", + "[ 47/16 → 3/1 | note:Eb4 ]", + "[ 3/1 → 49/16 | note:Db2 ]", + "[ 49/16 → 25/8 | note:Db2 ]", + "[ 25/8 → 51/16 | note:Db2 ]", + "[ 51/16 → 13/4 | note:Db2 ]", + "[ 13/4 → 53/16 | note:Db2 ]", + "[ 53/16 → 27/8 | note:Db2 ]", + "[ 27/8 → 55/16 | note:Db2 ]", + "[ 55/16 → 7/2 | note:Db2 ]", + "[ 7/2 → 57/16 | note:Db2 ]", + "[ 57/16 → 29/8 | note:Db2 ]", + "[ 29/8 → 59/16 | note:Db2 ]", + "[ 59/16 → 15/4 | note:Db2 ]", + "[ 15/4 → 61/16 | note:Db2 ]", + "[ 61/16 → 31/8 | note:Db2 ]", + "[ 31/8 → 63/16 | note:Db2 ]", + "[ 63/16 → 4/1 | note:Db2 ]", ] `; @@ -9392,46 +9406,46 @@ exports[`runs examples > example "scope" example index 0 1`] = ` exports[`runs examples > example "scramble" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:d s:piano ]", - "[ 1/4 → 1/2 | note:c s:piano ]", + "[ 0/1 → 1/4 | note:c s:piano ]", + "[ 1/4 → 1/2 | note:d s:piano ]", "[ 1/2 → 3/4 | note:d s:piano ]", - "[ 3/4 → 1/1 | note:d s:piano ]", - "[ 1/1 → 5/4 | note:c s:piano ]", - "[ 5/4 → 3/2 | note:f s:piano ]", - "[ 3/2 → 7/4 | note:c s:piano ]", - "[ 7/4 → 2/1 | note:d s:piano ]", - "[ 2/1 → 9/4 | note:e s:piano ]", - "[ 9/4 → 5/2 | note:c s:piano ]", - "[ 5/2 → 11/4 | note:e s:piano ]", - "[ 11/4 → 3/1 | note:d s:piano ]", - "[ 3/1 → 13/4 | note:e s:piano ]", + "[ 3/4 → 1/1 | note:c s:piano ]", + "[ 1/1 → 5/4 | note:e s:piano ]", + "[ 5/4 → 3/2 | note:e s:piano ]", + "[ 3/2 → 7/4 | note:e s:piano ]", + "[ 7/4 → 2/1 | note:c s:piano ]", + "[ 2/1 → 9/4 | note:f s:piano ]", + "[ 9/4 → 5/2 | note:d s:piano ]", + "[ 5/2 → 11/4 | note:d s:piano ]", + "[ 11/4 → 3/1 | note:e s:piano ]", + "[ 3/1 → 13/4 | note:c s:piano ]", "[ 13/4 → 7/2 | note:f s:piano ]", - "[ 7/2 → 15/4 | note:c s:piano ]", - "[ 15/4 → 4/1 | note:e s:piano ]", + "[ 7/2 → 15/4 | note:d s:piano ]", + "[ 15/4 → 4/1 | note:f s:piano ]", ] `; exports[`runs examples > example "scramble" example index 1 1`] = ` [ - "[ 0/1 → 1/8 | note:d s:piano ]", - "[ 1/8 → 1/4 | note:c s:piano ]", + "[ 0/1 → 1/8 | note:c s:piano ]", + "[ 1/8 → 1/4 | note:d s:piano ]", "[ 1/4 → 3/8 | note:d s:piano ]", - "[ 3/8 → 1/2 | note:d s:piano ]", + "[ 3/8 → 1/2 | note:c s:piano ]", "[ 1/2 → 1/1 | note:g s:piano ]", - "[ 1/1 → 9/8 | note:c s:piano ]", - "[ 9/8 → 5/4 | note:f s:piano ]", - "[ 5/4 → 11/8 | note:c s:piano ]", - "[ 11/8 → 3/2 | note:d s:piano ]", + "[ 1/1 → 9/8 | note:e s:piano ]", + "[ 9/8 → 5/4 | note:e s:piano ]", + "[ 5/4 → 11/8 | note:e s:piano ]", + "[ 11/8 → 3/2 | note:c s:piano ]", "[ 3/2 → 2/1 | note:g s:piano ]", - "[ 2/1 → 17/8 | note:e s:piano ]", - "[ 17/8 → 9/4 | note:c s:piano ]", - "[ 9/4 → 19/8 | note:e s:piano ]", - "[ 19/8 → 5/2 | note:d s:piano ]", + "[ 2/1 → 17/8 | note:f s:piano ]", + "[ 17/8 → 9/4 | note:d s:piano ]", + "[ 9/4 → 19/8 | note:d s:piano ]", + "[ 19/8 → 5/2 | note:e s:piano ]", "[ 5/2 → 3/1 | note:g s:piano ]", - "[ 3/1 → 25/8 | note:e s:piano ]", + "[ 3/1 → 25/8 | note:c s:piano ]", "[ 25/8 → 13/4 | note:f s:piano ]", - "[ 13/4 → 27/8 | note:c s:piano ]", - "[ 27/8 → 7/2 | note:e s:piano ]", + "[ 13/4 → 27/8 | note:d s:piano ]", + "[ 27/8 → 7/2 | note:f s:piano ]", "[ 7/2 → 4/1 | note:g s:piano ]", ] `; @@ -9484,18 +9498,13 @@ exports[`runs examples > example "scrub" example index 1 1`] = ` exports[`runs examples > example "seed" example index 0 1`] = ` [ + "[ 0/1 → 1/4 | s:bd ]", "[ 1/4 → 1/2 | s:bd ]", "[ 1/2 → 3/4 | s:bd ]", - "[ 3/4 → 1/1 | s:bd ]", "[ 1/1 → 5/4 | s:bd ]", - "[ 5/4 → 3/2 | s:bd ]", - "[ 3/2 → 7/4 | s:bd ]", "[ 7/4 → 2/1 | s:bd ]", - "[ 2/1 → 9/4 | s:bd ]", + "[ 9/4 → 5/2 | s:bd ]", "[ 11/4 → 3/1 | s:bd ]", - "[ 3/1 → 13/4 | s:bd ]", - "[ 13/4 → 7/2 | s:bd ]", - "[ 7/2 → 15/4 | s:bd ]", ] `; @@ -9705,38 +9714,38 @@ exports[`runs examples > example "setGainCurve" example index 0 1`] = ` exports[`runs examples > example "setMaxPolyphony" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | note:E4 room:1 release:4 gain:0.5 ]", - "[ 1/8 → 1/4 | note:D#5 room:1 release:4 gain:0.5 ]", - "[ 1/4 → 3/8 | note:F#3 room:1 release:4 gain:0.5 ]", - "[ 3/8 → 1/2 | note:B5 room:1 release:4 gain:0.5 ]", - "[ 1/2 → 5/8 | note:C#4 room:1 release:4 gain:0.5 ]", - "[ 5/8 → 3/4 | note:E5 room:1 release:4 gain:0.5 ]", - "[ 3/4 → 7/8 | note:B3 room:1 release:4 gain:0.5 ]", - "[ 7/8 → 1/1 | note:C#6 room:1 release:4 gain:0.5 ]", - "[ 1/1 → 9/8 | note:E3 room:1 release:4 gain:0.5 ]", - "[ 9/8 → 5/4 | note:D#3 room:1 release:4 gain:0.5 ]", - "[ 5/4 → 11/8 | note:D#6 room:1 release:4 gain:0.5 ]", - "[ 11/8 → 3/2 | note:D#3 room:1 release:4 gain:0.5 ]", - "[ 3/2 → 13/8 | note:F#3 room:1 release:4 gain:0.5 ]", - "[ 13/8 → 7/4 | note:D#5 room:1 release:4 gain:0.5 ]", - "[ 7/4 → 15/8 | note:D#4 room:1 release:4 gain:0.5 ]", - "[ 15/8 → 2/1 | note:D#5 room:1 release:4 gain:0.5 ]", - "[ 2/1 → 17/8 | note:B4 room:1 release:4 gain:0.5 ]", - "[ 17/8 → 9/4 | note:F#4 room:1 release:4 gain:0.5 ]", - "[ 9/4 → 19/8 | note:G#3 room:1 release:4 gain:0.5 ]", + "[ 0/1 → 1/8 | note:C#3 room:1 release:4 gain:0.5 ]", + "[ 1/8 → 1/4 | note:E5 room:1 release:4 gain:0.5 ]", + "[ 1/4 → 3/8 | note:D#4 room:1 release:4 gain:0.5 ]", + "[ 3/8 → 1/2 | note:E4 room:1 release:4 gain:0.5 ]", + "[ 1/2 → 5/8 | note:B3 room:1 release:4 gain:0.5 ]", + "[ 5/8 → 3/4 | note:F#3 room:1 release:4 gain:0.5 ]", + "[ 3/4 → 7/8 | note:G#3 room:1 release:4 gain:0.5 ]", + "[ 7/8 → 1/1 | note:E4 room:1 release:4 gain:0.5 ]", + "[ 1/1 → 9/8 | note:A4 room:1 release:4 gain:0.5 ]", + "[ 9/8 → 5/4 | note:E5 room:1 release:4 gain:0.5 ]", + "[ 5/4 → 11/8 | note:F#5 room:1 release:4 gain:0.5 ]", + "[ 11/8 → 3/2 | note:G#3 room:1 release:4 gain:0.5 ]", + "[ 3/2 → 13/8 | note:C#5 room:1 release:4 gain:0.5 ]", + "[ 13/8 → 7/4 | note:G#4 room:1 release:4 gain:0.5 ]", + "[ 7/4 → 15/8 | note:G#3 room:1 release:4 gain:0.5 ]", + "[ 15/8 → 2/1 | note:C#5 room:1 release:4 gain:0.5 ]", + "[ 2/1 → 17/8 | note:E6 room:1 release:4 gain:0.5 ]", + "[ 17/8 → 9/4 | note:C#6 room:1 release:4 gain:0.5 ]", + "[ 9/4 → 19/8 | note:D#4 room:1 release:4 gain:0.5 ]", "[ 19/8 → 5/2 | note:B5 room:1 release:4 gain:0.5 ]", - "[ 5/2 → 21/8 | note:F#5 room:1 release:4 gain:0.5 ]", - "[ 21/8 → 11/4 | note:D#4 room:1 release:4 gain:0.5 ]", - "[ 11/4 → 23/8 | note:D#4 room:1 release:4 gain:0.5 ]", - "[ 23/8 → 3/1 | note:B5 room:1 release:4 gain:0.5 ]", - "[ 3/1 → 25/8 | note:C#5 room:1 release:4 gain:0.5 ]", - "[ 25/8 → 13/4 | note:B4 room:1 release:4 gain:0.5 ]", - "[ 13/4 → 27/8 | note:D#6 room:1 release:4 gain:0.5 ]", - "[ 27/8 → 7/2 | note:D#3 room:1 release:4 gain:0.5 ]", - "[ 7/2 → 29/8 | note:A3 room:1 release:4 gain:0.5 ]", - "[ 29/8 → 15/4 | note:C#5 room:1 release:4 gain:0.5 ]", - "[ 15/4 → 31/8 | note:B4 room:1 release:4 gain:0.5 ]", - "[ 31/8 → 4/1 | note:F#3 room:1 release:4 gain:0.5 ]", + "[ 5/2 → 21/8 | note:G#4 room:1 release:4 gain:0.5 ]", + "[ 21/8 → 11/4 | note:F#3 room:1 release:4 gain:0.5 ]", + "[ 11/4 → 23/8 | note:D#5 room:1 release:4 gain:0.5 ]", + "[ 23/8 → 3/1 | note:C#3 room:1 release:4 gain:0.5 ]", + "[ 3/1 → 25/8 | note:A3 room:1 release:4 gain:0.5 ]", + "[ 25/8 → 13/4 | note:D#4 room:1 release:4 gain:0.5 ]", + "[ 13/4 → 27/8 | note:E6 room:1 release:4 gain:0.5 ]", + "[ 27/8 → 7/2 | note:E4 room:1 release:4 gain:0.5 ]", + "[ 7/2 → 29/8 | note:E4 room:1 release:4 gain:0.5 ]", + "[ 29/8 → 15/4 | note:D#6 room:1 release:4 gain:0.5 ]", + "[ 15/4 → 31/8 | note:A5 room:1 release:4 gain:0.5 ]", + "[ 31/8 → 4/1 | note:F#5 room:1 release:4 gain:0.5 ]", ] `; @@ -9972,45 +9981,45 @@ exports[`runs examples > example "shrink" example index 3 1`] = ` exports[`runs examples > example "shuffle" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:f s:piano ]", + "[ 0/1 → 1/4 | note:e s:piano ]", "[ 1/4 → 1/2 | note:d s:piano ]", - "[ 1/2 → 3/4 | note:c s:piano ]", - "[ 3/4 → 1/1 | note:e s:piano ]", - "[ 1/1 → 5/4 | note:c s:piano ]", - "[ 5/4 → 3/2 | note:f s:piano ]", - "[ 3/2 → 7/4 | note:d s:piano ]", - "[ 7/4 → 2/1 | note:e s:piano ]", - "[ 2/1 → 9/4 | note:e s:piano ]", - "[ 9/4 → 5/2 | note:d s:piano ]", - "[ 5/2 → 11/4 | note:f s:piano ]", - "[ 11/4 → 3/1 | note:c s:piano ]", - "[ 3/1 → 13/4 | note:f s:piano ]", - "[ 13/4 → 7/2 | note:c s:piano ]", - "[ 7/2 → 15/4 | note:e s:piano ]", + "[ 1/2 → 3/4 | note:f s:piano ]", + "[ 3/4 → 1/1 | note:c s:piano ]", + "[ 1/1 → 5/4 | note:e s:piano ]", + "[ 5/4 → 3/2 | note:c s:piano ]", + "[ 3/2 → 7/4 | note:f s:piano ]", + "[ 7/4 → 2/1 | note:d s:piano ]", + "[ 2/1 → 9/4 | note:d s:piano ]", + "[ 9/4 → 5/2 | note:c s:piano ]", + "[ 5/2 → 11/4 | note:e s:piano ]", + "[ 11/4 → 3/1 | note:f s:piano ]", + "[ 3/1 → 13/4 | note:c s:piano ]", + "[ 13/4 → 7/2 | note:e s:piano ]", + "[ 7/2 → 15/4 | note:f s:piano ]", "[ 15/4 → 4/1 | note:d s:piano ]", ] `; exports[`runs examples > example "shuffle" example index 1 1`] = ` [ - "[ 0/1 → 1/8 | note:f s:piano ]", + "[ 0/1 → 1/8 | note:e s:piano ]", "[ 1/8 → 1/4 | note:d s:piano ]", - "[ 1/4 → 3/8 | note:c s:piano ]", - "[ 3/8 → 1/2 | note:e s:piano ]", + "[ 1/4 → 3/8 | note:f s:piano ]", + "[ 3/8 → 1/2 | note:c s:piano ]", "[ 1/2 → 1/1 | note:g s:piano ]", - "[ 1/1 → 9/8 | note:c s:piano ]", - "[ 9/8 → 5/4 | note:f s:piano ]", - "[ 5/4 → 11/8 | note:d s:piano ]", - "[ 11/8 → 3/2 | note:e s:piano ]", + "[ 1/1 → 9/8 | note:e s:piano ]", + "[ 9/8 → 5/4 | note:c s:piano ]", + "[ 5/4 → 11/8 | note:f s:piano ]", + "[ 11/8 → 3/2 | note:d s:piano ]", "[ 3/2 → 2/1 | note:g s:piano ]", - "[ 2/1 → 17/8 | note:e s:piano ]", - "[ 17/8 → 9/4 | note:d s:piano ]", - "[ 9/4 → 19/8 | note:f s:piano ]", - "[ 19/8 → 5/2 | note:c s:piano ]", + "[ 2/1 → 17/8 | note:d s:piano ]", + "[ 17/8 → 9/4 | note:c s:piano ]", + "[ 9/4 → 19/8 | note:e s:piano ]", + "[ 19/8 → 5/2 | note:f s:piano ]", "[ 5/2 → 3/1 | note:g s:piano ]", - "[ 3/1 → 25/8 | note:f s:piano ]", - "[ 25/8 → 13/4 | note:c s:piano ]", - "[ 13/4 → 27/8 | note:e s:piano ]", + "[ 3/1 → 25/8 | note:c s:piano ]", + "[ 25/8 → 13/4 | note:e s:piano ]", + "[ 13/4 → 27/8 | note:f s:piano ]", "[ 27/8 → 7/2 | note:d s:piano ]", "[ 7/2 → 4/1 | note:g s:piano ]", ] @@ -10198,15 +10207,15 @@ exports[`runs examples > example "someCycles" example index 0 1`] = ` "[ 5/8 → 3/4 | s:hh speed:0.5 ]", "[ 3/4 → 7/8 | s:hh speed:0.5 ]", "[ 7/8 → 1/1 | s:hh speed:0.5 ]", - "[ 1/1 → 9/8 | s:hh speed:0.5 ]", - "[ 1/1 → 2/1 | s:bd speed:0.5 ]", - "[ 9/8 → 5/4 | s:hh speed:0.5 ]", - "[ 5/4 → 11/8 | s:hh speed:0.5 ]", - "[ 11/8 → 3/2 | s:hh speed:0.5 ]", - "[ 3/2 → 13/8 | s:hh speed:0.5 ]", - "[ 13/8 → 7/4 | s:hh speed:0.5 ]", - "[ 7/4 → 15/8 | s:hh speed:0.5 ]", - "[ 15/8 → 2/1 | s:hh speed:0.5 ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 1/1 → 2/1 | s:bd ]", + "[ 9/8 → 5/4 | s:hh ]", + "[ 5/4 → 11/8 | s:hh ]", + "[ 11/8 → 3/2 | s:hh ]", + "[ 3/2 → 13/8 | s:hh ]", + "[ 13/8 → 7/4 | s:hh ]", + "[ 7/4 → 15/8 | s:hh ]", + "[ 15/8 → 2/1 | s:hh ]", "[ 2/1 → 17/8 | s:hh ]", "[ 2/1 → 3/1 | s:bd ]", "[ 17/8 → 9/4 | s:hh ]", @@ -10216,38 +10225,38 @@ exports[`runs examples > example "someCycles" example index 0 1`] = ` "[ 21/8 → 11/4 | s:hh ]", "[ 11/4 → 23/8 | s:hh ]", "[ 23/8 → 3/1 | s:hh ]", - "[ 3/1 → 25/8 | s:hh ]", - "[ 3/1 → 4/1 | s:bd ]", - "[ 25/8 → 13/4 | s:hh ]", - "[ 13/4 → 27/8 | s:hh ]", - "[ 27/8 → 7/2 | s:hh ]", - "[ 7/2 → 29/8 | s:hh ]", - "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:hh ]", - "[ 31/8 → 4/1 | s:hh ]", + "[ 3/1 → 25/8 | s:hh speed:0.5 ]", + "[ 3/1 → 4/1 | s:bd speed:0.5 ]", + "[ 25/8 → 13/4 | s:hh speed:0.5 ]", + "[ 13/4 → 27/8 | s:hh speed:0.5 ]", + "[ 27/8 → 7/2 | s:hh speed:0.5 ]", + "[ 7/2 → 29/8 | s:hh speed:0.5 ]", + "[ 29/8 → 15/4 | s:hh speed:0.5 ]", + "[ 15/4 → 31/8 | s:hh speed:0.5 ]", + "[ 31/8 → 4/1 | s:hh speed:0.5 ]", ] `; exports[`runs examples > example "someCyclesBy" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | s:hh ]", - "[ 0/1 → 1/1 | s:bd ]", - "[ 1/8 → 1/4 | s:hh ]", - "[ 1/4 → 3/8 | s:hh ]", - "[ 3/8 → 1/2 | s:hh ]", - "[ 1/2 → 5/8 | s:hh ]", - "[ 5/8 → 3/4 | s:hh ]", - "[ 3/4 → 7/8 | s:hh ]", - "[ 7/8 → 1/1 | s:hh ]", - "[ 1/1 → 9/8 | s:hh speed:0.5 ]", - "[ 1/1 → 2/1 | s:bd speed:0.5 ]", - "[ 9/8 → 5/4 | s:hh speed:0.5 ]", - "[ 5/4 → 11/8 | s:hh speed:0.5 ]", - "[ 11/8 → 3/2 | s:hh speed:0.5 ]", - "[ 3/2 → 13/8 | s:hh speed:0.5 ]", - "[ 13/8 → 7/4 | s:hh speed:0.5 ]", - "[ 7/4 → 15/8 | s:hh speed:0.5 ]", - "[ 15/8 → 2/1 | s:hh speed:0.5 ]", + "[ 0/1 → 1/8 | s:hh speed:0.5 ]", + "[ 0/1 → 1/1 | s:bd speed:0.5 ]", + "[ 1/8 → 1/4 | s:hh speed:0.5 ]", + "[ 1/4 → 3/8 | s:hh speed:0.5 ]", + "[ 3/8 → 1/2 | s:hh speed:0.5 ]", + "[ 1/2 → 5/8 | s:hh speed:0.5 ]", + "[ 5/8 → 3/4 | s:hh speed:0.5 ]", + "[ 3/4 → 7/8 | s:hh speed:0.5 ]", + "[ 7/8 → 1/1 | s:hh speed:0.5 ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 1/1 → 2/1 | s:bd ]", + "[ 9/8 → 5/4 | s:hh ]", + "[ 5/4 → 11/8 | s:hh ]", + "[ 11/8 → 3/2 | s:hh ]", + "[ 3/2 → 13/8 | s:hh ]", + "[ 13/8 → 7/4 | s:hh ]", + "[ 7/4 → 15/8 | s:hh ]", + "[ 15/8 → 2/1 | s:hh ]", "[ 2/1 → 17/8 | s:hh ]", "[ 2/1 → 3/1 | s:bd ]", "[ 17/8 → 9/4 | s:hh ]", @@ -10257,15 +10266,15 @@ exports[`runs examples > example "someCyclesBy" example index 0 1`] = ` "[ 21/8 → 11/4 | s:hh ]", "[ 11/4 → 23/8 | s:hh ]", "[ 23/8 → 3/1 | s:hh ]", - "[ 3/1 → 25/8 | s:hh ]", - "[ 3/1 → 4/1 | s:bd ]", - "[ 25/8 → 13/4 | s:hh ]", - "[ 13/4 → 27/8 | s:hh ]", - "[ 27/8 → 7/2 | s:hh ]", - "[ 7/2 → 29/8 | s:hh ]", - "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:hh ]", - "[ 31/8 → 4/1 | s:hh ]", + "[ 3/1 → 25/8 | s:hh speed:0.5 ]", + "[ 3/1 → 4/1 | s:bd speed:0.5 ]", + "[ 25/8 → 13/4 | s:hh speed:0.5 ]", + "[ 13/4 → 27/8 | s:hh speed:0.5 ]", + "[ 27/8 → 7/2 | s:hh speed:0.5 ]", + "[ 7/2 → 29/8 | s:hh speed:0.5 ]", + "[ 29/8 → 15/4 | s:hh speed:0.5 ]", + "[ 15/4 → 31/8 | s:hh speed:0.5 ]", + "[ 31/8 → 4/1 | s:hh speed:0.5 ]", ] `; @@ -10274,35 +10283,35 @@ exports[`runs examples > example "sometimes" example index 0 1`] = ` "[ 0/1 → 1/8 | s:hh speed:0.5 ]", "[ 1/8 → 1/4 | s:hh ]", "[ 1/4 → 3/8 | s:hh speed:0.5 ]", - "[ 3/8 → 1/2 | s:hh ]", + "[ 3/8 → 1/2 | s:hh speed:0.5 ]", "[ 1/2 → 5/8 | s:hh speed:0.5 ]", - "[ 5/8 → 3/4 | s:hh ]", + "[ 5/8 → 3/4 | s:hh speed:0.5 ]", "[ 3/4 → 7/8 | s:hh speed:0.5 ]", - "[ 7/8 → 1/1 | s:hh ]", - "[ 1/1 → 9/8 | s:hh speed:0.5 ]", - "[ 9/8 → 5/4 | s:hh speed:0.5 ]", + "[ 7/8 → 1/1 | s:hh speed:0.5 ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 9/8 → 5/4 | s:hh ]", "[ 5/4 → 11/8 | s:hh ]", "[ 11/8 → 3/2 | s:hh speed:0.5 ]", - "[ 3/2 → 13/8 | s:hh speed:0.5 ]", - "[ 13/8 → 7/4 | s:hh ]", + "[ 3/2 → 13/8 | s:hh ]", + "[ 13/8 → 7/4 | s:hh speed:0.5 ]", "[ 7/4 → 15/8 | s:hh speed:0.5 ]", "[ 15/8 → 2/1 | s:hh ]", "[ 2/1 → 17/8 | s:hh ]", - "[ 17/8 → 9/4 | s:hh speed:0.5 ]", + "[ 17/8 → 9/4 | s:hh ]", "[ 9/4 → 19/8 | s:hh speed:0.5 ]", "[ 19/8 → 5/2 | s:hh ]", - "[ 5/2 → 21/8 | s:hh ]", + "[ 5/2 → 21/8 | s:hh speed:0.5 ]", "[ 21/8 → 11/4 | s:hh speed:0.5 ]", - "[ 11/4 → 23/8 | s:hh speed:0.5 ]", - "[ 23/8 → 3/1 | s:hh ]", - "[ 3/1 → 25/8 | s:hh ]", - "[ 25/8 → 13/4 | s:hh ]", + "[ 11/4 → 23/8 | s:hh ]", + "[ 23/8 → 3/1 | s:hh speed:0.5 ]", + "[ 3/1 → 25/8 | s:hh speed:0.5 ]", + "[ 25/8 → 13/4 | s:hh speed:0.5 ]", "[ 13/4 → 27/8 | s:hh ]", "[ 27/8 → 7/2 | s:hh speed:0.5 ]", "[ 7/2 → 29/8 | s:hh speed:0.5 ]", "[ 29/8 → 15/4 | s:hh ]", "[ 15/4 → 31/8 | s:hh ]", - "[ 31/8 → 4/1 | s:hh speed:0.5 ]", + "[ 31/8 → 4/1 | s:hh ]", ] `; @@ -10313,14 +10322,14 @@ exports[`runs examples > example "sometimesBy" example index 0 1`] = ` "[ 1/4 → 3/8 | s:hh speed:0.5 ]", "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:hh speed:0.5 ]", - "[ 5/8 → 3/4 | s:hh ]", + "[ 5/8 → 3/4 | s:hh speed:0.5 ]", "[ 3/4 → 7/8 | s:hh speed:0.5 ]", - "[ 7/8 → 1/1 | s:hh ]", - "[ 1/1 → 9/8 | s:hh speed:0.5 ]", - "[ 9/8 → 5/4 | s:hh speed:0.5 ]", + "[ 7/8 → 1/1 | s:hh speed:0.5 ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 9/8 → 5/4 | s:hh ]", "[ 5/4 → 11/8 | s:hh ]", "[ 11/8 → 3/2 | s:hh speed:0.5 ]", - "[ 3/2 → 13/8 | s:hh speed:0.5 ]", + "[ 3/2 → 13/8 | s:hh ]", "[ 13/8 → 7/4 | s:hh ]", "[ 7/4 → 15/8 | s:hh speed:0.5 ]", "[ 15/8 → 2/1 | s:hh ]", @@ -10330,16 +10339,16 @@ exports[`runs examples > example "sometimesBy" example index 0 1`] = ` "[ 19/8 → 5/2 | s:hh ]", "[ 5/2 → 21/8 | s:hh ]", "[ 21/8 → 11/4 | s:hh speed:0.5 ]", - "[ 11/4 → 23/8 | s:hh speed:0.5 ]", - "[ 23/8 → 3/1 | s:hh ]", - "[ 3/1 → 25/8 | s:hh ]", - "[ 25/8 → 13/4 | s:hh ]", + "[ 11/4 → 23/8 | s:hh ]", + "[ 23/8 → 3/1 | s:hh speed:0.5 ]", + "[ 3/1 → 25/8 | s:hh speed:0.5 ]", + "[ 25/8 → 13/4 | s:hh speed:0.5 ]", "[ 13/4 → 27/8 | s:hh ]", - "[ 27/8 → 7/2 | s:hh speed:0.5 ]", - "[ 7/2 → 29/8 | s:hh speed:0.5 ]", + "[ 27/8 → 7/2 | s:hh ]", + "[ 7/2 → 29/8 | s:hh ]", "[ 29/8 → 15/4 | s:hh ]", "[ 15/4 → 31/8 | s:hh ]", - "[ 31/8 → 4/1 | s:hh speed:0.5 ]", + "[ 31/8 → 4/1 | s:hh ]", ] `; @@ -11724,20 +11733,22 @@ exports[`runs examples > example "undegrade" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh ]", "[ 1/4 → 3/8 | s:hh ]", + "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:hh ]", + "[ 5/8 → 3/4 | s:hh ]", "[ 3/4 → 7/8 | s:hh ]", - "[ 1/1 → 9/8 | s:hh ]", - "[ 9/8 → 5/4 | s:hh ]", + "[ 7/8 → 1/1 | s:hh ]", "[ 11/8 → 3/2 | s:hh ]", - "[ 3/2 → 13/8 | s:hh ]", + "[ 13/8 → 7/4 | s:hh ]", "[ 7/4 → 15/8 | s:hh ]", - "[ 17/8 → 9/4 | s:hh ]", "[ 9/4 → 19/8 | s:hh ]", + "[ 5/2 → 21/8 | s:hh ]", "[ 21/8 → 11/4 | s:hh ]", - "[ 11/4 → 23/8 | s:hh ]", + "[ 23/8 → 3/1 | s:hh ]", + "[ 3/1 → 25/8 | s:hh ]", + "[ 25/8 → 13/4 | s:hh ]", "[ 27/8 → 7/2 | s:hh ]", "[ 7/2 → 29/8 | s:hh ]", - "[ 31/8 → 4/1 | s:hh ]", ] `; @@ -11746,42 +11757,42 @@ exports[`runs examples > example "undegrade" example index 1 1`] = ` "[ 0/1 → 1/10 | s:hh pan:1 ]", "[ 1/10 → 1/5 | s:hh pan:1 ]", "[ 1/5 → 3/10 | s:hh pan:0 ]", - "[ 3/10 → 2/5 | s:hh pan:1 ]", + "[ 3/10 → 2/5 | s:hh pan:0 ]", "[ 2/5 → 1/2 | s:hh pan:0 ]", "[ 1/2 → 3/5 | s:hh pan:1 ]", - "[ 3/5 → 7/10 | s:hh pan:1 ]", - "[ 7/10 → 4/5 | s:hh pan:1 ]", + "[ 3/5 → 7/10 | s:hh pan:0 ]", + "[ 7/10 → 4/5 | s:hh pan:0 ]", "[ 4/5 → 9/10 | s:hh pan:1 ]", "[ 9/10 → 1/1 | s:hh pan:0 ]", - "[ 1/1 → 11/10 | s:hh pan:1 ]", - "[ 11/10 → 6/5 | s:hh pan:1 ]", - "[ 6/5 → 13/10 | s:hh pan:0 ]", + "[ 1/1 → 11/10 | s:hh pan:0 ]", + "[ 11/10 → 6/5 | s:hh pan:0 ]", + "[ 6/5 → 13/10 | s:hh pan:1 ]", "[ 13/10 → 7/5 | s:hh pan:1 ]", - "[ 7/5 → 3/2 | s:hh pan:0 ]", - "[ 3/2 → 8/5 | s:hh pan:1 ]", - "[ 8/5 → 17/10 | s:hh pan:0 ]", + "[ 7/5 → 3/2 | s:hh pan:1 ]", + "[ 3/2 → 8/5 | s:hh pan:0 ]", + "[ 8/5 → 17/10 | s:hh pan:1 ]", "[ 17/10 → 9/5 | s:hh pan:1 ]", - "[ 9/5 → 19/10 | s:hh pan:0 ]", + "[ 9/5 → 19/10 | s:hh pan:1 ]", "[ 19/10 → 2/1 | s:hh pan:1 ]", "[ 2/1 → 21/10 | s:hh pan:0 ]", "[ 21/10 → 11/5 | s:hh pan:1 ]", "[ 11/5 → 23/10 | s:hh pan:0 ]", - "[ 23/10 → 12/5 | s:hh pan:1 ]", + "[ 23/10 → 12/5 | s:hh pan:0 ]", "[ 12/5 → 5/2 | s:hh pan:0 ]", - "[ 5/2 → 13/5 | s:hh pan:0 ]", - "[ 13/5 → 27/10 | s:hh pan:0 ]", + "[ 5/2 → 13/5 | s:hh pan:1 ]", + "[ 13/5 → 27/10 | s:hh pan:1 ]", "[ 27/10 → 14/5 | s:hh pan:1 ]", "[ 14/5 → 29/10 | s:hh pan:0 ]", - "[ 29/10 → 3/1 | s:hh pan:0 ]", - "[ 3/1 → 31/10 | s:hh pan:0 ]", + "[ 29/10 → 3/1 | s:hh pan:1 ]", + "[ 3/1 → 31/10 | s:hh pan:1 ]", "[ 31/10 → 16/5 | s:hh pan:1 ]", "[ 16/5 → 33/10 | s:hh pan:1 ]", "[ 33/10 → 17/5 | s:hh pan:0 ]", - "[ 17/5 → 7/2 | s:hh pan:1 ]", + "[ 17/5 → 7/2 | s:hh pan:0 ]", "[ 7/2 → 18/5 | s:hh pan:1 ]", - "[ 18/5 → 37/10 | s:hh pan:1 ]", - "[ 37/10 → 19/5 | s:hh pan:1 ]", - "[ 19/5 → 39/10 | s:hh pan:0 ]", + "[ 18/5 → 37/10 | s:hh pan:0 ]", + "[ 37/10 → 19/5 | s:hh pan:0 ]", + "[ 19/5 → 39/10 | s:hh pan:1 ]", "[ 39/10 → 4/1 | s:hh pan:1 ]", ] `; @@ -11791,36 +11802,36 @@ exports[`runs examples > example "undegradeBy" example index 0 1`] = ` "[ 0/1 → 1/8 | s:hh ]", "[ 1/8 → 1/4 | s:hh ]", "[ 1/4 → 3/8 | s:hh ]", + "[ 3/8 → 1/2 | s:hh ]", "[ 1/2 → 5/8 | s:hh ]", "[ 5/8 → 3/4 | s:hh ]", "[ 3/4 → 7/8 | s:hh ]", + "[ 7/8 → 1/1 | s:hh ]", "[ 1/1 → 9/8 | s:hh ]", "[ 9/8 → 5/4 | s:hh ]", + "[ 5/4 → 11/8 | s:hh ]", "[ 11/8 → 3/2 | s:hh ]", "[ 3/2 → 13/8 | s:hh ]", "[ 13/8 → 7/4 | s:hh ]", "[ 7/4 → 15/8 | s:hh ]", "[ 15/8 → 2/1 | s:hh ]", - "[ 2/1 → 17/8 | s:hh ]", - "[ 17/8 → 9/4 | s:hh ]", "[ 9/4 → 19/8 | s:hh ]", "[ 5/2 → 21/8 | s:hh ]", "[ 21/8 → 11/4 | s:hh ]", "[ 11/4 → 23/8 | s:hh ]", + "[ 23/8 → 3/1 | s:hh ]", "[ 3/1 → 25/8 | s:hh ]", "[ 25/8 → 13/4 | s:hh ]", "[ 27/8 → 7/2 | s:hh ]", "[ 7/2 → 29/8 | s:hh ]", - "[ 29/8 → 15/4 | s:hh ]", - "[ 15/4 → 31/8 | s:hh ]", "[ 31/8 → 4/1 | s:hh ]", ] `; exports[`runs examples > example "undegradeBy" example index 1 1`] = ` [ - "[ 0/1 → 1/10 | s:hh pan:0 ]", - "[ 1/10 → 1/5 | s:hh pan:0 ]", + "[ 0/1 → 1/10 | s:hh pan:1 ]", + "[ 1/10 → 1/5 | s:hh pan:1 ]", "[ 1/5 → 3/10 | s:hh pan:0 ]", "[ 3/10 → 2/5 | s:hh pan:0 ]", "[ 2/5 → 1/2 | s:hh pan:0 ]", @@ -11829,18 +11840,18 @@ exports[`runs examples > example "undegradeBy" example index 1 1`] = ` "[ 7/10 → 4/5 | s:hh pan:0 ]", "[ 4/5 → 9/10 | s:hh pan:0 ]", "[ 9/10 → 1/1 | s:hh pan:0 ]", - "[ 1/1 → 11/10 | s:hh pan:1 ]", + "[ 1/1 → 11/10 | s:hh pan:0 ]", "[ 11/10 → 6/5 | s:hh pan:0 ]", "[ 6/5 → 13/10 | s:hh pan:0 ]", - "[ 13/10 → 7/5 | s:hh pan:1 ]", + "[ 13/10 → 7/5 | s:hh pan:0 ]", "[ 7/5 → 3/2 | s:hh pan:0 ]", - "[ 3/2 → 8/5 | s:hh pan:1 ]", + "[ 3/2 → 8/5 | s:hh pan:0 ]", "[ 8/5 → 17/10 | s:hh pan:0 ]", "[ 17/10 → 9/5 | s:hh pan:0 ]", "[ 9/5 → 19/10 | s:hh pan:0 ]", - "[ 19/10 → 2/1 | s:hh pan:0 ]", + "[ 19/10 → 2/1 | s:hh pan:1 ]", "[ 2/1 → 21/10 | s:hh pan:0 ]", - "[ 21/10 → 11/5 | s:hh pan:1 ]", + "[ 21/10 → 11/5 | s:hh pan:0 ]", "[ 11/5 → 23/10 | s:hh pan:0 ]", "[ 23/10 → 12/5 | s:hh pan:0 ]", "[ 12/5 → 5/2 | s:hh pan:0 ]", @@ -11851,14 +11862,14 @@ exports[`runs examples > example "undegradeBy" example index 1 1`] = ` "[ 29/10 → 3/1 | s:hh pan:0 ]", "[ 3/1 → 31/10 | s:hh pan:0 ]", "[ 31/10 → 16/5 | s:hh pan:1 ]", - "[ 16/5 → 33/10 | s:hh pan:0 ]", + "[ 16/5 → 33/10 | s:hh pan:1 ]", "[ 33/10 → 17/5 | s:hh pan:0 ]", "[ 17/5 → 7/2 | s:hh pan:0 ]", "[ 7/2 → 18/5 | s:hh pan:0 ]", "[ 18/5 → 37/10 | s:hh pan:0 ]", "[ 37/10 → 19/5 | s:hh pan:0 ]", - "[ 19/5 → 39/10 | s:hh pan:0 ]", - "[ 39/10 → 4/1 | s:hh pan:1 ]", + "[ 19/5 → 39/10 | s:hh pan:1 ]", + "[ 39/10 → 4/1 | s:hh pan:0 ]", ] `; @@ -11936,7 +11947,7 @@ exports[`runs examples > example "unit" example index 0 1`] = ` ] `; -exports[`runs examples > example "useOldRandom" example index 0 1`] = ` +exports[`runs examples > example "useRNG" example index 0 1`] = ` [ "[ 0/1 → 1/16 | note:D8 ]", "[ 1/16 → 1/8 | note:Bb7 ]", @@ -12163,38 +12174,38 @@ exports[`runs examples > example "vowel" example index 0 1`] = ` exports[`runs examples > example "vowel" example index 1 1`] = ` [ - "[ 0/1 → 1/8 | s:bd vowel:e ]", - "[ 1/8 → 1/4 | s:sd vowel:e ]", - "[ 1/4 → 3/8 | s:mt vowel:e ]", - "[ 3/8 → 1/2 | s:ht vowel:e ]", - "[ 1/2 → 5/8 | s:bd vowel:e ]", - "[ 11/16 → 3/4 | s:cp vowel:e ]", - "[ 3/4 → 7/8 | s:ht vowel:e ]", - "[ 7/8 → 1/1 | s:lt vowel:e ]", - "[ 1/1 → 9/8 | s:bd vowel:a ]", - "[ 9/8 → 5/4 | s:sd vowel:a ]", - "[ 5/4 → 11/8 | s:mt vowel:a ]", - "[ 11/8 → 3/2 | s:ht vowel:a ]", - "[ 3/2 → 13/8 | s:bd vowel:a ]", - "[ 27/16 → 7/4 | s:cp vowel:a ]", - "[ 7/4 → 15/8 | s:ht vowel:a ]", - "[ 15/8 → 2/1 | s:lt vowel:a ]", - "[ 2/1 → 17/8 | s:bd vowel:i ]", - "[ 17/8 → 9/4 | s:sd vowel:i ]", - "[ 9/4 → 19/8 | s:mt vowel:i ]", - "[ 19/8 → 5/2 | s:ht vowel:i ]", - "[ 5/2 → 21/8 | s:bd vowel:i ]", - "[ 43/16 → 11/4 | s:cp vowel:i ]", - "[ 11/4 → 23/8 | s:ht vowel:i ]", - "[ 23/8 → 3/1 | s:lt vowel:i ]", - "[ 3/1 → 25/8 | s:bd vowel:o ]", - "[ 25/8 → 13/4 | s:sd vowel:o ]", - "[ 13/4 → 27/8 | s:mt vowel:o ]", - "[ 27/8 → 7/2 | s:ht vowel:o ]", - "[ 7/2 → 29/8 | s:bd vowel:o ]", - "[ 59/16 → 15/4 | s:cp vowel:o ]", - "[ 15/4 → 31/8 | s:ht vowel:o ]", - "[ 31/8 → 4/1 | s:lt vowel:o ]", + "[ 0/1 → 1/8 | s:bd vowel:a ]", + "[ 1/8 → 1/4 | s:sd vowel:a ]", + "[ 1/4 → 3/8 | s:mt vowel:a ]", + "[ 3/8 → 1/2 | s:ht vowel:a ]", + "[ 1/2 → 5/8 | s:bd vowel:a ]", + "[ 11/16 → 3/4 | s:cp vowel:a ]", + "[ 3/4 → 7/8 | s:ht vowel:a ]", + "[ 7/8 → 1/1 | s:lt vowel:a ]", + "[ 1/1 → 9/8 | s:bd vowel:i ]", + "[ 9/8 → 5/4 | s:sd vowel:i ]", + "[ 5/4 → 11/8 | s:mt vowel:i ]", + "[ 11/8 → 3/2 | s:ht vowel:i ]", + "[ 3/2 → 13/8 | s:bd vowel:i ]", + "[ 27/16 → 7/4 | s:cp vowel:i ]", + "[ 7/4 → 15/8 | s:ht vowel:i ]", + "[ 15/8 → 2/1 | s:lt vowel:i ]", + "[ 2/1 → 17/8 | s:bd vowel:u ]", + "[ 17/8 → 9/4 | s:sd vowel:u ]", + "[ 9/4 → 19/8 | s:mt vowel:u ]", + "[ 19/8 → 5/2 | s:ht vowel:u ]", + "[ 5/2 → 21/8 | s:bd vowel:u ]", + "[ 43/16 → 11/4 | s:cp vowel:u ]", + "[ 11/4 → 23/8 | s:ht vowel:u ]", + "[ 23/8 → 3/1 | s:lt vowel:u ]", + "[ 3/1 → 25/8 | s:bd vowel:e ]", + "[ 25/8 → 13/4 | s:sd vowel:e ]", + "[ 13/4 → 27/8 | s:mt vowel:e ]", + "[ 27/8 → 7/2 | s:ht vowel:e ]", + "[ 7/2 → 29/8 | s:bd vowel:e ]", + "[ 59/16 → 15/4 | s:cp vowel:e ]", + "[ 15/4 → 31/8 | s:ht vowel:e ]", + "[ 31/8 → 4/1 | s:lt vowel:e ]", ] `; @@ -12316,16 +12327,16 @@ exports[`runs examples > example "wchoose" example index 0 1`] = ` "[ 7/5 → 8/5 | note:g2 s:sine ]", "[ 8/5 → 9/5 | note:d2 s:sine ]", "[ 9/5 → 2/1 | note:f1 s:sine ]", - "[ 2/1 → 11/5 | note:c2 s:sine ]", - "[ 11/5 → 12/5 | note:g2 s:bd n:6 ]", - "[ 12/5 → 13/5 | note:g2 s:sine ]", + "[ 2/1 → 11/5 | note:c2 s:bd n:6 ]", + "[ 11/5 → 12/5 | note:g2 s:sine ]", + "[ 12/5 → 13/5 | note:g2 s:bd n:6 ]", "[ 13/5 → 14/5 | note:d2 s:sine ]", "[ 14/5 → 3/1 | note:f1 s:sine ]", "[ 3/1 → 16/5 | note:c2 s:sine ]", "[ 16/5 → 17/5 | note:g2 s:sine ]", "[ 17/5 → 18/5 | note:g2 s:sine ]", "[ 18/5 → 19/5 | note:d2 s:sine ]", - "[ 19/5 → 4/1 | note:f1 s:bd n:6 ]", + "[ 19/5 → 4/1 | note:f1 s:sine ]", ] `; @@ -12333,33 +12344,33 @@ exports[`runs examples > example "wchooseCycles" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:bd ]", "[ 1/8 → 1/4 | s:bd ]", - "[ 1/4 → 3/8 | s:bd ]", + "[ 1/4 → 3/8 | s:sd ]", "[ 3/8 → 1/2 | s:bd ]", "[ 1/2 → 5/8 | s:bd ]", - "[ 5/8 → 3/4 | s:hh ]", + "[ 5/8 → 3/4 | s:bd ]", "[ 3/4 → 7/8 | s:bd ]", "[ 7/8 → 1/1 | s:bd ]", - "[ 1/1 → 9/8 | s:bd ]", + "[ 1/1 → 9/8 | s:hh ]", "[ 9/8 → 5/4 | s:bd ]", "[ 5/4 → 11/8 | s:bd ]", - "[ 11/8 → 3/2 | s:sd ]", + "[ 11/8 → 3/2 | s:bd ]", "[ 3/2 → 13/8 | s:bd ]", - "[ 13/8 → 7/4 | s:bd ]", + "[ 13/8 → 7/4 | s:sd ]", "[ 7/4 → 15/8 | s:bd ]", "[ 15/8 → 2/1 | s:bd ]", "[ 2/1 → 17/8 | s:bd ]", - "[ 17/8 → 9/4 | s:hh ]", - "[ 9/4 → 19/8 | s:hh ]", - "[ 19/8 → 5/2 | s:hh ]", + "[ 17/8 → 9/4 | s:bd ]", + "[ 9/4 → 19/8 | s:bd ]", + "[ 19/8 → 5/2 | s:bd ]", "[ 5/2 → 21/8 | s:bd ]", "[ 21/8 → 11/4 | s:bd ]", - "[ 11/4 → 23/8 | s:hh ]", - "[ 23/8 → 3/1 | s:sd ]", + "[ 11/4 → 23/8 | s:sd ]", + "[ 23/8 → 3/1 | s:bd ]", "[ 3/1 → 25/8 | s:bd ]", - "[ 25/8 → 13/4 | s:hh ]", + "[ 25/8 → 13/4 | s:bd ]", "[ 13/4 → 27/8 | s:bd ]", "[ 27/8 → 7/2 | s:bd ]", - "[ 7/2 → 29/8 | s:sd ]", + "[ 7/2 → 29/8 | s:bd ]", "[ 29/8 → 15/4 | s:bd ]", "[ 15/4 → 31/8 | s:bd ]", "[ 31/8 → 4/1 | s:bd ]", @@ -12374,45 +12385,45 @@ exports[`runs examples > example "wchooseCycles" example index 1 1`] = ` "[ 1/4 → 1/3 | s:bd ]", "[ 1/3 → 5/12 | s:bd ]", "[ 5/12 → 1/2 | s:bd ]", - "[ 1/2 → 7/12 | s:hh ]", - "[ 7/12 → 2/3 | s:hh ]", - "[ 2/3 → 3/4 | s:hh ]", - "[ 3/4 → 5/6 | s:hh ]", - "[ 5/6 → 11/12 | s:hh ]", - "[ 11/12 → 1/1 | s:hh ]", + "[ 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:hh ]", - "[ 4/3 → 17/12 | s:hh ]", - "[ 17/12 → 3/2 | s:hh ]", - "[ 3/2 → 19/12 | s:bd ]", - "[ 19/12 → 5/3 | s:bd ]", - "[ 5/3 → 7/4 | s:bd ]", - "[ 7/4 → 11/6 | s:hh ]", - "[ 11/6 → 23/12 | s:hh ]", - "[ 23/12 → 2/1 | s:hh ]", + "[ 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:hh ]", - "[ 31/12 → 8/3 | s:hh ]", - "[ 8/3 → 11/4 | s:hh ]", - "[ 11/4 → 17/6 | s:sd ]", - "[ 17/6 → 35/12 | s:sd ]", - "[ 35/12 → 3/1 | s:sd ]", - "[ 3/1 → 37/12 | s:hh ]", - "[ 37/12 → 19/6 | s:hh ]", - "[ 19/6 → 13/4 | s:hh ]", - "[ 13/4 → 10/3 | s:bd ]", - "[ 10/3 → 41/12 | s:bd ]", - "[ 41/12 → 7/2 | s:bd ]", - "[ 7/2 → 43/12 | s:bd ]", - "[ 43/12 → 11/3 | s:bd ]", - "[ 11/3 → 15/4 | s:bd ]", + "[ 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 ]", @@ -12427,9 +12438,9 @@ exports[`runs examples > example "wchooseCycles" example index 2 1`] = ` "[ 1/4 → 1/3 | s:hh ]", "[ 1/3 → 5/12 | s:hh ]", "[ 5/12 → 1/2 | s:hh ]", - "[ 1/2 → 17/32 | s:bd ]", - "[ 19/32 → 5/8 | s:bd ]", - "[ 11/16 → 23/32 | s:bd ]", + "[ 1/2 → 7/12 | s:hh ]", + "[ 7/12 → 2/3 | s:hh ]", + "[ 2/3 → 3/4 | s:hh ]", "[ 3/4 → 5/6 | s:hh ]", "[ 5/6 → 11/12 | s:hh ]", "[ 11/12 → 1/1 | s:hh ]", @@ -12445,27 +12456,27 @@ exports[`runs examples > example "wchooseCycles" example index 2 1`] = ` "[ 7/4 → 11/6 | s:hh ]", "[ 11/6 → 23/12 | s:hh ]", "[ 23/12 → 2/1 | s:hh ]", - "[ 2/1 → 65/32 | s:bd ]", - "[ 67/32 → 17/8 | s:bd ]", - "[ 35/16 → 71/32 | 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:hh ]", - "[ 31/12 → 8/3 | s:hh ]", - "[ 8/3 → 11/4 | s:hh ]", + "[ 5/2 → 81/32 | s:bd ]", + "[ 83/32 → 21/8 | s:bd ]", + "[ 43/16 → 87/32 | s:bd ]", "[ 11/4 → 17/6 | s:hh ]", "[ 17/6 → 35/12 | s:hh ]", "[ 35/12 → 3/1 | s:hh ]", - "[ 3/1 → 37/12 | s:hh ]", - "[ 37/12 → 19/6 | s:hh ]", - "[ 19/6 → 13/4 | s:hh ]", + "[ 3/1 → 97/32 | s:bd ]", + "[ 99/32 → 25/8 | s:bd ]", + "[ 51/16 → 103/32 | s:bd ]", "[ 13/4 → 10/3 | s:hh ]", "[ 10/3 → 41/12 | s:hh ]", "[ 41/12 → 7/2 | s:hh ]", - "[ 7/2 → 113/32 | s:bd ]", - "[ 115/32 → 29/8 | s:bd ]", - "[ 59/16 → 119/32 | s:bd ]", + "[ 7/2 → 43/12 | s:hh ]", + "[ 43/12 → 11/3 | s:hh ]", + "[ 11/3 → 15/4 | s:hh ]", "[ 15/4 → 23/6 | s:hh ]", "[ 23/6 → 47/12 | s:hh ]", "[ 47/12 → 4/1 | s:hh ]", diff --git a/vitest.setup.mjs b/vitest.setup.mjs index bb9d24b0..ea965df6 100644 --- a/vitest.setup.mjs +++ b/vitest.setup.mjs @@ -1,7 +1,7 @@ import { afterEach } from 'vitest'; -import { useOldRandom } from './packages/core/signal.mjs'; +import { useRNG } from './packages/core/signal.mjs'; afterEach(() => { // Avoid bleed between tests - useOldRandom(false); + useRNG('legacy'); }); diff --git a/website/src/repl/tunes.mjs b/website/src/repl/tunes.mjs index 23efb606..697e83c9 100644 --- a/website/src/repl/tunes.mjs +++ b/website/src/repl/tunes.mjs @@ -393,7 +393,7 @@ samples({ bass: { d2: 'https://cdn.freesound.org/previews/608/608286_13074022-lq.mp3' } }) -useOldRandom(true) +useRNG('legacy') stack( // bells @@ -432,7 +432,7 @@ export const festivalOfFingers3 = `// "Festival of fingers 3" // @by Felix Roos setcps(1) -useOldRandom(true) +useRNG('legacy') n("[-7*3],0,2,6,[8 7]") .echoWith( @@ -457,7 +457,7 @@ export const meltingsubmarine = `// "Melting submarine" // @by Felix Roos samples('github:tidalcycles/dirt-samples') -useOldRandom(true) +useRNG('legacy') stack( s("bd:5,[~ ],hh27(3,4,1)") // drums @@ -608,7 +608,7 @@ export const belldub = `// "Belldub" samples({ bell: {b4:'https://cdn.freesound.org/previews/339/339809_5121236-lq.mp3'}}) // "Hand Bells, B, Single.wav" by InspectorJ (www.jshaw.co.uk) of Freesound.org -useOldRandom(true) +useRNG('legacy') stack( // bass @@ -646,7 +646,7 @@ export const dinofunk = `// "Dinofunk" // @by Felix Roos setcps(1) -useOldRandom(true) +useRNG('legacy') samples({bass:'https://cdn.freesound.org/previews/614/614637_2434927-hq.mp3', dino:{b4:'https://cdn.freesound.org/previews/316/316403_5123851-hq.mp3'}}) @@ -675,7 +675,7 @@ export const sampleDemo = `// "Sample demo" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos -useOldRandom(true) +useRNG('legacy') stack( // percussion @@ -695,7 +695,7 @@ export const holyflute = `// "Holy flute" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos -useOldRandom(true) +useRNG('legacy') "c3 eb3(3,8) c4/2 g3*2" .superimpose( @@ -712,7 +712,7 @@ export const flatrave = `// "Flatrave" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos -useOldRandom(true) +useRNG('legacy') stack( s("bd*2,~ [cp,sd]").bank('RolandTR909'), @@ -742,7 +742,7 @@ export const amensister = `// "Amensister" samples('github:tidalcycles/dirt-samples') -useOldRandom(true) +useRNG('legacy') stack( // amen @@ -851,7 +851,7 @@ export const arpoon = `// "Arpoon" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos -useOldRandom(true) +useRNG('legacy') samples('github:tidalcycles/dirt-samples') From e0344dbd67c741a4586e0d345bf4cbe85d43d4c6 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 13 Dec 2025 17:49:19 -0600 Subject: [PATCH 137/194] Simple seeding for this first iteration --- packages/core/signal.mjs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index a56009ad..c7e143ae 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -359,7 +359,7 @@ export const binaryNL = (n, nBits = 16) => { * .partials(randL(8)) */ export const randL = (n) => { - return signal((t) => (nVal) => timeToRands(t, nVal).map(Math.abs)).appLeft(reify(n)); + return signal((t) => (nVal) => getRandsAtTime(t, nVal).map(Math.abs)).appLeft(reify(n)); }; export const randrun = (n) => { @@ -434,19 +434,6 @@ export const withSeed = (func, pat) => { * $: s("bd*4").degrade().seed(1); // Will degrade different events from the hi-hat */ export const seed = register('seed', (n, pat) => { - return withSeed((prev) => (prev !== undefined ? randAt(prev, n) * Number.MAX_SAFE_INTEGER : n), pat); -}); - -/** - * Set the seed for random signals. Differs from `seed` in that `seed` can be used - * multiple times with the final signal depending on all seeds. `setSeed` on the - * other hand overwrites the previous calls to `seed`. - * - * @name setSeed - * @param {number} n An arbitrary number to be used as the new seed. 0 is the same - * as the default random behavior. - */ -export const setSeed = register('setSeed', (n, pat) => { return withSeed(() => n, pat); }); From 34991e9602a3d1f194d99ff45c48a4640edcdaaa Mon Sep 17 00:00:00 2001 From: jeromew Date: Sun, 14 Dec 2025 11:27:41 +0000 Subject: [PATCH 138/194] Fix: wrong warning in build environments --- 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 5abc2bad..a3ba7df9 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -593,7 +593,7 @@ export const releaseAudioNode = (node) => { // make sure all AudioScheduledSourceNodes are in a stopped state // https://developer.mozilla.org/en-US/docs/Web/API/AudioScheduledSourceNode if (node instanceof AudioScheduledSourceNode) { - if (node.onended && node.onended.name !== 'cleanup') { + if (process.env.NODE_ENV === 'development' && node.onended && node.onended.name !== 'cleanup') { logger( `[superdough] Deprecation warning: it seems your code path is setting 'node.onended = callback' instead of using the onceEnded helper`, ); From 181ebee374058b6bf7fcf686f39e7ab3ef23d733 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 19 Dec 2025 01:25:58 +0100 Subject: [PATCH 139/194] fix: repl package init audio properly --- examples/buildless/web-component-no-iframe.html | 3 ++- packages/repl/repl-component.mjs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/buildless/web-component-no-iframe.html b/examples/buildless/web-component-no-iframe.html index 8afa85cb..ae4fb700 100644 --- a/examples/buildless/web-component-no-iframe.html +++ b/examples/buildless/web-component-no-iframe.html @@ -1,4 +1,5 @@ - + + - + diff --git a/website/src/components/HeadCommonNext.astro b/website/src/components/HeadCommonNext.astro index 9f323a7a..1f878bac 100644 --- a/website/src/components/HeadCommonNext.astro +++ b/website/src/components/HeadCommonNext.astro @@ -24,7 +24,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL - + From 3b2e9fa0b4bdf1837b7993b59fbb3516d14bb25a Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 10 Jan 2026 01:22:50 -0500 Subject: [PATCH 174/194] wip --- packages/superdough/superdough.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 6ef8435c..c4793927 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -547,6 +547,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } else if (getSound(s)) { const { onTrigger } = getSound(s); const onEnded = () => { + chain.releaseNodes(); audioNodes.forEach((n) => (isPoolable(n) ? releaseNodeToPool(n) : n?.disconnect())); activeSoundSources.delete(chainID); }; From e837c516c5eb3815e838df1707ff05a99d740261 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 10 Jan 2026 01:37:14 -0500 Subject: [PATCH 175/194] merge main --- packages/superdough/superdough.mjs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index c4793927..2fe80d44 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -10,17 +10,7 @@ import './vowel.mjs'; import { clamp, nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs'; import workletsUrl from './worklets.mjs?audioworklet'; import { getNodeFromPool, isPoolable, releaseNodeToPool } from './nodePools.mjs'; -import { - createFilter, - effectSend, - gainNode, - getCompressor, - getDistortion, - getLfo, - getWorklet, - releaseAudioNode, - webAudioTimeout, -} from './helpers.mjs'; +import { createFilter, effectSend, gainNode, getCompressor, getDistortion, getLfo, getWorklet } from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { connectLFO, connectEnvelope, connectBusModulator } from './modulators.mjs'; @@ -429,7 +419,7 @@ class Chain { return this; } releaseNodes() { - this.audioNodes.forEach((n) => releaseAudioNode(n)); + this.audioNodes.forEach((n) => (isPoolable(n) ? releaseNodeToPool(n) : n?.disconnect())); this.audioNodes = []; this.tails = []; } @@ -548,7 +538,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) const { onTrigger } = getSound(s); const onEnded = () => { chain.releaseNodes(); - audioNodes.forEach((n) => (isPoolable(n) ? releaseNodeToPool(n) : n?.disconnect())); activeSoundSources.delete(chainID); }; // We have to use onEnded because some sources (e.g. `sampler`) have From 0e9c50bf0a55e3225a26404ca593a9e34c484d45 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 10 Jan 2026 01:39:39 -0500 Subject: [PATCH 176/194] fix import --- packages/superdough/wavetable.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index f5da1f3e..ee044ee4 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -9,6 +9,7 @@ import { getPitchEnvelope, getVibratoOscillator, webAudioTimeout, + releaseAudioNode } from './helpers.mjs'; import { getNodeFromPool, markWorkletAsDead, releaseNodeToPool } from './nodePools.mjs'; import { logger } from './logger.mjs'; From a205f7d87388c8fba8366bee02db55e7a030521e Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 10 Jan 2026 01:42:06 -0500 Subject: [PATCH 177/194] rm dead code --- packages/superdough/superdough.mjs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 2fe80d44..5d562ee2 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -540,18 +540,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) chain.releaseNodes(); activeSoundSources.delete(chainID); }; - // We have to use onEnded because some sources (e.g. `sampler`) have - // an internal duration which is longer than `value.duration` - // const onEnded = () => - // webAudioTimeout( - // ac, - // () => { - // chain.releaseNodes(); - // activeSoundSources.delete(chainID); - // }, - // 0, - // endWithRelease, - // ); + const soundHandle = await onTrigger(t, value, onEnded, cps); if (soundHandle) { From 7a5020aa71f5692b627ac39e4d3ca7dcc9464896 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 10 Jan 2026 01:42:43 -0500 Subject: [PATCH 178/194] format --- packages/superdough/superdough.mjs | 2 +- packages/superdough/wavetable.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 5d562ee2..dea314ea 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -540,7 +540,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) chain.releaseNodes(); activeSoundSources.delete(chainID); }; - + const soundHandle = await onTrigger(t, value, onEnded, cps); if (soundHandle) { diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index ee044ee4..adf8b0c4 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -9,7 +9,7 @@ import { getPitchEnvelope, getVibratoOscillator, webAudioTimeout, - releaseAudioNode + releaseAudioNode, } from './helpers.mjs'; import { getNodeFromPool, markWorkletAsDead, releaseNodeToPool } from './nodePools.mjs'; import { logger } from './logger.mjs'; From b4e00538f86c7d50a276bcfe6698ca4d38ab9fa0 Mon Sep 17 00:00:00 2001 From: floy Date: Sat, 10 Jan 2026 15:30:07 +0100 Subject: [PATCH 179/194] Don't show 'No sounds loaded' text if search is active --- 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 52fbf928..0e46b138 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -210,7 +210,7 @@ export function SoundsTab() { ) : ( '' )} - {!soundEntries.length && soundsFilter !== 'importSounds' ? 'No sounds loaded' : ''} + {!soundEntries.length && soundsFilter !== 'importSounds' && search == '' ? 'No sounds loaded' : ''} ); From fd0209dbc067b4c683f946738dedc9da3f752942 Mon Sep 17 00:00:00 2001 From: floy Date: Sat, 10 Jan 2026 15:39:02 +0100 Subject: [PATCH 180/194] Show explicit help text when search doesn't match --- website/src/repl/components/panel/SoundsTab.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index 0e46b138..2060c64a 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -210,7 +210,10 @@ export function SoundsTab() { ) : ( '' )} - {!soundEntries.length && soundsFilter !== 'importSounds' && search == '' ? 'No sounds loaded' : ''} + {!soundEntries.length && soundsFilter !== 'importSounds' ? + search == '' ? 'No sounds loaded' : 'No sounds found' + : '' + } ); From 0cf11adc2c8416ed1df1563b3f6c16cc7dc637fd Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 10 Jan 2026 13:07:54 -0600 Subject: [PATCH 181/194] Allow naked distortions for the purpose of FX --- packages/core/pattern.mjs | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 02cf9188..58c83a6f 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3586,6 +3586,20 @@ export const morph = (frompat, topat, bypat) => { return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by)))); }; +const _distortWithAlg = function (name) { + const func = function (args, pat) { + const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name])); + if (!pat) { + return pure({}).distort(argsPat); + } + return pat.distort(argsPat); + }; + Pattern.prototype[name] = function (args) { + return func(args, this); + }; + return func; +}; + /** * Soft-clipping distortion * @@ -3594,6 +3608,8 @@ export const morph = (frompat, topat, bypat) => { * @param {number | Pattern} volume linear postgain of the distortion * */ +export const soft = _distortWithAlg('soft'); + /** * Hard-clipping distortion * @@ -3602,6 +3618,8 @@ export const morph = (frompat, topat, bypat) => { * @param {number | Pattern} volume linear postgain of the distortion * */ +export const hard = _distortWithAlg('hard'); + /** * Cubic polynomial distortion * @@ -3610,6 +3628,8 @@ export const morph = (frompat, topat, bypat) => { * @param {number | Pattern} volume linear postgain of the distortion * */ +export const cubic = _distortWithAlg('cubic'); + /** * Diode-emulating distortion * @@ -3618,6 +3638,8 @@ export const morph = (frompat, topat, bypat) => { * @param {number | Pattern} volume linear postgain of the distortion * */ +export const diode = _distortWithAlg('diode'); + /** * Asymmetrical diode distortion * @@ -3626,6 +3648,8 @@ export const morph = (frompat, topat, bypat) => { * @param {number | Pattern} volume linear postgain of the distortion * */ +export const asym = _distortWithAlg('asym'); + /** * Wavefolding distortion * @@ -3634,6 +3658,8 @@ export const morph = (frompat, topat, bypat) => { * @param {number | Pattern} volume linear postgain of the distortion * */ +export const fold = _distortWithAlg('fold'); + /** * Wavefolding distortion composed with sinusoid * @@ -3642,6 +3668,8 @@ export const morph = (frompat, topat, bypat) => { * @param {number | Pattern} volume linear postgain of the distortion * */ +export const sinefold = _distortWithAlg('sinefold'); + /** * Distortion via Chebyshev polynomials * @@ -3650,14 +3678,7 @@ export const morph = (frompat, topat, bypat) => { * @param {number | Pattern} volume linear postgain of the distortion * */ -const distAlgoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev']; -for (const name of distAlgoNames) { - // Add aliases for distortion algorithms - Pattern.prototype[name] = function (args) { - const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name])); - return this.distort(argsPat); - }; -} +export const chebyshev = _distortWithAlg('chebyshev'); /** * Turns a list of patterns into a single pattern which outputs list-values From 447692106923167d3c0ddc308041bb79d4a944ca Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 10 Jan 2026 16:46:11 -0500 Subject: [PATCH 182/194] use releasenode --- packages/superdough/superdough.mjs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index dea314ea..41b5ce97 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -419,7 +419,7 @@ class Chain { return this; } releaseNodes() { - this.audioNodes.forEach((n) => (isPoolable(n) ? releaseNodeToPool(n) : n?.disconnect())); + this.audioNodes.forEach((n) => (isPoolable(n) ? releaseNodeToPool(n) : releaseAudioNode(n))); this.audioNodes = []; this.tails = []; } @@ -536,10 +536,17 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) nodes.main['source'] = [sourceNode]; } else if (getSound(s)) { const { onTrigger } = getSound(s); - const onEnded = () => { - chain.releaseNodes(); - activeSoundSources.delete(chainID); - }; + + const onEnded = () => + webAudioTimeout( + ac, + () => { + chain.releaseNodes(); + activeSoundSources.delete(chainID); + }, + 0, + endWithRelease, + ); const soundHandle = await onTrigger(t, value, onEnded, cps); From 741ff3f3f0117298b5c665869979d4144a706984 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 10 Jan 2026 16:55:53 -0500 Subject: [PATCH 183/194] fix import --- packages/superdough/superdough.mjs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 41b5ce97..d2baa939 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -10,7 +10,17 @@ import './vowel.mjs'; import { clamp, nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs'; import workletsUrl from './worklets.mjs?audioworklet'; import { getNodeFromPool, isPoolable, releaseNodeToPool } from './nodePools.mjs'; -import { createFilter, effectSend, gainNode, getCompressor, getDistortion, getLfo, getWorklet } from './helpers.mjs'; +import { + createFilter, + effectSend, + gainNode, + getCompressor, + getDistortion, + getLfo, + getWorklet, + releaseAudioNode, + webAudioTimeout, +} from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { connectLFO, connectEnvelope, connectBusModulator } from './modulators.mjs'; From 0571a8dc7d706f2fbd2647668b8acba18625ece0 Mon Sep 17 00:00:00 2001 From: gueejla Date: Sat, 10 Jan 2026 22:11:13 -0600 Subject: [PATCH 184/194] fixes Serial onTrigger() params #1633 --- packages/serial/serial.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/serial/serial.mjs b/packages/serial/serial.mjs index 69210952..e007b24b 100644 --- a/packages/serial/serial.mjs +++ b/packages/serial/serial.mjs @@ -68,7 +68,7 @@ Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids if (!(name in writeMessagers)) { getWriter(name, br); } - const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => { + const onTrigger = (hap, currentTime, _cps, targetTime) => { var message = ''; var chk = 0; if (typeof hap.value === 'object') { From 604a1b770cab5f55d6cada4915bccff4de460435 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 11 Jan 2026 00:27:07 -0500 Subject: [PATCH 185/194] dialog --- .../panel/ImportPrebakeScriptButton.jsx | 15 +++++++- .../src/repl/components/panel/SettingsTab.jsx | 36 ++++++------------- website/src/repl/util.mjs | 16 +++++++-- 3 files changed, 39 insertions(+), 28 deletions(-) diff --git a/website/src/repl/components/panel/ImportPrebakeScriptButton.jsx b/website/src/repl/components/panel/ImportPrebakeScriptButton.jsx index c997f6b6..64ce198b 100644 --- a/website/src/repl/components/panel/ImportPrebakeScriptButton.jsx +++ b/website/src/repl/components/panel/ImportPrebakeScriptButton.jsx @@ -1,6 +1,7 @@ import { errorLogger } from '@strudel/core'; import { useSettings, storePrebakeScript } from '../../../settings.mjs'; import { SpecialActionInput } from '../button/action-button'; +import { confirmDialog, SETTING_CHANGE_RELOAD_MSG } from '@src/repl/util.mjs'; async function importScript(script) { const reader = new FileReader(); @@ -23,7 +24,19 @@ export function ImportPrebakeScriptButton() { type="file" label="import prebake script" accept=".strudel" - onChange={(e) => importScript(e.target.files[0])} + onChange={async (e) => { + const file = e.target.files[0]; + const confirmed = await confirmDialog(SETTING_CHANGE_RELOAD_MSG); + if (!confirmed) { + return; + } + try { + await importScript(file); + window.location.reload(); + } catch (e) { + errorLogger(e); + } + }} /> ); } diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index f46fa661..7f488c54 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -1,13 +1,13 @@ import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs'; import { themes } from '@strudel/codemirror'; import { Textbox } from '../textbox/Textbox.jsx'; -import { isUdels } from '../../util.mjs'; +import { confirmAndReloadPage, isUdels } from '../../util.mjs'; import { ButtonGroup } from './Forms.jsx'; 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 { SpecialActionButton } from '../button/action-button.jsx'; import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx'; function Checkbox({ label, value, onChange, disabled = false }) { @@ -86,8 +86,6 @@ const fontFamilyOptions = { galactico: 'galactico', }; -const RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?'; - export function SettingsTab({ started }) { const { theme, @@ -127,11 +125,8 @@ export function SettingsTab({ started }) { isDisabled={started} audioDeviceName={audioDeviceName} onChange={(audioDeviceName) => { - confirmDialog(RELOAD_MSG).then((r) => { - if (r == true) { - settingsMap.setKey('audioDeviceName', audioDeviceName); - return window.location.reload(); - } + confirmAndReloadPage(() => { + settingsMap.setKey('audioDeviceName', audioDeviceName); }); }} /> @@ -141,11 +136,8 @@ export function SettingsTab({ started }) { { - confirmDialog(RELOAD_MSG).then((r) => { - if (r == true) { - settingsMap.setKey('audioEngineTarget', target); - return window.location.reload(); - } + confirmAndReloadPage(() => { + settingsMap.setKey('audioEngineTarget', target); }); }} /> @@ -175,12 +167,9 @@ export function SettingsTab({ started }) { label="Multi Channel Orbits" onChange={(cbEvent) => { const val = cbEvent.target.checked; - confirmDialog(RELOAD_MSG).then((r) => { - if (r == true) { - settingsMap.setKey('multiChannelOrbits', val); - setMultiChannelOrbits(val); - return window.location.reload(); - } + confirmAndReloadPage(() => { + settingsMap.setKey('multiChannelOrbits', val); + setMultiChannelOrbits(val); }); }} value={multiChannelOrbits} @@ -297,11 +286,8 @@ export function SettingsTab({ started }) { label="Sync across Browser Tabs / Windows" onChange={(cbEvent) => { const newVal = cbEvent.target.checked; - confirmDialog(RELOAD_MSG).then((r) => { - if (r) { - settingsMap.setKey('isSyncEnabled', newVal); - window.location.reload(); - } + confirmAndReloadPage(() => { + settingsMap.setKey('isSyncEnabled', newVal); }); }} disabled={shouldAlwaysSync} diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 4a7cb26a..06dad246 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -1,4 +1,4 @@ -import { code2hash, evalScope, hash2code, logger } from '@strudel/core'; +import { code2hash, errorLogger, evalScope, hash2code, logger } from '@strudel/core'; import { settingPatterns } from '../settings.mjs'; import { setVersionDefaults } from '@strudel/webaudio'; import { getMetadata } from '../metadata_parser'; @@ -107,7 +107,19 @@ export function confirmDialog(msg) { resolve(confirmed); }); } - +export const SETTING_CHANGE_RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?'; +export function confirmAndReloadPage(onSuccess) { + confirmDialog(SETTING_CHANGE_RELOAD_MSG).then((r) => { + if (r == true) { + try { + onSuccess(); + return window.location.reload(); + } catch (e) { + errorLogger(e); + } + } + }); +} //RIP due to SPAM // let lastShared; // export async function shareCode(codeToShare) { From 0174e086399ae0411e7f6da31cc3fd2bf8df0840 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 11 Jan 2026 10:50:44 +0100 Subject: [PATCH 186/194] fix: add trem to top level --- 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 8fef9129..f15b3d4c 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -877,7 +877,7 @@ export const { coarse } = registerControl('coarse'); * note("d d d# d".fast(4)).s("supersaw").tremolo("<3 2 100> ").tremoloskew("<.5>") * */ -export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem'); +export const { tremolo, trem } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem'); /** * Modulate the amplitude of a sound with a continuous waveform From c74707e804f926ffc3910a3e80aa8bda8e86d5a4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 11 Jan 2026 10:58:13 +0100 Subject: [PATCH 187/194] fix: export start cycle min 0 --- website/src/repl/components/panel/ExportTab.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/repl/components/panel/ExportTab.jsx b/website/src/repl/components/panel/ExportTab.jsx index 9671ff1a..cbb132e4 100644 --- a/website/src/repl/components/panel/ExportTab.jsx +++ b/website/src/repl/components/panel/ExportTab.jsx @@ -66,7 +66,7 @@ export default function ExportTab(Props) {
{ let v = parseInt(e.target.value); From 8f3dc8daea3759330f4a7bd930a42d5653d500a1 Mon Sep 17 00:00:00 2001 From: yaxu Date: Sun, 11 Jan 2026 11:34:55 +0100 Subject: [PATCH 188/194] Update website/src/pages/learn/faq.mdx --- website/src/pages/learn/faq.mdx | 35 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/website/src/pages/learn/faq.mdx b/website/src/pages/learn/faq.mdx index b91b48cc..7fbc0d57 100644 --- a/website/src/pages/learn/faq.mdx +++ b/website/src/pages/learn/faq.mdx @@ -8,27 +8,26 @@ import { JsDoc } from '../../docs/JsDoc'; # Frequently Asked Questions -This page contains frequently asked questions. Usually, the topic is explained in more detail in a section which is linked in the answer. This page's aim is to give an overview over topics which are important to the users of strudel. +This page contains frequently asked questions, with answers. Usually, the topic is explained in more detail in a section which is linked in the answer. ## Is Strudel/Tidal free? -Yes - there is no charge, this is a collective open source project, and the music you make with it is your own. +Yes - there is no charge, this is a collective open source project, and the music you make with it is your own. However if you can, please make a one-off or regular donation to our [opencollective fund](https://opencollective.com/tidalcycles), that supports the software and cultural development of Strudel and other Uzu languages. -However there are some caveats - the source code must stay free, i.e. you cannot distribute strudel or tidal as part of projects with incompatible licenses - see the [license](https://www.gnu.org/licenses/agpl-3.0.en.html) for details. The contributed examples and tracks are also separately licensed, and must not e.g. be used to train AI models without permission. +While there is no charge there are some caveats, e.g.: + +* the source code must stay free, i.e. you cannot distribute strudel or tidal as part of projects with incompatible licenses - see the [license](https://www.gnu.org/licenses/agpl-3.0.en.html) for details. +* the contributed examples and tracks are also separately licensed, and must not e.g. be used to train AI models without permission. ## How to record or export audio? -There are multiple ways to record the audio -- and video -- output of Strudel: +Strudel is not a digital audio workstation and does not operate following the same principles shared by most traditional audio softwares. However, there are multiple ways to record the audio -- and video -- output of Strudel: - - capture the raw stereo signal coming out of your web browser. - - - use the alternative SuperDirt audio engine. Read [this page](/learn/input-output/#oscsuperdirtstrudeldirt) to know more about it. - - - capture the audio/video stream using a capture tool such as [OBS](). - - - don't record anything and code it again in front of your friends. - -You will need an external audio editor/DAW such as Reaper/Audacity/Ardour, etc. Strudel itself does not have functionality for exporting stems / individual tracks to an audio or midi file. Strudel is not a digital audio workstation and does not operate following the same principles shared by most traditional audio softwares. +* Use the 'export' tab to render and download as an audio file. +* capture the raw stereo signal coming out of your web browser. You will need an external audio editor/DAW such as Reaper/Audacity/Ardour, etc. +* use the alternative SuperDirt audio engine. Read [this page](/learn/input-output/#oscsuperdirtstrudeldirt) to know more about it. +* capture the audio/video stream using a capture tool such as [OBS](), which is designed for live streaming, but also works very well for recording. +* don't record anything and code it again in front of your friends. ## Can I use strudel with my IDE? @@ -37,13 +36,13 @@ Yes you can. There are experimental modes, made by community members, for severa - VS Code: [Strudel VS](https://marketplace.visualstudio.com/items?itemName=cmillsdev.strudelvs): an experimental mode for Microsoft VSCode. A revived version of [TidalStrudel](https://marketplace.visualstudio.com/items?itemName=roipoussiere.tidal-strudel), which is defunct. - nvim: [strudel.nvim](https://github.com/gruvw/strudel.nvim) -## How can I record samples? +## How can I use my own samples? -You can use your own samples with Strudel. There are multiple ways to load your sample collection. Some methods are good for quick experimentation, some others are good to share your audio collection with other musicians: +There are multiple ways to load your sample collection. Some methods are good for quick experimentation, some others are good to share your audio collection with other musicians: - - Import folders [from the interface](/learn/samples/#from-disk-via-import-sounds-folder) - - - - Host your sound library e.g. on github and [load them from an URL](/learn/samples/#loading-custom-samples) +- Import folders [from the interface](/learn/samples/#from-disk-via-import-sounds-folder). These are stored locally in your web browser, and not uploaded. +- Serve a folder of samples locally using the [strudel 'sampler' commandline tool](https://strudel.cc/learn/samples/#from-disk-via-strudelsampler). This can be most reliable method, but requires [nodejs](https://nodejs.org) to be installed. +- Host your sound library online on the web and [load them from an URL](/learn/samples/#loading-custom-samples) ## Can I use Strudel with AI/LLM tools? From 25dbfe4c7d38330892a2e8000eb75f8569b7dcfa Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 11 Jan 2026 11:43:04 +0100 Subject: [PATCH 189/194] fix: remove faulty default readme --- website/README.md | 184 +--------------------------------------------- 1 file changed, 1 insertion(+), 183 deletions(-) diff --git a/website/README.md b/website/README.md index 115d6d55..84bf732e 100644 --- a/website/README.md +++ b/website/README.md @@ -3,186 +3,4 @@ This is the website for Strudel, deployed at [strudel.cc](https://strudel.cc). It includes the REPL live coding editor and the documentation site. -## Run locally - -```bash -# from project root -npm run setup -npm run repl -``` - -## Build - -```bash -cd website -npm run build # <- builds repl + tutorial to ../docs -npm run preview # <- test static build -``` - -## Generate PWA icons - -```sh -cd website/public -npx pwa-asset-generator icon.png icons -``` - -# Standard Readme of Astro Starter Kit: Docs Site - -```bash -npm create astro@latest -- --template docs -``` - -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/docs) -[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/s/github/withastro/astro/tree/latest/examples/docs) - -![docs](https://user-images.githubusercontent.com/4677417/186189283-0831b9ab-d6b9-485d-8955-3057e532ab31.png) - - -## Features - -- ✅ **Full Markdown support** -- ✅ **Responsive mobile-friendly design** -- ✅ **Sidebar navigation** -- ✅ **Search (powered by Algolia)** -- ✅ **Multi-language i18n** -- ✅ **Automatic table of contents** -- ✅ **Automatic list of contributors** -- ✅ (and, best of all) **dark mode** - -## Commands Cheatsheet - -All commands are run from the root of the project, from a terminal: - -| Command | Action | -| :--------------------- | :----------------------------------------------- | -| `npm install` | Installs dependencies | -| `npm run dev` | Starts local dev server at `localhost:4321` | -| `npm run build` | Build your production site to `./dist/` | -| `npm run preview` | Preview your build locally, before deploying | -| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | -| `npm run astro --help` | Get help using the Astro CLI | - -To deploy your site to production, check out our [Deploy an Astro Website](https://docs.astro.build/guides/deploy) guide. - -## New to Astro? - -Welcome! Check out [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). - -## Customize This Theme - -edit: removed, as css styles have been replaced with tailwind styles - -### Site metadata - -`src/config.ts` contains several data objects that describe metadata about your site like title, description, default language, and Open Graph details. You can customize these to match your project. - -## Page metadata - -Astro uses frontmatter in Markdown pages to choose layouts and pass properties to those layouts. If you are using the default layout, you can customize the page in many different ways to optimize SEO and other things. For example, you can use the `title` and `description` properties to set the document title, meta title, meta description, and Open Graph description. - -```markdown ---- -title: Example title -description: Really cool docs example that uses Astro -layout: ../../layouts/MainLayout.astro ---- - -# Page content... -``` - -For more SEO related properties, look at `src/components/HeadSEO.astro` - -### Sidebar navigation - -The sidebar navigation is controlled by the `SIDEBAR` variable in your `src/config.ts` file. You can customize the sidebar by modifying this object. A default, starter navigation has already been created for you. - -```ts -export const SIDEBAR = { - en: [ - { text: "Section Header", header: true }, - { text: "Introduction", link: "en/introduction" }, - { text: "Page 2", link: "en/page-2" }, - { text: "Page 3", link: "en/page-3" }, - - { text: "Another Section", header: true }, - { text: "Page 4", link: "en/page-4" }, - ], -}; -``` - -Note the top-level `en` key: This is needed for multi-language support. You can change it to whatever language you'd like, or add new languages as you go. More details on this below. - -### Multiple Languages support - -The Astro docs template supports multiple languages out of the box. The default theme only shows `en` documentation, but you can enable multi-language support features by adding a second language to your project. - -To add a new language to your project, you'll want to extend the current `src/pages/[lang]/...` layout: - -```diff - 📂 src/pages - ┣ 📂 en - ┃ ┣ 📜 page-1.md - ┃ ┣ 📜 page-2.md - ┃ ┣ 📜 page-3.astro -+ ┣ 📂 es -+ ┃ ┣ 📜 page-1.md -+ ┃ ┣ 📜 page-2.md -+ ┃ ┣ 📜 page-3.astro -``` - -You'll also need to add the new language name to the `KNOWN_LANGUAGES` map in your `src/config.ts` file. This will enable your new language switcher in the site header. - -```diff -// src/config.ts -export const KNOWN_LANGUAGES = { - English: 'en', -+ Spanish: 'es', -}; -``` - -Last step: you'll need to add a new entry to your sidebar, to create the table of contents for that language. While duplicating every page might not sound ideal to everyone, this extra control allows you to create entirely custom content for every language. - -> Make sure the sidebar `link` value points to the correct language! - -```diff -// src/config.ts -export const SIDEBAR = { - en: [ - { text: 'Section Header', header: true, }, - { text: 'Introduction', link: 'en/introduction' }, - // ... - ], -+ es: [ -+ { text: 'Encabezado de sección', header: true, }, -+ { text: 'Introducción', link: 'es/introduction' }, -+ // ... -+ ], -}; - -// ... -``` - -If you plan to use Spanish as the default language, you just need to modify the redirect path in `src/pages/index.astro`: - -```diff - -``` - -You can also remove the above script and write a landing page in Spanish instead. - -### What if I don't plan to support multiple languages? - -That's totally fine! Not all projects need (or can support) multiple languages. You can continue to use this theme without ever adding a second language. - -If that single language is not English, you can just replace `en` in directory layouts and configurations with the preferred language. - -### Search (Powered by Algolia) - -[Algolia](https://www.algolia.com/) offers a free service to qualified open source projects called [DocSearch](https://docsearch.algolia.com/). If you are accepted to the DocSearch program, provide your API Key & index name in `src/config.ts` and a search box will automatically appear in your site header. - -Note that Aglolia and Astro are not affiliated. We have no say over acceptance to the DocSearch program. - -If you'd prefer to remove Algolia's search and replace it with your own, check out the `src/components/Header.astro` component to see where the component is added. +more setup info, see [project setup](../CONTRIBUTING.md#project-setup) \ No newline at end of file From f3e6f868577a0f3e3ed50c97c9022a5972ac17e3 Mon Sep 17 00:00:00 2001 From: yaxu Date: Sun, 11 Jan 2026 11:55:40 +0100 Subject: [PATCH 190/194] Update website/src/pages/learn/faq.mdx Trimmed all the DAW talk a little! --- website/src/pages/learn/faq.mdx | 61 +++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/website/src/pages/learn/faq.mdx b/website/src/pages/learn/faq.mdx index 7fbc0d57..f8ebcb0f 100644 --- a/website/src/pages/learn/faq.mdx +++ b/website/src/pages/learn/faq.mdx @@ -46,21 +46,34 @@ There are multiple ways to load your sample collection. Some methods are good fo ## Can I use Strudel with AI/LLM tools? -You are free to do what you like with Strudel, within the terms of the free/open source AGPLv3 license. However as a community we are interested in exploring human creativity. AI is _way_ over-hyped right now, including by people with very shady motives. Many in the community are very wary of people training models on their tunes that they've poured their love into. So please keep discussion and questions around AI and LLMs to channels dedicated to the topic and be fully respectful of other people's work. +You are free to do what you like with Strudel, within the terms of the free/open source AGPLv3 license. +However as a community we are interested in exploring human creativity. AI is _way_ over-hyped right now, +including by people with very shady motives. Many in the community are very wary of people training models +on their tunes that they've poured their love into. So please keep discussion and questions around AI and +LLMs to channels dedicated to the topic and be fully respectful of other people's work. -Furthermore, tools like ChatGPT generally give wrong answers. Please don't ask the community to fix those answers for you, as generally they will be timewasting nonsense. +Furthermore, tools like ChatGPT generally give wrong answers. Please don't ask the community to fix those +answers for you, as generally they will be timewasting nonsense. -Human questions only! +Human questions are always welcome! + +## Where can I download loads of patterns to train my LLM? + +You cannot, as there is no such place. For details regarding our stance towards AI/LLM, see [above](/learn/faq/#can-i-use-strudel-with-aillm-tools) ## How to run offline? -Strudel works offline just fine! There are multiple techniques to run it yourself, see [this explanation](learn/pwa/#using-strudel-offline). +Strudel works offline just fine! There are multiple techniques for this, see [this explanation](learn/pwa/#using-strudel-offline). ## How to change tempo? How do I translate BPM to cpm? -If you have your tempo in beats per minute and use 4 beats per cycle (e.g. if your track is in 4/4ths) then you can do `setcpm(BPM/4)` where BPM is your beats per minute. +Strudel works in cycles, rather than beats, but if you assume a certain number of beats per cycle, you can convert between them. -If you have a different number of beats per bar or are using more or less beats per cycle (e.g. If you want to put only half a bar or two bars into one cycle), adjust accordingly. +For example, if you have your tempo in beats per minute and use 4 beats per cycle (e.g. if your track is in 4/4ths) then you can do `setcpm(BPM/4)` +where BPM is your beats per minute. + +If you have a different number of beats per bar or are using more or less beats per cycle (e.g. If you want to put only half a bar or +two bars into one cycle), adjust accordingly. ## Where can I see all the functions? @@ -72,11 +85,17 @@ If you pop open the sidetab of strudel.cc (small white < on the right hand side) ## How do I use this exactly like a DAW? -Short answer: you don't. +Strudel has different design aims for a DAW, and so treating it like one will likely be frustrating. DAWs are geared towards +sequencing notes over time in predictable ways, whereas Strudel and similar Uzu languages are geared towards combining and +transforming patterns in ways that can be hard to predict. -Long answer: you can use Strudel to work along your creative work in a DAW. There are many ways to do so. +If you want to emulate the functionality of a DAW in Strudel, you'll have to identify the operations +executed by the DAW (sequencing, repeating, applying filters and envelopes) and write code that is equivalent to these +operations. For example in Strudel, the 'arrange' and 'pick' methods are useful for sequencing patterns over time (see question on these later in this document). -If you want to emulate the functionality of a DAW in a live coding language, you'll have to identify the operations executed by the DAW (sequencing, repeating, applying filters and envelopes) and write code that is equivalent to these operations. You might then find that the typical DAW workflow is not really adapted to live coding (because, despite both being ways of making music on the computer, they are two very different tools) and adapt your way of proceeding to the medium of code. This might mean leaving more place to serendipity and writing code that you don't predict the output of. +You might still find that the typical DAW workflow is not really adapted to live coding because, despite +both being ways of making music on the computer, they are two very different tools. You could then adapt your way of proceeding +to the medium of code, which might mean leaving more place to serendipity and writing code that you don't predict the output of. ## Why doesn't everyone just use a DAW? @@ -84,39 +103,29 @@ There is no easy answer to this question. Here are some thoughts: - Live coding tools such as Strudel are excellent for improvising music and visuals using a computer. DAWs are valuable and robust companions for other activities such as producing, mastering and mixing audio, among other usages. Using a tool does not exclude from using any another tool, just build a toolbox. -- Live coding has been practiced for quite some time as a performative activity. Artists like to show their screens while playing in front of an audience. It is an essential part of what they do, of the way they share their activity with everybody. +- Live coding has developed over decades as a distinct creative practice. For example, live coding artists like to show their screens while playing in front of an audience. It is an essential part of what they do, of the way they share their activity with everybody. -- Code is a human language, it is made for other humans to read it. You can read the code and enjoy the music too. It has meaning, value, and there might even be something poetic/important about it! - Strudel is free and open source, you can inspect the code, reshape it, contribute to it if you can/want. It is not opaque and this matters for many people. There is no black box, no obscure abstractions, no business model or hidden features. We need open tools in the arts! - Live coders don't shy away from using DAWs. They use them all of the time, especially when it makes their life easier for... live coding! +- Code is a human language, it is made for other humans to read it. You can read the code and enjoy the music too. It has meaning, value, and there might even be something poetic/important about it! - Strudel is free and open source, you can inspect the code, reshape it, contribute to it if you can/want. It is not opaque and this matters for many people. There is no black box, no obscure abstractions, no business model, no user tracking or hidden features. We need open tools in the arts! - Live coders don't all shy away from using DAWs. Many use them all of the time, especially when it makes their life easier for... live coding! - Code is an artistic material like any other. There is something valuable in the process of making music through code. More generally speaking, it is nice to tackle creative problems through the use of a programming language: creative thinking, building up your own solutions, DIY approach to music-making, unexpected outcome of algorithms, funny human errors, etc. - There are pianos and trumpets in your DAW: why do people continue playing the piano or the trumpet? Think of live coding tools as instruments that you activate through the act of programming. -## Is it more efficient to use Strudel than a DAW? - -Strudel was not build to be a DAW, yet it can still be used to make covers, arrange tracks, or prepare patterns for jamming. When playing concerts or jamming, some livecoders prepare their code, some perform from scratch. - -It might be interesting for you to check out for yourself how strudel can be used to express yourself creatively. Also you are free to combine a language like Strudel with a DAW. - ## How can I interface Strudel with my favorite music software? What can I do with it? Strudel can send [MIDI and OSC](/learn/input-output/), which are protocols for communicating musical information. -Another music software (or hardware!) can then listen to these messages and process them according to its capabilities. +Other music software (or hardware!) can then listen to these messages and process them according to its capabilities. -A simple example would be to send livecoded audio to Ableton on different tracks and then use it to mix them. +A simple example would be to send livecoded audio to a DAW like Ardour on different tracks and then use it to mix them. You could also send the MIDI of a sequenced pattern to Musescore and then have it transcribe your livecoded work as a musical score. -You could also send MIDI to your hardware synths because you think they sound better than the software synths built-in Strudel. +You could also send MIDI to your hardware synths, if you like their sound. -## How do I use this in my closed source webgame? +## How do I use this in my closed source webgame or other software? -You don't. You would need to re-license your game to AGPLv3 to fulfill the license Strudel is distributed under. - -## Where can I download loads of patterns to train my LLM? - -You cannot, as there is no such place. For details regarding our stance towards AI/LLM, see [above](/learn/faq/#can-i-use-strudel-with-aillm-tools) +You don't. You need to license your game to a free/open source license fulfill the [AGPLv3 license](https://codeberg.org/uzu/strudel/src/branch/main/LICENSE) Strudel is distributed under. ## How to play different patterns simultaneously? From 85fd567fb2e2ac40e1aac886af103dc83744201c Mon Sep 17 00:00:00 2001 From: yaxu Date: Sun, 11 Jan 2026 12:21:05 +0100 Subject: [PATCH 191/194] Format and remove link to open document --- website/src/pages/learn/faq.mdx | 46 ++++++++++++++------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/website/src/pages/learn/faq.mdx b/website/src/pages/learn/faq.mdx index f8ebcb0f..e43839dc 100644 --- a/website/src/pages/learn/faq.mdx +++ b/website/src/pages/learn/faq.mdx @@ -16,18 +16,18 @@ Yes - there is no charge, this is a collective open source project, and the musi While there is no charge there are some caveats, e.g.: -* the source code must stay free, i.e. you cannot distribute strudel or tidal as part of projects with incompatible licenses - see the [license](https://www.gnu.org/licenses/agpl-3.0.en.html) for details. -* the contributed examples and tracks are also separately licensed, and must not e.g. be used to train AI models without permission. +- the source code must stay free, i.e. you cannot distribute strudel or tidal as part of projects with incompatible licenses - see the [license](https://www.gnu.org/licenses/agpl-3.0.en.html) for details. +- the contributed examples and tracks are also separately licensed, and must not e.g. be used to train AI models without permission. ## How to record or export audio? Strudel is not a digital audio workstation and does not operate following the same principles shared by most traditional audio softwares. However, there are multiple ways to record the audio -- and video -- output of Strudel: -* Use the 'export' tab to render and download as an audio file. -* capture the raw stereo signal coming out of your web browser. You will need an external audio editor/DAW such as Reaper/Audacity/Ardour, etc. -* use the alternative SuperDirt audio engine. Read [this page](/learn/input-output/#oscsuperdirtstrudeldirt) to know more about it. -* capture the audio/video stream using a capture tool such as [OBS](), which is designed for live streaming, but also works very well for recording. -* don't record anything and code it again in front of your friends. +- Use the 'export' tab to render and download as an audio file. +- capture the raw stereo signal coming out of your web browser. You will need an external audio editor/DAW such as Reaper/Audacity/Ardour, etc. +- use the alternative SuperDirt audio engine. Read [this page](/learn/input-output/#oscsuperdirtstrudeldirt) to know more about it. +- capture the audio/video stream using a capture tool such as [OBS](https://obsproject.com/fr), which is designed for live streaming, but also works very well for recording. +- don't record anything and code it again in front of your friends. ## Can I use strudel with my IDE? @@ -40,19 +40,19 @@ Yes you can. There are experimental modes, made by community members, for severa There are multiple ways to load your sample collection. Some methods are good for quick experimentation, some others are good to share your audio collection with other musicians: -- Import folders [from the interface](/learn/samples/#from-disk-via-import-sounds-folder). These are stored locally in your web browser, and not uploaded. +- Import folders [from the interface](/learn/samples/#from-disk-via-import-sounds-folder). These are stored locally in your web browser, and not uploaded. - Serve a folder of samples locally using the [strudel 'sampler' commandline tool](https://strudel.cc/learn/samples/#from-disk-via-strudelsampler). This can be most reliable method, but requires [nodejs](https://nodejs.org) to be installed. - Host your sound library online on the web and [load them from an URL](/learn/samples/#loading-custom-samples) ## Can I use Strudel with AI/LLM tools? -You are free to do what you like with Strudel, within the terms of the free/open source AGPLv3 license. -However as a community we are interested in exploring human creativity. AI is _way_ over-hyped right now, -including by people with very shady motives. Many in the community are very wary of people training models -on their tunes that they've poured their love into. So please keep discussion and questions around AI and +You are free to do what you like with Strudel, within the terms of the free/open source AGPLv3 license. +However as a community we are interested in exploring human creativity. AI is _way_ over-hyped right now, +including by people with very shady motives. Many in the community are very wary of people training models +on their tunes that they've poured their love into. So please keep discussion and questions around AI and LLMs to channels dedicated to the topic and be fully respectful of other people's work. -Furthermore, tools like ChatGPT generally give wrong answers. Please don't ask the community to fix those +Furthermore, tools like ChatGPT generally give wrong answers. Please don't ask the community to fix those answers for you, as generally they will be timewasting nonsense. Human questions are always welcome! @@ -69,7 +69,7 @@ Strudel works offline just fine! There are multiple techniques for this, see [th Strudel works in cycles, rather than beats, but if you assume a certain number of beats per cycle, you can convert between them. -For example, if you have your tempo in beats per minute and use 4 beats per cycle (e.g. if your track is in 4/4ths) then you can do `setcpm(BPM/4)` +For example, if you have your tempo in beats per minute and use 4 beats per cycle (e.g. if your track is in 4/4ths) then you can do `setcpm(BPM/4)` where BPM is your beats per minute. If you have a different number of beats per bar or are using more or less beats per cycle (e.g. If you want to put only half a bar or @@ -85,16 +85,16 @@ If you pop open the sidetab of strudel.cc (small white < on the right hand side) ## How do I use this exactly like a DAW? -Strudel has different design aims for a DAW, and so treating it like one will likely be frustrating. DAWs are geared towards -sequencing notes over time in predictable ways, whereas Strudel and similar Uzu languages are geared towards combining and +Strudel has different design aims for a DAW, and so treating it like one will likely be frustrating. DAWs are geared towards +sequencing notes over time in predictable ways, whereas Strudel and similar Uzu languages are geared towards combining and transforming patterns in ways that can be hard to predict. -If you want to emulate the functionality of a DAW in Strudel, you'll have to identify the operations -executed by the DAW (sequencing, repeating, applying filters and envelopes) and write code that is equivalent to these +If you want to emulate the functionality of a DAW in Strudel, you'll have to identify the operations +executed by the DAW (sequencing, repeating, applying filters and envelopes) and write code that is equivalent to these operations. For example in Strudel, the 'arrange' and 'pick' methods are useful for sequencing patterns over time (see question on these later in this document). -You might still find that the typical DAW workflow is not really adapted to live coding because, despite -both being ways of making music on the computer, they are two very different tools. You could then adapt your way of proceeding +You might still find that the typical DAW workflow is not really adapted to live coding because, despite +both being ways of making music on the computer, they are two very different tools. You could then adapt your way of proceeding to the medium of code, which might mean leaving more place to serendipity and writing code that you don't predict the output of. ## Why doesn't everyone just use a DAW? @@ -329,9 +329,3 @@ s increase by one semitone, i.e. sharp, works for steps of scales, note names $: at the start of a line, defines a member of the stack. is the only stack name that should occur multiple names _ before a stack name: mutes the stack, i.e. hush(), for example _$: s("bd"), see above for a different usage. ``` - -## Are there more FAQ items? - -These pages have been taken from [this pad](https://doc.patternclub.org/_CgofWouTciXXHexUP9AVg?both). Some of the items there have not been brushed up and brought here. - -These include the following items: 9, 11, 12 and 19 From a659c89385b84e08c001c47669fb98253ffb55f9 Mon Sep 17 00:00:00 2001 From: floy Date: Sun, 11 Jan 2026 13:25:44 +0100 Subject: [PATCH 192/194] fix code formatting --- website/src/repl/components/panel/SoundsTab.jsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index 2060c64a..5a7df5ba 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -210,10 +210,11 @@ export function SoundsTab() { ) : ( '' )} - {!soundEntries.length && soundsFilter !== 'importSounds' ? - search == '' ? 'No sounds loaded' : 'No sounds found' - : '' - } + {!soundEntries.length && soundsFilter !== 'importSounds' + ? search == '' + ? 'No sounds loaded' + : 'No sounds found' + : ''}
); From 8d2bca59c4373a74d6f05ecb9432b5384135109c Mon Sep 17 00:00:00 2001 From: JesCoding Date: Wed, 24 Dec 2025 11:39:28 +0100 Subject: [PATCH 193/194] Fix sounds example to work in the REPL `freq` only takes one argument, so switch this to mini-notation so this can be used in the REPL as indicated in the paragraph below. Make it so this can be played by the user. Fix syntax-type typo. --- website/src/pages/technical-manual/sounds.mdx | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/website/src/pages/technical-manual/sounds.mdx b/website/src/pages/technical-manual/sounds.mdx index 11078713..73ae0579 100644 --- a/website/src/pages/technical-manual/sounds.mdx +++ b/website/src/pages/technical-manual/sounds.mdx @@ -13,7 +13,7 @@ Let's take a closer look about how sounds are implemented in the webaudio output All sounds are registered in the sound map, using the the `registerSound` function: -```ts +```js function registerSound( name: string, // The name of the sound that should be given to `s`, e.g. `mysaw` // The function called by the scheduler to trigger the sound: @@ -35,34 +35,36 @@ When `registerSound` is called, it registers `{ onTrigger, data }` under the giv This might be a bit abstract, so here is a minimal example: -```js -registerSound( - 'mysaw', - (time, value, onended) => { - let { freq } = value; // destructure control params - const ctx = getAudioContext(); - // create oscillator - const o = new OscillatorNode(ctx, { type: 'sawtooth', frequency: Number(freq) }); - o.start(time); - // add gain node to level down osc - const g = new GainNode(ctx, { gain: 0.3 }); - // connect osc to gain - const node = o.connect(g); - // this function can be called from outside to stop the sound - const stop = (time) => o.stop(time); - // ended will be fired when stop has been fired - o.addEventListener('ended', () => { - o.disconnect(); - g.disconnect(); - onended(); - }); - return { node, stop }; - }, - { type: 'synth' }, -); -// use the sound -freq(220, 440, 330).s('mysaw'); -``` + { + let { freq } = value; // destructure control params + const ctx = getAudioContext(); + // create oscillator + const o = new OscillatorNode(ctx, { type: 'sawtooth', frequency: Number(freq) }); + o.start(time); + // add gain node to level down osc + const g = new GainNode(ctx, { gain: 0.3 }); + // connect osc to gain + const node = o.connect(g); + // this function can be called from outside to stop the sound + const stop = (time) => o.stop(time); + // ended will be fired when stop has been fired + o.addEventListener('ended', () => { + o.disconnect(); + g.disconnect(); + onended(); + }); + return { node, stop }; + }, + { type: 'synth' }, + ); + // use the sound + freq("220 440 330").s('mysaw');`} +/> You can actually use this code in the [REPL](https://strudel.cc/) and it'll work. After evaluating the code, you should see `mysaw` in listed in the sounds tab. From 753dc48b69ef800d21efe4541a6126b30e9f292e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 11 Jan 2026 19:12:45 +0100 Subject: [PATCH 194/194] update changelog --- CHANGELOG.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a13e379d..19a502f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,62 @@ NOTE: you can generate this with `node warm.js`. it might still not be perfectly sorted... +## january 2026 + +- 2026-01-11T19:00:25+01:00 Fix sounds example to work in the REPL by JesCoding in: [#1851](https://codeberg.org/uzu/strudel/pulls/1851) +- 2026-01-11T13:52:02+01:00 Improve hint text when sound search has no results by floy in: [#1883](https://codeberg.org/uzu/strudel/pulls/1883) +- 2026-01-11T12:45:07+01:00 fix: remove faulty default readme by froos in: [#1889](https://codeberg.org/uzu/strudel/pulls/1889) +- 2026-01-11T12:15:34+01:00 fix/self-hosted-config by alienmind in: [#1880](https://codeberg.org/uzu/strudel/pulls/1880) +- 2026-01-11T12:07:48+01:00 Feat: Support External AudioContext Injection by 1d10t in: [#1833](https://codeberg.org/uzu/strudel/pulls/1833) +- 2026-01-11T11:37:37+01:00 fix: add trem to top level by froos in: [#1887](https://codeberg.org/uzu/strudel/pulls/1887) +- 2026-01-11T11:37:18+01:00 fix: export start cycle min 0 by froos in: [#1888](https://codeberg.org/uzu/strudel/pulls/1888) +- 2026-01-11T11:05:44+01:00 fix: repl package init audio properly by froos in: [#1836](https://codeberg.org/uzu/strudel/pulls/1836) +- 2026-01-11T06:51:24+01:00 Fix: show reload dialog when uploading prebake script by daslyfe in: [#1886](https://codeberg.org/uzu/strudel/pulls/1886) +- 2026-01-11T06:01:42+01:00 fixes Serial onTrigger() params #1633 by gueejla in: [#1885](https://codeberg.org/uzu/strudel/pulls/1885) +- 2026-01-10T20:52:40+01:00 Allow top level distortions for the purpose of FX by glossing in: [#1884](https://codeberg.org/uzu/strudel/pulls/1884) +- 2026-01-10T23:12:48+01:00 Perf: Targeted node pools by glossing in: [#1810](https://codeberg.org/uzu/strudel/pulls/1810) +- 2026-01-09T03:43:37+01:00 Bake in scaling by `freq` for FM with a gain node by glossing in: [#1878](https://codeberg.org/uzu/strudel/pulls/1878) +- 2026-01-09T02:53:38+01:00 Bug fix: Properly handle subcontrols by glossing in: [#1877](https://codeberg.org/uzu/strudel/pulls/1877) +- 2026-01-07T20:06:18+01:00 Feat: Add ability to turn mini parsing off with mini-off decorator by glossing in: [#1786](https://codeberg.org/uzu/strudel/pulls/1786) +- 2026-01-07T19:22:24+01:00 Bug Fix: Update loopStart/End to not be offset by glossing in: [#1826](https://codeberg.org/uzu/strudel/pulls/1826) +- 2026-01-06T06:35:49+01:00 Update modulator docstrings and allow ids to be patterns by glossing in: [#1874](https://codeberg.org/uzu/strudel/pulls/1874) +- 2026-01-04T17:14:11+01:00 Fix doc link in @strudel/osc README.md by forrcaho in: [#1872](https://codeberg.org/uzu/strudel/pulls/1872) +- 2026-01-04T02:01:07+01:00 Make stretch modulatable by glossing in: [#1870](https://codeberg.org/uzu/strudel/pulls/1870) +- 2026-01-02T18:44:53+01:00 Make pan modulatable by glossing in: [#1865](https://codeberg.org/uzu/strudel/pulls/1865) +- 2026-01-01T21:42:23+01:00 Fix transpilation example to have same mini-notation by JesCoding in: [#1850](https://codeberg.org/uzu/strudel/pulls/1850) +- 2026-01-01T21:39:05+01:00 fix: missing punctuation by eddyflux in: [#1860](https://codeberg.org/uzu/strudel/pulls/1860) +- 2026-01-01T20:20:34+01:00 Fix formatting of docstring by glossing in: [#1864](https://codeberg.org/uzu/strudel/pulls/1864) +- 2026-01-01T19:52:15+01:00 Feat: FX Chains by glossing in: [#1861](https://codeberg.org/uzu/strudel/pulls/1861) + +## december 2025 + +- 2025-12-29T21:59:11+01:00 Bugfix: Fix modulator clamping when min/max not specified by glossing in: [#1859](https://codeberg.org/uzu/strudel/pulls/1859) +- 2025-12-29T16:07:18+01:00 Fix AudioContext change detection. Use AudioNode.context by jeromew in: [#1858](https://codeberg.org/uzu/strudel/pulls/1858) +- 2025-12-28T14:20:03+01:00 dough repl fixes by froos in: [#1855](https://codeberg.org/uzu/strudel/pulls/1855) +- 2025-12-19T08:11:50+01:00 fix: visual block selection mode for vim bindings by Dsm0 in: [#1839](https://codeberg.org/uzu/strudel/pulls/1839) +- 2025-12-28T22:58:38+01:00 Say that @license should use SPDX identifier by Wuzzy in: [#1817](https://codeberg.org/uzu/strudel/pulls/1817) +- 2025-12-28T22:56:07+01:00 Expose Vim object in order to create custom keybindings by JohnBjrk in: [#1816](https://codeberg.org/uzu/strudel/pulls/1816) +- 2025-12-20T22:27:52+01:00 Document "-" in mini-notation by Wuzzy in: [#1818](https://codeberg.org/uzu/strudel/pulls/1818) +- 2025-12-20T22:20:27+01:00 simplify envValAtTime and remove asymmetric behavior (fix #1653) by pulu in: [#1815](https://codeberg.org/uzu/strudel/pulls/1815) +- 2025-12-14T15:09:12+01:00 delta -> per / perx / cyclesPer refinements by yaxu in: [#1832](https://codeberg.org/uzu/strudel/pulls/1832) +- 2025-12-14T19:33:37+01:00 Fix: wrong warning in build environments by jeromew in: [#1835](https://codeberg.org/uzu/strudel/pulls/1835) +- 2025-12-12T10:28:27+01:00 Add delta signal for representing the duration of events in patterns that are combined with it by yaxu in: [#1831](https://codeberg.org/uzu/strudel/pulls/1831) +- 2025-12-11T16:37:58+01:00 Updates relating to LLM, github, etc by yaxu in: [#1830](https://codeberg.org/uzu/strudel/pulls/1830) +- 2025-12-10T15:32:44+01:00 fix .as so it doesn't set undefined values by yaxu in: [#1827](https://codeberg.org/uzu/strudel/pulls/1827) +- 2025-12-08T19:27:31+01:00 [perf] propagate `onceEnded` and `releaseAudioNode` by jeromew in: [#1809](https://codeberg.org/uzu/strudel/pulls/1809) +- 2025-12-07T19:15:43+01:00 Add vel as a synonym for velocity & update a few docstrings by glossing in: [#1781](https://codeberg.org/uzu/strudel/pulls/1781) +- 2025-12-07T18:53:04+01:00 [perf] fix phaser leak of unused biquads by jeromew in: [#1800](https://codeberg.org/uzu/strudel/pulls/1800) +- 2025-12-07T18:42:50+01:00 Bug fix: Remove failing tests due to shabda removal by glossing in: [#1820](https://codeberg.org/uzu/strudel/pulls/1820) +- 2025-12-03T17:35:05+01:00 [perf] release unused AudioBufferSourceNode + releaseAudioNode by jeromew in: [#1805](https://codeberg.org/uzu/strudel/pulls/1805) +- **2025-12-01 strudel.cc deployed** + ## november 2025 +- 2025-11-29T01:00:42+01:00 added export to getSuperdoughAudioController() so that its possible to route superdough audio through other webaudio applications. by ndr0n in: [#1796](https://codeberg.org/uzu/strudel/pulls/1796) +- 2025-11-28T23:26:20+01:00 add revv() for reversing whole patterns by yaxu in: [#1791](https://codeberg.org/uzu/strudel/pulls/1791) +- 2025-11-28T20:19:16+01:00 [perf] Disconnect lfos for phaser and filters by glossing in: [#1787](https://codeberg.org/uzu/strudel/pulls/1787) +- 2025-11-27T23:08:52+01:00 fix: return silence when no pattern is returned by froos in: [#1795](https://codeberg.org/uzu/strudel/pulls/1795) +- 2025-11-27T22:38:05+01:00 add CHANGELOG.md + basic script to generate by froos in: [#1794](https://codeberg.org/uzu/strudel/pulls/1794) - 2025-11-27T22:03:53+01:00 [perf] in `noise`, let noiseMix do the disconnect when it exists by jeromew in: [#1783](https://codeberg.org/uzu/strudel/pulls/1783) - 2025-11-27T22:01:01+01:00 Bug Fix: Retries for sounds tab by glossing in: [#1754](https://codeberg.org/uzu/strudel/pulls/1754) - 2025-11-27T20:36:46+01:00 [hydra] return the hydra object when await initHydra(..) is called by jeromew in: [#1784](https://codeberg.org/uzu/strudel/pulls/1784) @@ -52,6 +106,7 @@ NOTE: you can generate this with `node warm.js`. it might still not be perfectly - 2025-10-28T22:51:16+01:00 Repurpose vim shortcuts for usability by dtricks in: [#1624](https://codeberg.org/uzu/strudel/pulls/1624) - 2025-10-28T22:35:21+01:00 Add support for euclidian in mondo with `bd&3:8` by TristanCacqueray in: [#1630](https://codeberg.org/uzu/strudel/pulls/1630) - 2025-10-27T10:42:07+01:00 Use bunny cdn for all samples and json files by yaxu in: [#1701](https://codeberg.org/uzu/strudel/pulls/1701) +- **2025-10-27 @strudel/core@1.2.5** - 2025-10-26T22:52:54+01:00 degithub - switch some samples to bunnycdn by yaxu in: [#1697](https://codeberg.org/uzu/strudel/pulls/1697) - 2025-10-26T17:09:22+01:00 Fix ZZFX example by moumar in: [#1685](https://codeberg.org/uzu/strudel/pulls/1685) - 2025-10-23T15:56:04+02:00 Make osc port and host configurable. Changes dependencies. by yaxu in: [#1682](https://codeberg.org/uzu/strudel/pulls/1682)