From 058d950121d8ae89656cf21dfd9568a0275585f9 Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 15 Aug 2025 22:57:59 -0500 Subject: [PATCH 001/130] 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/130] 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/130] 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/130] 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/130] 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/130] 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/130] 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/130] 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/130] 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/130] 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/130] 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/130] 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/130] 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/130] 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/130] 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/130] 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/130] 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 0fbde61b2dd4bb2f2c3bdf2b451d05bfa73264e4 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 19 Oct 2025 14:35:23 +0300 Subject: [PATCH 018/130] 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 025/130] 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 026/130] 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 027/130] 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 028/130] 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 029/130] 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 030/130] 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 031/130] 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 032/130] 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 033/130] 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 034/130] 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 035/130] 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 036/130] 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 037/130] 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 038/130] 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 039/130] 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 040/130] 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 041/130] 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 042/130] 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 043/130] 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 044/130] 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 045/130] 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 046/130] 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 047/130] 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 048/130] 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 049/130] 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 050/130] 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 051/130] 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 052/130] 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 053/130] 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 054/130] 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 055/130] 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 29b6729246fa45eab19e8ee0964cb4a50f002eae Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 19 Nov 2025 15:03:11 -0600 Subject: [PATCH 056/130] 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 b73f62405820993ad0be28291066023279580fad Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 19 Nov 2025 15:41:29 -0600 Subject: [PATCH 057/130] 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 058/130] 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 442f4879738b7f7cb3f3f1baef8a0a5fac7cfb19 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 19 Nov 2025 17:10:05 -0600 Subject: [PATCH 059/130] 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 060/130] 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 061/130] 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 062/130] 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 063/130] 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 064/130] 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 065/130] 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 066/130] 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 fc74dcdb1a56e7f2122e87210be53e6a60d1ea74 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 24 Oct 2025 19:21:34 +0300 Subject: [PATCH 067/130] 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 068/130] 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 069/130] 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 070/130] 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 071/130] 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 072/130] 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 073/130] [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 074/130] 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 71d1fe99e499d07f5d6a4d84bea5ea2e69b25908 Mon Sep 17 00:00:00 2001 From: jeromew Date: Tue, 2 Dec 2025 13:51:16 +0000 Subject: [PATCH 075/130] 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 076/130] 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 077/130] 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 078/130] 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 ad43259353a962fb48ec2374a5363715a77e816f Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 5 Dec 2025 13:39:52 -0600 Subject: [PATCH 079/130] 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 173e359a653895d3728aac34fa839f6d89a16dbb Mon Sep 17 00:00:00 2001 From: miranda Date: Sat, 6 Dec 2025 17:46:16 +0200 Subject: [PATCH 080/130] 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 82ca77be9d74c5cae55fb734f3262bbe9f01c1f6 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 6 Dec 2025 12:02:11 -0600 Subject: [PATCH 081/130] 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 082/130] 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 2aeba2e53fa3a72b12b21b97300b5fc0a724a1a0 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 11 Dec 2025 11:37:20 -0600 Subject: [PATCH 083/130] 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 084/130] 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 085/130] 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 086/130] 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 087/130] 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 088/130] 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 8e1fba04363cdcf39c946c8fb10a21554ef42cb7 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 12 Dec 2025 15:45:36 +0000 Subject: [PATCH 089/130] 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 090/130] 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 091/130] 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 092/130] 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 8eeebf1e4f31be65508260d791aed0fbd21ccdd5 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 13 Dec 2025 16:30:51 +0000 Subject: [PATCH 093/130] 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 094/130] 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 095/130] 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 096/130] 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 097/130] 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 098/130] 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 099/130] 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 100/130] 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 101/130] 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 102/130] 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 103/130] 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 104/130] 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 105/130] 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 106/130] 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 107/130] 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 0de212c12f1ecb7a6905c9073b21907b88529245 Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Thu, 18 Dec 2025 22:55:44 -0800 Subject: [PATCH 108/130] fix visual block mode --- packages/codemirror/keybindings.mjs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs index 81dc87a5..d92e9c95 100644 --- a/packages/codemirror/keybindings.mjs +++ b/packages/codemirror/keybindings.mjs @@ -1,5 +1,5 @@ import { defaultKeymap } from '@codemirror/commands'; -import { Prec } from '@codemirror/state'; +import { Prec, EditorState } from '@codemirror/state'; import { keymap, ViewPlugin } from '@codemirror/view'; // import { searchKeymap } from '@codemirror/search'; import { emacs } from '@replit/codemirror-emacs'; @@ -133,5 +133,9 @@ const keymaps = { export function keybindings(name) { const active = keymaps[name]; - return [active ? Prec.high(active()) : []]; + const extensions = active ? [Prec.high(active())] : []; + if (name === 'vim') { + extensions.push(EditorState.allowMultipleSelections.of(true)); + } + return extensions; } From a9752fb2a45104f7101c1b9e3dd653a3dd5fab8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Bj=C3=B6rk?= Date: Sat, 6 Dec 2025 19:54:33 +0100 Subject: [PATCH 109/130] Exposing Vim from codemirror-vim Docs for adding keybindings Only expose Vim from keybindings --- packages/codemirror/index.mjs | 1 + packages/codemirror/keybindings.mjs | 2 ++ website/src/pages/technical-manual/vim.mdx | 21 +++++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/packages/codemirror/index.mjs b/packages/codemirror/index.mjs index 3a5f2a23..2d5b3de2 100644 --- a/packages/codemirror/index.mjs +++ b/packages/codemirror/index.mjs @@ -4,3 +4,4 @@ export * from './flash.mjs'; export * from './slider.mjs'; export * from './themes.mjs'; export * from './widget.mjs'; +export { Vim } from './keybindings.mjs'; diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs index d92e9c95..24437b7e 100644 --- a/packages/codemirror/keybindings.mjs +++ b/packages/codemirror/keybindings.mjs @@ -131,6 +131,8 @@ const keymaps = { vscode: vscodeExtension, }; +export { Vim } from '@replit/codemirror-vim'; + export function keybindings(name) { const active = keymaps[name]; const extensions = active ? [Prec.high(active())] : []; diff --git a/website/src/pages/technical-manual/vim.mdx b/website/src/pages/technical-manual/vim.mdx index fa53aa55..6ec41d4a 100644 --- a/website/src/pages/technical-manual/vim.mdx +++ b/website/src/pages/technical-manual/vim.mdx @@ -35,3 +35,24 @@ Troubleshooting - If :w logs but evaluation doesn't apply, ensure Vim keybindings are active and try again. You can also use Ctrl+Enter as a fallback. - For :q / gc, ensure focus is inside the editor. If an error occurs, reload the page to reset editor state and try again. + +## Adding custom keybindings + +To add custom keybindings the `Vim` object can be used (either from within a pattern or in a prebake script) + +Example: + +```javascript +// Map 'jk' to Escape in normal mode +Vim.map('jk', '', 'insert'); +// Map 'U' to :w (Evaluate the current code) +Vim.map('U', ':w', 'normal'); +// Map 'Q' to :q — Stop/pause playback +Vim.map('Q', ':q', 'normal'); +// Map 'J' to find next '$' (jump to next label) +Vim.map('J', '/\\$', 'normal'); +// Map 'K' to find previous '$' (jump to previous label) +Vim.map('K', '?\\$', 'normal'); +``` + +For more information on how to use the `Vim` object see [CodeMirror Vim](https://github.com/replit/codemirror-vim) From c7a2c2bb9c37a4a334747330260048742c93d683 Mon Sep 17 00:00:00 2001 From: Wuzzy Date: Sun, 7 Dec 2025 00:02:24 +0100 Subject: [PATCH 110/130] Say that @license should use SPDX identifier --- website/src/pages/learn/metadata.mdx | 34 +++++++++++++++------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/website/src/pages/learn/metadata.mdx b/website/src/pages/learn/metadata.mdx index 61db16f0..c9425792 100644 --- a/website/src/pages/learn/metadata.mdx +++ b/website/src/pages/learn/metadata.mdx @@ -11,9 +11,9 @@ import { JsDoc } from '../../docs/JsDoc'; You can optionally add some music metadata in your Strudel code, by using tags in code comments: ```js -// @title Hey Hoo -// @by Sam Tagada -// @license CC BY-NC-SA +// @title My Cool Song +// @by John Doe +// @license CC-BY-SA-4.0 ``` Like other comments, those are ignored by Strudel, but it can be used by other tools to retrieve some information about the music. @@ -24,22 +24,22 @@ You can also use comment blocks: ```js /* -@title Hey Hoo -@by Sam Tagada -@license CC BY-NC-SA +@title My Cool Song +@by John Doe +@license CC-BY-SA-4.0 */ ``` Or define multiple tags in one line: ```js -// @title Hey Hoo @by Sam Tagada @license CC BY-NC-SA +// @title My Cool Song @by John Doe @license CC-BY-SA-4.0 ``` The `title` tag has an alternative syntax using quotes (must be defined at the very begining): ```js -// "Hey Hoo" @by Sam Tagada +// "My Cool Song" @by John Doe ``` ## Tags list @@ -48,20 +48,22 @@ Available tags are: - `@title`: music title - `@by`: music author(s), separated by comma, eventually followed with a link in `<>` (ex: `@by John Doe `) -- `@license`: music license(s), e.g. CC BY-NC-SA. Unsure? [Choose a creative commons license here](https://creativecommons.org/choose/) +- `@license`: music license(s), separated by comma. Each license should be specified by using the correct identifier in the [https://spdx.org/licenses/](SPDX License List). Example: CC-BY-SA-4.0. Unsure? [Choose a Creative Commons license here](https://creativecommons.org/choose/). - `@details`: some additional information about the music -- `@url`: web page(s) related to the music (git repo, soundcloud link, etc.) -- `@genre`: music genre(s) (pop, jazz, etc) +- `@url`: web page(s) related to the music (git repository, Soundcloud link, etc.) +- `@genre`: music genre(s) (pop, jazz, etc.) - `@album`: music album name +Note to tool authors: _Never_ trust that a song has filled those fields with syntactically correct values; make sure your software is robust enough it doesn't break if it encounters bad values + ## Multiple values Some of them accepts several values, using the comma or new line separator, or duplicating the tag: ```js /* -@by Sam Tagada - Jimmy +@by John Doe + Jane Doe @genre pop, jazz @url https://example.com @url https://example.org @@ -72,11 +74,11 @@ You can also add optional prefixes and use tags where you want: ```js /* -song @by Sam Tagada -samples @by Jimmy +song @by John Doe +samples @by Jane Doe */ ... -note("a3 c#4 e4 a4") // @by Sandy +note("a3 c#4 e4 a4") // @by Sandy Sue ``` ## Multiline From d349d2a0cd0efcce383e63ed414e3a009f872b06 Mon Sep 17 00:00:00 2001 From: JesCoding Date: Wed, 24 Dec 2025 10:38:21 +0100 Subject: [PATCH 111/130] Fix transpilation example to have same mini-notation This was introduced in https://codeberg.org/uzu/strudel/commit/66f8ca72c1b364f72f25b8cff041cac13b380432 --- website/src/pages/technical-manual/repl.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/technical-manual/repl.mdx b/website/src/pages/technical-manual/repl.mdx index f74a4fb8..d8a22cf5 100644 --- a/website/src/pages/technical-manual/repl.mdx +++ b/website/src/pages/technical-manual/repl.mdx @@ -41,7 +41,7 @@ note("c3 [e3 g3]*2") is transpiled to: ```strudel -note(m('c3 [e3 g3]', 5)) +note(m('c3 [e3 g3]*2', 5)) ``` Here, the string is wrapped in `m`, which will create a pattern from a mini-notation string. As the second parameter, it gets passed source code location of the string, which enables highlighting active events later. From e286ab2830f5681e1c92ad9f6bfd6913becbf3c5 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 28 Dec 2025 13:10:37 +0100 Subject: [PATCH 112/130] no default samples for now + add doughsamples to scope --- website/src/components/Dough/dough-repl.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/src/components/Dough/dough-repl.mjs b/website/src/components/Dough/dough-repl.mjs index ef28617e..c8b47a3e 100644 --- a/website/src/components/Dough/dough-repl.mjs +++ b/website/src/components/Dough/dough-repl.mjs @@ -6,6 +6,8 @@ import { transpiler } from '@strudel/transpiler'; //const doughBaseUrl = doughUrl.split('/').slice(0, -1).join('/') + '/'; const doughBaseUrl = 'https://unpkg.com/dough-synth@0.1.9/'; +Object.assign(globalThis, { doughsamples }); + export class DoughRepl { pattern; latency = 0.1; @@ -81,7 +83,9 @@ export class DoughRepl { this.origin = undefined; } prebake() { - return Promise.all([doughsamples('github:eddyflux/crate')]); + return Promise.all([ + // doughsamples('github:eddyflux/crate') + ]); } // tbd: move this to dough-synth get time() { From 53e49952d8942b7d78393e8626ec32a5808006a2 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 28 Dec 2025 13:10:57 +0100 Subject: [PATCH 113/130] persist code in hash --- website/src/components/Dough/dough-mirror.mjs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/website/src/components/Dough/dough-mirror.mjs b/website/src/components/Dough/dough-mirror.mjs index 5cdfeb3f..2626787e 100644 --- a/website/src/components/Dough/dough-mirror.mjs +++ b/website/src/components/Dough/dough-mirror.mjs @@ -9,12 +9,23 @@ import { updateMiniLocations, highlightMiniLocations, } from '@strudel/codemirror'; -import { evalScope } from '@strudel/core'; +import { evalScope, hash2code } 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")'; +let initialCode = '$: note("c a f e")'; +if (typeof window !== 'undefined') { + try { + const codeParam = window.location.href.split('#')[1] || ''; + if (codeParam) { + initialCode = hash2code(codeParam); + } + } catch (err) { + console.error('could not init code from url'); + } +} + export const code = persistentAtom('vanilla-repl-code', initialCode, { encode: JSON.stringify, decode: JSON.parse, @@ -64,6 +75,7 @@ export class DoughMirror { this.flash(); await this.prebaked; const { miniLocations } = await this.repl.evaluate(this.code); + window.location.hash = '#' + code2hash(this.code); updateMiniLocations(this.editor, miniLocations); } stop() { From 125eece2237d7fd361f731156a6f47d165ec2486 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 28 Dec 2025 13:20:22 +0100 Subject: [PATCH 114/130] fix: import --- website/src/components/Dough/dough-mirror.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/components/Dough/dough-mirror.mjs b/website/src/components/Dough/dough-mirror.mjs index 2626787e..b7794d2d 100644 --- a/website/src/components/Dough/dough-mirror.mjs +++ b/website/src/components/Dough/dough-mirror.mjs @@ -9,7 +9,7 @@ import { updateMiniLocations, highlightMiniLocations, } from '@strudel/codemirror'; -import { evalScope, hash2code } from '@strudel/core'; +import { evalScope, hash2code, code2hash } from '@strudel/core'; import { Framer } from '@strudel/draw'; import { persistentAtom } from '@nanostores/persistent'; import { DoughRepl } from './dough-repl.mjs'; From 300a1bbfd51dc763e0adc20ff0a6755a33fe6ff2 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 28 Dec 2025 14:07:27 +0100 Subject: [PATCH 115/130] fix: actually load code from url --- website/src/components/Dough/dough-mirror.mjs | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/website/src/components/Dough/dough-mirror.mjs b/website/src/components/Dough/dough-mirror.mjs index b7794d2d..7784402e 100644 --- a/website/src/components/Dough/dough-mirror.mjs +++ b/website/src/components/Dough/dough-mirror.mjs @@ -15,22 +15,24 @@ import { persistentAtom } from '@nanostores/persistent'; import { DoughRepl } from './dough-repl.mjs'; let initialCode = '$: note("c a f e")'; -if (typeof window !== 'undefined') { - try { - const codeParam = window.location.href.split('#')[1] || ''; - if (codeParam) { - initialCode = hash2code(codeParam); - } - } catch (err) { - console.error('could not init code from url'); - } -} export const code = persistentAtom('vanilla-repl-code', initialCode, { encode: JSON.stringify, decode: JSON.parse, }); +if (typeof window !== 'undefined') { + try { + const codeParam = window.location.href.split('#')[1] || ''; + if (codeParam) { + const codeFromHash = hash2code(codeParam); + code.set(codeFromHash); + } + } catch (err) { + console.error('could not init code from url'); + } +} + export class DoughMirror { constructor(options) { const { root, initialCode = code.get(), bgFill = true } = options; From 30fe5b47e38ec768c34186cfd348e3b891c83f4b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 28 Dec 2025 22:42:42 +0100 Subject: [PATCH 116/130] hotfix: fix scope and friends #1847 --- packages/superdough/superdough.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 4fd6164d..e98a2aa2 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -359,7 +359,9 @@ export let analysers = {}, analysersData = {}; export function getAnalyserById(id, fftSize = 1024, smoothingTimeConstant = 0.5) { - if (!analysers[id] || analysers[id].audioContext != getAudioContext()) { + // below commented out branch is hotfixing https://codeberg.org/uzu/strudel/issues/1847 + // might cause conflicts with exporting... + if (!analysers[id] /* || analysers[id].audioContext != getAudioContext() */) { // make sure this doesn't happen too often as it piles up garbage const analyserNode = getAudioContext().createAnalyser(); analyserNode.fftSize = fftSize; From 48f6a41683daf95b412ce99979b5ce933286349e Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 28 Dec 2025 20:13:15 -0500 Subject: [PATCH 117/130] Extend to FM, vibrato, etc --- packages/core/controls.mjs | 15 +++----- packages/soundfonts/fontloader.mjs | 6 +-- packages/superdough/helpers.mjs | 6 ++- packages/superdough/sampler.mjs | 6 +-- packages/superdough/superdough.mjs | 33 ++++++++-------- packages/superdough/superdoughdata.mjs | 37 ++++++++++++++++++ packages/superdough/synth.mjs | 52 +++++++++++++------------- packages/superdough/wavetable.mjs | 19 +++++++--- packages/superdough/zzfx.mjs | 2 +- 9 files changed, 112 insertions(+), 64 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 826174d8..5cc53de6 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2870,36 +2870,33 @@ registerSubControls('bmod', [ ]); Pattern.prototype.modulate = function (type, config, idx) { - if (config == null || typeof config !== 'object') { - return this; - } + config ??= { control: undefined }; 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; + let defaultValue = undefined; for (const [rawKey, value] of Object.entries(config)) { const key = getMainSubcontrolName(type, rawKey); const valuePat = reify(value); output = output .fmap((v) => (c) => { - if (!defaultSet) { + if (defaultValue === undefined) { // 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; + defaultValue = control; } v[type] ??= []; const t = v[type]; idx ??= t.length; - t[idx] ??= defaultValue; + t[idx] ??= { control: defaultValue }; + if (c === undefined) return v; if (key === 'control' || key === 'subControl') { t[idx][key] = getControlName(c); } else { diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index 81b9eb50..b4ae5982 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -166,7 +166,7 @@ export function registerSoundfonts() { let envEnd = holdEnd + release + 0.01; // vibrato - let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, time); + const vibratoHandle = getVibratoOscillator(bufferSource.detune, value, time); // pitch envelope getPitchEnvelope(bufferSource.detune, value, time, holdEnd); @@ -174,10 +174,10 @@ export function registerSoundfonts() { const stop = (releaseTime) => {}; onceEnded(bufferSource, () => { releaseAudioNode(bufferSource); - releaseAudioNode(vibratoOscillator); + vibratoHandle?.stop(); onended(); }); - return { node, stop, source: bufferSource }; + return { node, stop, nodes: { source: [bufferSource], ...vibratoHandle?.nodes } }; }, { type: 'soundfont', prebake: true, fonts }, ); diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index fc7734ea..8bd8b067 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -351,7 +351,7 @@ export function getVibratoOscillator(param, value, t) { releaseAudioNode(vibratoOscillator); }); vibratoOscillator.start(t); - return vibratoOscillator; + return { stop: (t) => vibratoOscillator.stop(t), nodes: { vib: [vibratoOscillator], vib_gain: [gain] } }; } } @@ -407,6 +407,7 @@ export function applyFM(param, value, begin) { const ac = getAudioContext(); const toStop = []; // fm oscillators we will expose `stop` for const fms = {}; + const nodes = {}; // Matrix for (let i = 1; i <= 8; i++) { for (let j = 0; j <= 8; j++) { @@ -457,11 +458,13 @@ export function applyFM(param, value, begin) { output = osc.connect(envGain); } fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup }; + nodes[`fm_${idx}`] = [osc]; } const { input, output, freq, osc, toCleanup } = fms[idx]; const g = gainNode(amt * freq); io.push(isMod ? output.connect(g) : input); cleanupOnEnd(osc, [...toCleanup, g]); + nodes[`fm_${idx}_gain`] = [g]; } if (!io[1]) { logger( @@ -474,6 +477,7 @@ export function applyFM(param, value, begin) { } } return { + nodes, stop: (t) => toStop.forEach((m) => m?.stop(t)), }; } diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 55f08156..819de3c7 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -301,7 +301,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { } // vibrato - let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t); + const vibratoHandle = getVibratoOscillator(bufferSource.detune, value, t); const time = t + nudge; bufferSource.start(time, offset); @@ -324,7 +324,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { node.connect(out); onceEnded(bufferSource, function () { releaseAudioNode(bufferSource); - releaseAudioNode(vibratoOscillator); + vibratoHandle?.stop(); releaseAudioNode(node); releaseAudioNode(out); onended(); @@ -334,7 +334,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { const stop = (endTime) => { bufferSource.stop(endTime); }; - const handle = { node: out, source: bufferSource, stop }; + const handle = { node: out, nodes: { source: [bufferSource], ...vibratoHandle?.nodes }, stop }; // cut groups if (cut !== undefined) { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index fd198c78..8bca5b30 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -532,7 +532,7 @@ function mapChannelNumbers(channels) { } export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { - const nodes = {}; + let nodes = {}; // new: t is always expected to be the absolute target onset time const ac = getAudioContext(); const audioController = getSuperdoughAudioController(); @@ -690,7 +690,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.source]; + nodes = { ...nodes, ...soundHandle.nodes }; } } else { throw new Error(`sound ${s} not found! Is it loaded?`); @@ -709,22 +709,23 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) chain.push(sourceNode); stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch })); - transient !== undefined && - chain.push( - getWorklet( - ac, - 'transient-processor', - {}, - { - processorOptions: { - attack: transient, - sustain: transsustain, - begin: t, - end: endWithRelease, - }, + if (transient !== undefined) { + const transProcessor = getWorklet( + ac, + 'transient-processor', + {}, + { + processorOptions: { + attack: transient, + sustain: transsustain, + begin: t, + end: endWithRelease, }, - ), + }, ); + chain.push(transProcessor); + nodes['transient'] = transProcessor; + } // gain stage const initialGain = gainNode(gain); diff --git a/packages/superdough/superdoughdata.mjs b/packages/superdough/superdoughdata.mjs index cab82975..2ae03101 100644 --- a/packages/superdough/superdoughdata.mjs +++ b/packages/superdough/superdoughdata.mjs @@ -23,6 +23,9 @@ const CONTROL_TARGETS = { lfo_sync: { node: 'lfo', param: 'frequency' }, lfo_depth: { node: 'lfo', param: 'depth' }, lfo_depthabs: { node: 'lfo', param: 'depth' }, + lfo_skew: { node: 'lfo', param: 'skew' }, + lfo_curve: { node: 'lfo', param: 'curve' }, + lfo_dcoffset: { node: 'lfo', param: 'dcoffset' }, env: { node: 'env', param: 'depth' }, env_attack: { node: 'env', param: 'attack' }, env_decay: { node: 'env', param: 'decay' }, @@ -107,6 +110,40 @@ const CONTROL_TARGETS = { warp: { node: 'source', param: 'warp' }, freq: { node: 'source', param: 'frequency' }, note: { node: 'source', param: 'frequency' }, + wtdc: { node: 'wt_lfo', param: 'dc' }, + wtskew: { node: 'wt_lfo', param: 'skew' }, + wtrate: { node: 'wt_lfo', param: 'frequency' }, + wtsync: { node: 'wt_lfo', param: 'frequency' }, + wtdepth: { node: 'wt_lfo', param: 'depth' }, + warpdc: { node: 'warp_lfo', param: 'dc' }, + warpskew: { node: 'warp_lfo', param: 'skew' }, + warprate: { node: 'warp_lfo', param: 'frequency' }, + warpsync: { node: 'warp_lfo', param: 'frequency' }, + warpdepth: { node: 'warp_lfo', param: 'depth' }, + fmi: { node: 'fm_1_gain', param: 'gain' }, + fmi2: { node: 'fm_2_gain', param: 'gain' }, + fmi3: { node: 'fm_3_gain', param: 'gain' }, + fmi4: { node: 'fm_4_gain', param: 'gain' }, + fmi5: { node: 'fm_5_gain', param: 'gain' }, + fmi6: { node: 'fm_6_gain', param: 'gain' }, + fmi7: { node: 'fm_7_gain', param: 'gain' }, + fmi8: { node: 'fm_8_gain', param: 'gain' }, + fmh: { node: 'fm_1', param: 'frequency' }, + fmh2: { node: 'fm_2', param: 'frequency' }, + fmh3: { node: 'fm_3', param: 'frequency' }, + fmh4: { node: 'fm_4', param: 'frequency' }, + fmh5: { node: 'fm_5', param: 'frequency' }, + fmh6: { node: 'fm_6', param: 'frequency' }, + fmh7: { node: 'fm_7', param: 'frequency' }, + fmh8: { node: 'fm_8', param: 'frequency' }, + pw: { node: 'source', param: 'pulsewidth' }, + pwrate: { node: 'pw_lfo', param: 'frequency' }, + pwsweep: { node: 'pw_lfo', param: 'depth' }, + vib: { node: 'vib_gain', param: 'gain' }, + vibmod: { node: 'vib', param: 'frequency' }, + byteBeatStartTime: { node: 'source', param: 'byteBeatStartTime' }, + spread: { node: 'source', param: 'panspread' }, + transient: { node: 'transient', param: 'attack' }, }; export function getSuperdoughControlTargets() { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 40e62f86..d2069272 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -52,17 +52,17 @@ export function registerSynthSounds() { // turn down const g = gainNode(0.3); - let sound = getOscillator(s, t, value, () => { + const sound = getOscillator(s, t, value, () => { releaseAudioNode(g); onended(); }); - let { node: o, stop, triggerRelease } = sound; + const { node: o, nodes, stop, triggerRelease } = sound; const { duration } = value; const envGain = gainNode(1); - let node = o.connect(g).connect(envGain); + const node = o.connect(g).connect(envGain); const holdEnd = t + duration; getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); const envEnd = holdEnd + release + 0.01; @@ -70,7 +70,7 @@ export function registerSynthSounds() { stop(envEnd); return { node, - source: o, + nodes, stop: (endTime) => { stop(endTime); }, @@ -140,7 +140,7 @@ export function registerSynthSounds() { return { node, - source: o, + nodes: { source: [o] }, stop: (endTime) => { o.stop(endTime); }, @@ -185,8 +185,8 @@ export function registerSynthSounds() { const gainAdjustment = 1 / Math.sqrt(voices); getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); - const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin); - const fm = applyFM(o.parameters.get('frequency'), value, begin); + const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin); + const fmHandle = applyFM(o.parameters.get('frequency'), value, begin); let envGain = gainNode(1); envGain = o.connect(envGain); @@ -197,8 +197,8 @@ export function registerSynthSounds() { () => { releaseAudioNode(o); onended(); - fm?.stop(); - vibratoOscillator?.stop(); + fmHandle?.stop(); + vibratoHandle?.stop(); }, begin, end, @@ -206,7 +206,7 @@ export function registerSynthSounds() { return { node: envGain, - source: o, + nodes: { source: [o], ...fmHandle?.nodes, ...vibratoHandle?.nodes }, stop: (time) => { timeoutNode.stop(time); }, @@ -333,25 +333,25 @@ export function registerSynthSounds() { ); getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); - const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin); - const fm = applyFM(o.parameters.get('frequency'), value, begin); + const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin); + const fmHandle = applyFM(o.parameters.get('frequency'), value, begin); let envGain = gainNode(1); envGain = o.connect(envGain); getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear'); - let lfo; + let pw_lfo; if (pwsweep != 0) { - lfo = getLfo(ac, { frequency: pwrate, depth: pwsweep, begin, end }); - lfo.connect(o.parameters.get('pulsewidth')); + pw_lfo = getLfo(ac, { frequency: pwrate, depth: pwsweep, begin, end }); + pw_lfo.connect(o.parameters.get('pulsewidth')); } let timeoutNode = webAudioTimeout( ac, () => { releaseAudioNode(o); - releaseAudioNode(lfo); + releaseAudioNode(pw_lfo); onended(); - fm?.stop(); - vibratoOscillator?.stop(); + fmHandle?.stop(); + vibratoHandle?.stop(); }, begin, end, @@ -359,7 +359,7 @@ export function registerSynthSounds() { return { node: envGain, - source: o, + nodes: { source: [o], pw_lfo: [pw_lfo], ...fmHandle?.nodes, ...vibratoHandle?.nodes }, stop: (time) => { timeoutNode.stop(time); }, @@ -394,7 +394,7 @@ export function registerSynthSounds() { return { node: envGain, - source: bus, + nodes: { source: [bus] }, stop: (time) => { timeoutNode.stop(time); }, @@ -440,7 +440,7 @@ export function registerSynthSounds() { stop(envEnd); return { node, - source: o, + nodes: { source: [o] }, stop: (endTime) => { stop(endTime); }, @@ -520,11 +520,11 @@ export function getOscillator(s, t, value, onended) { // set frequency o.frequency.value = getFrequencyFromValue(value); - let vibratoOscillator = getVibratoOscillator(o.detune, value, t); + const vibratoHandle = getVibratoOscillator(o.detune, value, t); // pitch envelope getPitchEnvelope(o.detune, value, t, t + duration); - const fmModulator = applyFM(o.frequency, value, t); + const fmHandle = applyFM(o.frequency, value, t); let noiseMix; if (noise) { @@ -541,10 +541,10 @@ export function getOscillator(s, t, value, onended) { return { node: noiseMix?.node || o, - source: o, + nodes: { source: [o], ...vibratoHandle?.nodes, ...fmHandle?.nodes }, stop: (time) => { - fmModulator.stop(time); - vibratoOscillator?.stop(time); + fmHandle.stop(time); + vibratoHandle?.stop(time); noiseMix?.stop(time); o.stop(time); }, diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index ebf951a4..b4fc401f 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -314,19 +314,28 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { dcoffset: value.warpdc ?? 0, }, ); - const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t); - const fm = applyFM(source.parameters.get('frequency'), value, t); + const vibratoHandle = getVibratoOscillator(source.parameters.get('detune'), value, t); + const fmHandle = applyFM(source.parameters.get('frequency'), value, t); const envGain = ac.createGain(); const node = source.connect(envGain); getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear'); getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd); - const handle = { node, source }; + const handle = { + node, + nodes: { + source: [source], + wt_lfo: [wtPosModulators], + warp_lfo: [wtWarpModulators], + ...fmHandle?.nodes, + ...vibratoHandle?.nodes, + }, + }; const timeoutNode = webAudioTimeout( ac, () => { releaseAudioNode(source); - releaseAudioNode(vibratoOscillator); - fm?.stop(); + vibratoHandle?.stop(); + fmHandle?.stop(); releaseAudioNode(wtPosModulators); releaseAudioNode(wtWarpModulators); onended(); diff --git a/packages/superdough/zzfx.mjs b/packages/superdough/zzfx.mjs index 708f3450..f3dc710b 100644 --- a/packages/superdough/zzfx.mjs +++ b/packages/superdough/zzfx.mjs @@ -75,7 +75,7 @@ export const getZZFX = (value, t) => { source.start(t); return { node: source, - source, + nodes: { source: [source] }, }; }; From b4496fcc24f8bd9f306fc1c9f0026f9d5f6b1d24 Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 28 Dec 2025 21:43:08 -0500 Subject: [PATCH 118/130] More control mapping tweaks --- packages/superdough/superdough.mjs | 5 ++++- packages/superdough/superdoughdata.mjs | 15 +++++++-------- packages/superdough/superdoughoutput.mjs | 1 + 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 8bca5b30..980f1814 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -950,6 +950,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) const delayNode = orbitBus.getDelay(delaytime, delayfeedback, t); nodes['delay'] = [delayNode]; const delaySend = orbitBus.sendDelay(post, delay); + nodes['delay_mix'] = [delaySend]; audioNodes.push(delaySend); } // reverb @@ -968,6 +969,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) const roomNode = orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin); nodes['room'] = [roomNode]; const reverbSend = orbitBus.sendReverb(post, room); + nodes['room_mix'] = [reverbSend]; audioNodes.push(reverbSend); } if (bus != null) { @@ -977,7 +979,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } if (djf != null) { - nodes['djf'] = orbitBus.getDjf(djf, t); + const djfNode = orbitBus.getDjf(djf, t); + nodes['djf'] = [djfNode]; } // analyser diff --git a/packages/superdough/superdoughdata.mjs b/packages/superdough/superdoughdata.mjs index 2ae03101..f9df6ff8 100644 --- a/packages/superdough/superdoughdata.mjs +++ b/packages/superdough/superdoughdata.mjs @@ -87,25 +87,24 @@ const CONTROL_TARGETS = { compressorRelease: { node: 'compressor', param: 'release' }, // PHASER - phaserrate: { node: 'phaser', param: 'rate' }, - phaserdepth: { node: 'phaser', param: 'depth' }, - phasersweep: { node: 'phaser', param: 'sweep' }, + phaserrate: { node: 'phaser_lfo', param: 'rate' }, + phasersweep: { node: 'phaser_lfo', param: 'depth' }, phasercenter: { node: 'phaser', param: 'frequency' }, + phaserdepth: { node: 'phaser', param: 'Q' }, // ORBIT EFFECTS + delay: { node: 'delay_mix', param: 'gain' }, delaytime: { node: 'delay', param: 'delayTime' }, delayfeedback: { node: 'delay', param: 'feedback' }, - delaysync: { node: 'delay', param: 'sync' }, + delaysync: { node: 'delay', param: 'delayTime' }, dry: { node: 'dry', param: 'gain' }, - room: { node: 'room', param: 'wet' }, - roomfade: { node: 'room', param: 'fade' }, - roomlp: { node: 'room', param: 'lp' }, + room: { node: 'room_mix', param: 'gain' }, djf: { node: 'djf', param: 'value' }, busgain: { node: 'bus', param: 'gain' }, // SYNTHS s: { node: 'source', param: 'frequency' }, - detune: { node: 'source', param: 'detune' }, + detune: { node: 'source', param: 'freqspread' }, wt: { node: 'source', param: 'position' }, warp: { node: 'source', param: 'warp' }, freq: { node: 'source', param: 'frequency' }, diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs index 204eb62a..d3936646 100644 --- a/packages/superdough/superdoughoutput.mjs +++ b/packages/superdough/superdoughoutput.mjs @@ -38,6 +38,7 @@ export class Orbit { } const val = this.djfNode.parameters.get('value'); val.setValueAtTime(value, t); + return this.djfNode; } getDelay(delaytime = 0, feedback = 0.5, t) { From c0f4f4715026d909cf3339e370a3298027f554a7 Mon Sep 17 00:00:00 2001 From: jeromew Date: Mon, 29 Dec 2025 14:04:40 +0000 Subject: [PATCH 119/130] Fix AudioContext change detection. Use AudioNode.context --- 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 e98a2aa2..1ce91c83 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -359,9 +359,7 @@ export let analysers = {}, analysersData = {}; export function getAnalyserById(id, fftSize = 1024, smoothingTimeConstant = 0.5) { - // below commented out branch is hotfixing https://codeberg.org/uzu/strudel/issues/1847 - // might cause conflicts with exporting... - if (!analysers[id] /* || analysers[id].audioContext != getAudioContext() */) { + if (!analysers[id] || analysers[id].context != getAudioContext()) { // make sure this doesn't happen too often as it piles up garbage const analyserNode = getAudioContext().createAnalyser(); analyserNode.fftSize = fftSize; From 98367868ac0c7c95e44e48d294467c63c1e85dcb Mon Sep 17 00:00:00 2001 From: Aria Date: Mon, 29 Dec 2025 10:22:48 -0600 Subject: [PATCH 120/130] Handle min/max when target is LFO; fix vib and tremolo --- packages/superdough/superdough.mjs | 23 +++++++++++++---------- packages/superdough/superdoughdata.mjs | 8 ++++---- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 980f1814..13a4ef68 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -420,10 +420,10 @@ function _getControlData(control) { return controlTargets[_stripIndex(control)]; } -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')) { +function _getRangeForParam(paramName, currentValue) { + // We clamp the frequency to a reasonable range unless the currentValue + // is low, which indicates this may be an LFO + if (paramName === 'frequency' && currentValue >= 30) { return { min: 20 - currentValue, max: 24000 - currentValue }; } return { min: undefined, max: undefined }; @@ -459,8 +459,9 @@ 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); + let currentValue = targetParams[0].value; + currentValue = currentValue === 0 ? 1 : currentValue; + const { min, max } = _getRangeForParam(paramName, currentValue); const depthValue = depthabs != null ? depthabs : depth * currentValue; const modParams = { ...filteredParams, @@ -480,8 +481,9 @@ 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); + let currentValue = targetParams[0].value; + currentValue = currentValue === 0 ? 1 : currentValue; + const { min, max } = _getRangeForParam(paramName, currentValue); const depthValue = depthabs != null ? depthabs : depth * currentValue; const envNode = getEnvelope(getAudioContext(), { ...filteredParams, @@ -507,8 +509,9 @@ function connectBusModulator(params, nodeTracker) { dc.start(params.begin); const shifted = dc.connect(gainNode(1)); signal.connect(shifted); - const currentValue = targetParams[0].value; - const { min, max } = _getRangeForParam(paramName, currentValue, control); + let currentValue = targetParams[0].value; + currentValue = currentValue === 0 ? 1 : currentValue; + const { min, max } = _getRangeForParam(paramName, 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) || Math.abs(depthValue); diff --git a/packages/superdough/superdoughdata.mjs b/packages/superdough/superdoughdata.mjs index f9df6ff8..2dac194f 100644 --- a/packages/superdough/superdoughdata.mjs +++ b/packages/superdough/superdoughdata.mjs @@ -10,8 +10,8 @@ const CONTROL_TARGETS = { 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' }, + tremolo: { node: 'tremolo', param: 'frequency' }, + tremolosync: { node: 'tremolo', param: 'frequency' }, tremolodepth: { node: 'tremolo_gain', param: 'gain' }, tremoloskew: { node: 'tremolo', param: 'skew' }, tremolophase: { node: 'tremolo', param: 'phase' }, @@ -138,8 +138,8 @@ const CONTROL_TARGETS = { pw: { node: 'source', param: 'pulsewidth' }, pwrate: { node: 'pw_lfo', param: 'frequency' }, pwsweep: { node: 'pw_lfo', param: 'depth' }, - vib: { node: 'vib_gain', param: 'gain' }, - vibmod: { node: 'vib', param: 'frequency' }, + vib: { node: 'vib', param: 'frequency' }, + vibmod: { node: 'vib_gain', param: 'gain' }, byteBeatStartTime: { node: 'source', param: 'byteBeatStartTime' }, spread: { node: 'source', param: 'panspread' }, transient: { node: 'transient', param: 'attack' }, From e79b4b8ccb749b7c1a3cafbeb00e12ee66f220a6 Mon Sep 17 00:00:00 2001 From: Aria Date: Mon, 29 Dec 2025 10:41:20 -0600 Subject: [PATCH 121/130] Refactor modulators --- packages/superdough/index.mjs | 1 + packages/superdough/modulators.mjs | 141 +++++++++++++++++++++++++++++ packages/superdough/superdough.mjs | 140 +--------------------------- packages/superdough/worklets.mjs | 3 +- 4 files changed, 147 insertions(+), 138 deletions(-) create mode 100644 packages/superdough/modulators.mjs diff --git a/packages/superdough/index.mjs b/packages/superdough/index.mjs index a382bd15..c3d2f6c5 100644 --- a/packages/superdough/index.mjs +++ b/packages/superdough/index.mjs @@ -10,6 +10,7 @@ export * from './helpers.mjs'; export * from './synth.mjs'; export * from './zzfx.mjs'; export * from './logger.mjs'; +export * from './modulators.mjs'; export * from './dspworklet.mjs'; export * from './audioContext.mjs'; export * from './wavetable.mjs'; diff --git a/packages/superdough/modulators.mjs b/packages/superdough/modulators.mjs new file mode 100644 index 00000000..3411aacb --- /dev/null +++ b/packages/superdough/modulators.mjs @@ -0,0 +1,141 @@ +/* +modulators.mjs - Helpers for constructing modulators (envelopes, LFOs, 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 . +*/ + +import { getAudioContext } from './audioContext.mjs'; +import { gainNode, getEnvelope, getLfo, webAudioTimeout } from './helpers.mjs'; +import { errorLogger } from './logger.mjs'; +import { getSuperdoughControlTargets } from './superdoughdata.mjs'; + +const 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; +}; + +const controlTargets = getSuperdoughControlTargets(); + +const stripIndex = (control) => control?.replace(/\d+$/, ''); +const getControlData = (control) => { + return controlTargets[stripIndex(control)]; +}; + +const getRangeForParam = (paramName, currentValue) => { + // We clamp the frequency to a reasonable range unless the currentValue + // is low, which indicates this may be an LFO + if (paramName === 'frequency' && currentValue >= 30) { + return { min: 20 - currentValue, max: 24000 - currentValue }; + } + return { min: undefined, max: undefined }; +}; + +const getTargetParamsForControl = (control, nodes, subControl) => { + const lookupKey = subControl ? `${control}_${subControl}` : control; + const targetInfo = getControlData(lookupKey) ?? getControlData(control); + if (!targetInfo) { + errorLogger(new Error(`Could not find control data for target '${control}'`), 'superdough'); + return { targetParams: [], paramName: control }; + } + const paramName = targetInfo.param; + const nodeKey = nodes[targetInfo.node] ? targetInfo.node : control; + const targetNodes = nodes[nodeKey]; + if (!targetNodes) { + const keys = Object.keys(nodes); + errorLogger( + new Error(`Could not connect to target '${nodeKey}' — it does not exist. Available targets: ${keys.join(', ')}`), + 'superdough', + ); + return { targetParams: [], paramName }; + } + const audioParams = []; + targetNodes.forEach((targetNode) => { + const targetParam = getNodeParam(targetNode, paramName); + audioParams.push(targetParam); + }); + return { targetParams: audioParams, paramName }; +}; + +export const 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; + let currentValue = targetParams[0].value; + currentValue = currentValue === 0 ? 1 : currentValue; + const { min, max } = getRangeForParam(paramName, 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]; + targetParams.forEach((t) => lfoNode.connect(t)); + return lfoNode; +}; + +export const 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; + let currentValue = targetParams[0].value; + currentValue = currentValue === 0 ? 1 : currentValue; + const { min, max } = getRangeForParam(paramName, 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]; + targetParams.forEach((t) => envNode.connect(t)); + return envNode; +}; + +export const connectBusModulator = (params, nodeTracker, controller) => { + 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); + let currentValue = targetParams[0].value; + debugger; + currentValue = currentValue === 0 ? 1 : currentValue; + const { min, max } = getRangeForParam(paramName, 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) || Math.abs(depthValue); + const modulator = shifted.connect(gainNode((Math.sign(depthValue) * boundedDepth) / 0.3)); + webAudioTimeout( + ac, + () => { + targetParams.forEach((t) => modulator.connect(t)); + }, + 0, + params.begin, + ); + return { modulator, toCleanup: [dc, shifted, modulator] }; +}; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 13a4ef68..517b1d3e 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -15,18 +15,16 @@ import { gainNode, getCompressor, getDistortion, - getEnvelope, getLfo, getWorklet, releaseAudioNode, - webAudioTimeout, } from './helpers.mjs'; import { map } from 'nanostores'; -import { errorLogger, logger } from './logger.mjs'; +import { logger } from './logger.mjs'; +import { connectLFO, connectEnvelope, connectBusModulator } from './modulators.mjs'; import { loadBuffer } from './sampler.mjs'; -import { getAudioContext, setAudioContext } from './audioContext.mjs'; +import { getAudioContext } from './audioContext.mjs'; import { SuperdoughAudioController } from './superdoughoutput.mjs'; -import { getSuperdoughControlTargets } from './superdoughdata.mjs'; import { resetSeenKeys } from './wavetable.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; @@ -397,136 +395,6 @@ export function resetGlobalEffects() { analysersData = {}; } -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; -} - -const controlTargets = getSuperdoughControlTargets(); - -const _stripIndex = (control) => control?.replace(/\d+$/, ''); -function _getControlData(control) { - return controlTargets[_stripIndex(control)]; -} - -function _getRangeForParam(paramName, currentValue) { - // We clamp the frequency to a reasonable range unless the currentValue - // is low, which indicates this may be an LFO - if (paramName === 'frequency' && currentValue >= 30) { - return { min: 20 - currentValue, max: 24000 - currentValue }; - } - return { min: undefined, max: undefined }; -} - -function _getTargetParamsForControl(control, nodes, subControl) { - const lookupKey = subControl ? `${control}_${subControl}` : control; - const targetInfo = _getControlData(lookupKey) ?? _getControlData(control); - if (!targetInfo) { - errorLogger(new Error(`Could not find control data for target '${control}'`), 'superdough'); - return { targetParams: [], paramName: control }; - } - const paramName = targetInfo.param; - const nodeKey = nodes[targetInfo.node] ? targetInfo.node : control; - const targetNodes = nodes[nodeKey]; - if (!targetNodes) { - const keys = Object.keys(nodes); - errorLogger( - new Error(`Could not connect to target '${nodeKey}' — it does not exist. Available targets: ${keys.join(', ')}`), - 'superdough', - ); - return { targetParams: [], paramName }; - } - const audioParams = []; - targetNodes.forEach((targetNode) => { - const targetParam = _getNodeParam(targetNode, paramName); - audioParams.push(targetParam); - }); - return { targetParams: audioParams, paramName }; -} - -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; - let currentValue = targetParams[0].value; - currentValue = currentValue === 0 ? 1 : currentValue; - const { min, max } = _getRangeForParam(paramName, 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]; - targetParams.forEach((t) => lfoNode.connect(t)); - return lfoNode; -} - -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; - let currentValue = targetParams[0].value; - currentValue = currentValue === 0 ? 1 : currentValue; - const { min, max } = _getRangeForParam(paramName, 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]; - targetParams.forEach((t) => envNode.connect(t)); - return envNode; -} - -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); - let currentValue = targetParams[0].value; - currentValue = currentValue === 0 ? 1 : currentValue; - const { min, max } = _getRangeForParam(paramName, 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) || Math.abs(depthValue); - const modulator = shifted.connect(gainNode((Math.sign(depthValue) * boundedDepth) / 0.3)); - webAudioTimeout( - ac, - () => { - targetParams.forEach((t) => modulator.connect(t)); - }, - 0, - params.begin, - ); - return { modulator, toCleanup: [dc, shifted, modulator] }; -} - 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 @@ -1038,7 +906,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } if (value.bmod) { for (const p of value.bmod) { - const { toCleanup } = connectBusModulator({ ...p, begin: t, end: endWithRelease }, nodes); + const { toCleanup } = connectBusModulator({ ...p, begin: t, end: endWithRelease }, nodes, controller); audioNodes.push(...toCleanup); } } diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 4523e9e8..8001c7da 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1055,8 +1055,7 @@ class EnvelopeProcessor extends AudioWorkletProcessor { this.state = (this.state + 1) % states.length; time = states[this.state].time; } - const clamped = clamp(this.val * depth, min, max); - out[i] = clamped; + out[i] = clamp(this.val * depth, min, max); } return true; } From 680b7d78ec3fb5ab89ca27c56a9695519f564cec Mon Sep 17 00:00:00 2001 From: Aria Date: Mon, 29 Dec 2025 12:59:48 -0600 Subject: [PATCH 122/130] Lots of examples; use id instead of index; waveshaper clamping; one mode for synth --- packages/core/controls.mjs | 110 ++++++++++++++++++---- packages/superdough/modulators.mjs | 35 ++++--- packages/superdough/superdough.mjs | 15 +-- packages/superdough/synth.mjs | 15 ++- test/__snapshots__/examples.test.mjs.snap | 90 ++++++++++++++++++ test/examples.test.mjs | 1 + 6 files changed, 228 insertions(+), 38 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 5cc53de6..1bf3fa85 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2869,8 +2869,8 @@ registerSubControls('bmod', [ ['dc'], ]); -Pattern.prototype.modulate = function (type, config, idx) { - config ??= { control: undefined }; +Pattern.prototype.modulate = function (type, config, id) { + config = { control: undefined, ...config }; const modulatorKeys = ['lfo', 'env', 'bmod']; if (!modulatorKeys.includes(type)) { logger(`[core] Modulation type ${type} not found. Please use one of 'lfo', 'env', 'bmod'`); @@ -2888,19 +2888,20 @@ Pattern.prototype.modulate = function (type, config, idx) { // 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}`; + control = `${control}_${[...v[control].__ids].at(-1)}`; } defaultValue = control; } - v[type] ??= []; + v[type] ??= { __ids: new Set() }; const t = v[type]; - idx ??= t.length; - t[idx] ??= { control: defaultValue }; + id ??= t.__ids.size; + t[id] ??= { control: defaultValue }; + t.__ids.add(id); // keeps track of insertion order if (c === undefined) return v; if (key === 'control' || key === 'subControl') { - t[idx][key] = getControlName(c); + t[id][key] = getControlName(c); } else { - t[idx][key] = c; + t[id][key] = c; } return v; }) @@ -2910,10 +2911,16 @@ Pattern.prototype.modulate = function (type, config, idx) { }; /** - * Configures an LFO. Can be called in sequence like pat.lfo(...).lfo(...) to set up multiple LFOs + * Configures an LFO. Can be called in sequence like pat.lfo(...).lfo(...) to set up multiple LFOs. + * There are two ways to declare which control will be modulated: + * 1. Explicitly put `control` in the config (e.g. `lfo({ c: "lpf" })`) + * 2. If the control parameter is absent, the control _immediately before_ the `lfo` call will be used + * (e.g. `s("saw").lpf(500).lfo()` to modulate `lpf`) + * + * Modulators can be referred to by `id` so that they can be updated later e.g. inside + * a `sometimes`. See example below. * * @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 @@ -2925,18 +2932,47 @@ Pattern.prototype.modulate = function (type, config, idx) { * @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 + * @param {string | Pattern} id ID to use for this modulator * @returns Pattern + * + * @example + * s("saw").note("F1").lpf(500).lfo() + * + * @example + * s("saw").lfo().lpf(500).lfo({ s: 0.3 }) + * + * @example + * s("saw").lpf(500).diode(0.3) + * .lfo({ c: "lpf" }) + * + * @example + * s("pulse").lpf(500).lfo() + * .lfo({ c: "s" }) + * .diode(0.3) + * .sometimes(x => x.lfo({ s: "8" }, 1)) // lfo #1 (0-indexed) + * + * @example + * s("pulse").lpf(500).lfo({ depth: 4 }, 'lpf_mod') + * .lfo({ c: "s" }) + * .diode(0.3) + * .sometimes(x => x.lfo({ s: "8" }, 'lpf_mod')) */ -Pattern.prototype.lfo = function (config, idx) { - return this.modulate('lfo', config, idx); +Pattern.prototype.lfo = function (config, id) { + return this.modulate('lfo', config, id); }; export const lfo = (config) => pure({}).lfo(config); /** * Configures an envelope. Can be called in sequence like pat.env(...).env(...) to set up multiple envelopes + * There are two ways to declare which control will be modulated: + * 1. Explicitly put `control` in the config (e.g. `env({ c: "lpf" })`) + * 2. If the control parameter is absent, the control _immediately before_ the `env` call will be used + * (e.g. `s("saw").lpf(500).env({ a: 1 })` to modulate `lpf`) + * + * Modulators can be referred to by `id` so that they can be updated later e.g. inside + * a `sometimes`. See example below. * * @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 @@ -2949,10 +2985,35 @@ 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 {string | Pattern} id ID to use for this modulator * @returns Pattern + * + * @example + * s("saw").note("F1").lpf(500).env({ a: 1 }) + * + * @example + * s("saw").env({ d: 1 }).note("F1") + * .lpq(4).lpf(50) + * .env({ a: 0.1, d: 1, ac: 0.8, dc: 0.3, depth: 50 }) + * + * @example + * s("saw").lpf(500).diode(0.3) + * .env({ c: "lpf", a: 0.5, d: 0.5 }) + * + * @example + * s("pulse").lpf(500).env({ a: 1 }) + * .env({ c: "s", a: 1 }) + * .diode(0.3) + * .sometimes(x => x.env({ a: "0.5" }, 1)) // envelope #1 (0-indexed) + * + * @example + * s("pulse").lpf(500).env({ a: 1 }, 'lpf_mod') + * .env({ c: "s", a: 1 }) + * .diode(0.3) + * .sometimes(x => x.env({ a: "0.5" }, 'lpf_mod')) */ -Pattern.prototype.env = function (config, idx) { - return this.modulate('env', config, idx); +Pattern.prototype.env = function (config, id) { + return this.modulate('env', config, id); }; export const env = (config) => pure({}).env(config); @@ -2962,8 +3023,15 @@ export const env = (config) => pure({}).env(config); * * Send to an audio bus with `otherPat.bus(..)`. * + * There are two ways to declare which control will be modulated: + * 1. Explicitly put `control` in the config (e.g. `bmod({ id: 2, c: "lpf" })`) + * 2. If the control parameter is absent, the control _immediately before_ the `bmod` call will be used + * (e.g. `s("saw").lpf(500).bmod({ id: 2 })` to modulate `lpf`) + * + * Modulators can be referred to by `id` so that they can be updated later e.g. inside + * a `sometimes`. See example below. + * * @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 @@ -2971,10 +3039,16 @@ export const env = (config) => pure({}).env(config); * @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.dc] DC offset prior to application + * @param {string | Pattern} id ID to use for this modulator * @returns Pattern + * + * @example + * modulator: s("one").seg(64).gain(slider(0, 0, 1)).bus(1).dry(0) + * carrier: s("saw").bmod({ b: 1 }) + * */ -Pattern.prototype.bmod = function (config, idx) { - return this.modulate('bmod', config, idx); +Pattern.prototype.bmod = function (config, id) { + return this.modulate('bmod', config, id); }; export const bmod = (config) => pure({}).bmod(config); diff --git a/packages/superdough/modulators.mjs b/packages/superdough/modulators.mjs index 3411aacb..32ebae26 100644 --- a/packages/superdough/modulators.mjs +++ b/packages/superdough/modulators.mjs @@ -8,6 +8,7 @@ import { getAudioContext } from './audioContext.mjs'; import { gainNode, getEnvelope, getLfo, webAudioTimeout } from './helpers.mjs'; import { errorLogger } from './logger.mjs'; import { getSuperdoughControlTargets } from './superdoughdata.mjs'; +import { clamp } from './util.mjs'; const getNodeParam = (node, name) => { // Worklet case @@ -27,9 +28,8 @@ const getNodeParam = (node, name) => { const controlTargets = getSuperdoughControlTargets(); -const stripIndex = (control) => control?.replace(/\d+$/, ''); const getControlData = (control) => { - return controlTargets[stripIndex(control)]; + return controlTargets[control.split('_')[0]]; }; const getRangeForParam = (paramName, currentValue) => { @@ -41,6 +41,19 @@ const getRangeForParam = (paramName, currentValue) => { return { min: undefined, max: undefined }; }; +const clampWithWaveShaper = (modulator, min, max) => { + const ac = getAudioContext(); + const curve = new Float32Array(256); + for (let i = 0; i < curve.length; i++) { + const x = (i / (curve.length - 1)) * 2 - 1; + curve[i] = clamp(x * max, min, max); + } + const shaper = new WaveShaperNode(ac, { curve }); + const scaleGain = gainNode(1 / max); + modulator.connect(scaleGain).connect(shaper); + return { modulator, toCleanup: [shaper, scaleGain] }; +}; + const getTargetParamsForControl = (control, nodes, subControl) => { const lookupKey = subControl ? `${control}_${subControl}` : control; const targetInfo = getControlData(lookupKey) ?? getControlData(control); @@ -67,7 +80,7 @@ const getTargetParamsForControl = (control, nodes, subControl) => { return { targetParams: audioParams, paramName }; }; -export const connectLFO = (idx, params, nodeTracker) => { +export const connectLFO = (id, 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; @@ -84,12 +97,12 @@ export const connectLFO = (idx, params, nodeTracker) => { max, }; const lfoNode = getLfo(getAudioContext(), modParams); - nodeTracker[`lfo${idx}`] = [lfoNode]; + nodeTracker[`lfo_${id}`] = [lfoNode]; targetParams.forEach((t) => lfoNode.connect(t)); return lfoNode; }; -export const connectEnvelope = (idx, params, nodeTracker) => { +export const connectEnvelope = (id, params, nodeTracker) => { const { control, subControl, acurve, dcurve, rcurve, depth = 1, depthabs, ...filteredParams } = params; const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker, subControl); if (!targetParams.length) return; @@ -106,7 +119,7 @@ export const connectEnvelope = (idx, params, nodeTracker) => { decayCurve: dcurve, releaseCurve: rcurve, }); - nodeTracker[`env${idx}`] = [envNode]; + nodeTracker[`env_${id}`] = [envNode]; targetParams.forEach((t) => envNode.connect(t)); return envNode; }; @@ -122,13 +135,12 @@ export const connectBusModulator = (params, nodeTracker, controller) => { const shifted = dc.connect(gainNode(1)); signal.connect(shifted); let currentValue = targetParams[0].value; - debugger; currentValue = currentValue === 0 ? 1 : currentValue; const { min, max } = getRangeForParam(paramName, 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) || Math.abs(depthValue); - const modulator = shifted.connect(gainNode((Math.sign(depthValue) * boundedDepth) / 0.3)); + const depthGain = gainNode((Math.sign(depthValue) * Math.abs(depthValue)) / 0.3); + const unClamped = shifted.connect(depthGain); + let { modulator, toCleanup } = clampWithWaveShaper(unClamped, min, max); webAudioTimeout( ac, () => { @@ -137,5 +149,6 @@ export const connectBusModulator = (params, nodeTracker, controller) => { 0, params.begin, ); - return { modulator, toCleanup: [dc, shifted, modulator] }; + toCleanup.push(dc, shifted, depthGain); + return { modulator, toCleanup }; }; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 517b1d3e..ee0cec83 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -875,9 +875,10 @@ 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 [idx, params] of Object.entries(value.lfo)) { + for (const id of value.lfo.__ids) { + const params = value.lfo[id]; const lfo = connectLFO( - idx, + id, { ...params, cps, @@ -891,9 +892,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } } if (value.env) { - for (const [idx, params] of Object.entries(value.env)) { + for (const id of value.env.__ids) { + const params = value.env[id]; const env = connectEnvelope( - idx, + id, { ...params, begin: t, @@ -905,8 +907,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } } if (value.bmod) { - for (const p of value.bmod) { - const { toCleanup } = connectBusModulator({ ...p, begin: t, end: endWithRelease }, nodes, controller); + for (const id of value.bmod.__ids) { + const params = value.bmod[id]; + const { toCleanup } = connectBusModulator({ ...params, begin: t, end: endWithRelease }, nodes, controller); audioNodes.push(...toCleanup); } } diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index d2069272..6c8d1fea 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -19,7 +19,7 @@ import { import { logger } from './logger.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; -const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user']; +const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user', 'one']; const waveformAliases = [ ['tri', 'triangle'], ['sqr', 'square'], @@ -508,8 +508,17 @@ export function getOscillator(s, t, value, onended) { s = 'triangle'; } s = s === 'user' && !partials ? 'triangle' : s; - // If no partials are given, use stock waveforms - if (!partials || partials?.length === 0 || s === 'sine') { + if (s === 'one') { + // Constant 1 oscillator (used for modulation) + o = new ConstantSourceNode(getAudioContext(), { offset: 1 }); + o.start(t); + return { + node: o, + nodes: { source: o }, + stop: (time) => o?.stop(time), + }; + } else if (!partials || partials?.length === 0 || s === 'sine') { + // If no partials are given, use stock waveforms o = getAudioContext().createOscillator(); o.type = s || 'triangle'; } diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 5f8133ec..61ab1186 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3803,6 +3803,51 @@ exports[`runs examples > example "end" example index 0 1`] = ` ] `; +exports[`runs examples > example "env" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]", + "[ 1/1 → 2/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]", + "[ 2/1 → 3/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]", + "[ 3/1 → 4/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]", +] +`; + +exports[`runs examples > example "env" example index 1 1`] = ` +[ + "[ 0/1 → 1/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]", + "[ 1/1 → 2/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]", + "[ 2/1 → 3/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]", + "[ 3/1 → 4/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]", +] +`; + +exports[`runs examples > example "env" example index 2 1`] = ` +[ + "[ 0/1 → 1/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]", + "[ 1/1 → 2/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]", + "[ 2/1 → 3/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]", + "[ 3/1 → 4/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]", +] +`; + +exports[`runs examples > example "env" example index 3 1`] = ` +[ + "[ 0/1 → 1/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:0.5} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 1/1 → 2/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:1} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 2/1 → 3/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:1} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 3/1 → 4/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:0.5} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]", +] +`; + +exports[`runs examples > example "env" example index 4 1`] = ` +[ + "[ 0/1 → 1/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:0.5}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 1/1 → 2/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:1}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 2/1 → 3/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:1}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 3/1 → 4/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:0.5}} distort:0.3 distortvol:1 distorttype:diode ]", +] +`; + exports[`runs examples > example "euclid" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:c3 ]", @@ -6113,6 +6158,51 @@ exports[`runs examples > example "leslie" example index 0 1`] = ` ] `; +exports[`runs examples > example "lfo" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]", + "[ 1/1 → 2/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]", + "[ 2/1 → 3/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]", + "[ 3/1 → 4/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]", +] +`; + +exports[`runs examples > example "lfo" example index 1 1`] = ` +[ + "[ 0/1 → 1/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]", + "[ 1/1 → 2/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]", + "[ 2/1 → 3/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]", + "[ 3/1 → 4/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]", +] +`; + +exports[`runs examples > example "lfo" example index 2 1`] = ` +[ + "[ 0/1 → 1/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]", + "[ 1/1 → 2/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]", + "[ 2/1 → 3/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]", + "[ 3/1 → 4/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]", +] +`; + +exports[`runs examples > example "lfo" example index 3 1`] = ` +[ + "[ 0/1 → 1/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s sync:8} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 1/1 → 2/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 2/1 → 3/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 3/1 → 4/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s sync:8} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]", +] +`; + +exports[`runs examples > example "lfo" example index 4 1`] = ` +[ + "[ 0/1 → 1/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4 sync:8}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 1/1 → 2/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 2/1 → 3/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4}} distort:0.3 distortvol:1 distorttype:diode ]", + "[ 3/1 → 4/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4 sync:8}} distort:0.3 distortvol:1 distorttype:diode ]", +] +`; + exports[`runs examples > example "linger" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:lt ]", diff --git a/test/examples.test.mjs b/test/examples.test.mjs index 896a02f8..ebec2252 100644 --- a/test/examples.test.mjs +++ b/test/examples.test.mjs @@ -20,6 +20,7 @@ const skippedExamples = [ 'accelerationX', 'defaultmidimap', 'midimaps', + 'bmod', ]; describe('runs examples', () => { From 0edede106aaf44194c53348658991ccd9391af44 Mon Sep 17 00:00:00 2001 From: Aria Date: Mon, 29 Dec 2025 13:28:26 -0600 Subject: [PATCH 123/130] Fixes for zzfx and soundfonts --- packages/superdough/modulators.mjs | 6 +++++- packages/superdough/zzfx.mjs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/superdough/modulators.mjs b/packages/superdough/modulators.mjs index 32ebae26..96c68d4b 100644 --- a/packages/superdough/modulators.mjs +++ b/packages/superdough/modulators.mjs @@ -19,7 +19,11 @@ const getNodeParam = (node, name) => { } } // Built-in node case - const p = node?.[name]; + let p = node?.[name]; + if (p === undefined && name === 'frequency') { + // Fallbacks for source nodes without 'frequency' params (e.g. soundfonts) + p = node?.['detune'] ?? node?.['playbackRate']; + } if (p instanceof AudioParam) { return p; } diff --git a/packages/superdough/zzfx.mjs b/packages/superdough/zzfx.mjs index f3dc710b..3d6b8e9f 100644 --- a/packages/superdough/zzfx.mjs +++ b/packages/superdough/zzfx.mjs @@ -75,7 +75,6 @@ export const getZZFX = (value, t) => { source.start(t); return { node: source, - nodes: { source: [source] }, }; }; @@ -91,6 +90,7 @@ export function registerZZFXSounds() { }); return { node: o, + nodes: { source: [o] }, stop: () => {}, }; }, From 6051b922c065175c3c64b5ea34d736ffb16a445c Mon Sep 17 00:00:00 2001 From: Aria Date: Mon, 29 Dec 2025 14:45:49 -0600 Subject: [PATCH 124/130] Fix clamping when min/max not specified --- packages/superdough/modulators.mjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/superdough/modulators.mjs b/packages/superdough/modulators.mjs index 96c68d4b..7a1fe252 100644 --- a/packages/superdough/modulators.mjs +++ b/packages/superdough/modulators.mjs @@ -144,7 +144,13 @@ export const connectBusModulator = (params, nodeTracker, controller) => { const depthValue = depthabs != null ? depthabs : depth * currentValue; const depthGain = gainNode((Math.sign(depthValue) * Math.abs(depthValue)) / 0.3); const unClamped = shifted.connect(depthGain); - let { modulator, toCleanup } = clampWithWaveShaper(unClamped, min, max); + const toCleanup = []; + let modulator = unClamped; + if (min !== undefined && max !== undefined) { + const wsData = clampWithWaveShaper(unClamped, min, max); + modulator = wsData.modulator; + toCleanup.push(...wsData.toCleanup); + } webAudioTimeout( ac, () => { From d41ebf190b6c36d6124cb5af17f78f77e056ab66 Mon Sep 17 00:00:00 2001 From: eddyflux Date: Tue, 30 Dec 2025 00:22:51 +0100 Subject: [PATCH 125/130] fix: missing punctuation --- website/src/pages/workshop/first-sounds.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/workshop/first-sounds.mdx b/website/src/pages/workshop/first-sounds.mdx index 1d659972..84deb055 100644 --- a/website/src/pages/workshop/first-sounds.mdx +++ b/website/src/pages/workshop/first-sounds.mdx @@ -377,4 +377,4 @@ insect [crow metal] - -, punchcard /> -Now that we know the basics of how to make beats, let's look at how we can play [notes](/workshop/first-notes) +Now that we know the basics of how to make beats, let's look at how we can play [notes](/workshop/first-notes). From aec33710b751b84c7d65ce22caea24ed34a813c1 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 31 Dec 2025 14:58:31 -0600 Subject: [PATCH 126/130] Working version --- packages/core/controls.mjs | 8 + packages/core/pattern.mjs | 16 + packages/superdough/feedbackdelay.mjs | 19 +- packages/superdough/modulators.mjs | 27 +- packages/superdough/reverbGen.mjs | 6 +- packages/superdough/superdough.mjs | 746 ++++++++++++++------------ 6 files changed, 472 insertions(+), 350 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 1bf3fa85..b18a8dff 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2846,6 +2846,7 @@ registerSubControls('lfo', [ ['skew', 'sk'], ['curve'], ['sync', 's'], + ['fxi'], ]); registerSubControls('env', [ ['control', 'c'], @@ -2859,6 +2860,7 @@ registerSubControls('env', [ ['acurve', 'ac'], ['dcurve', 'dc'], ['rcurve', 'rc'], + ['fxi'], ]); registerSubControls('bmod', [ ['bus', 'b'], @@ -2867,6 +2869,7 @@ registerSubControls('bmod', [ ['depth', 'dep', 'dr'], ['depthabs', 'da'], ['dc'], + ['fxi'], ]); Pattern.prototype.modulate = function (type, config, id) { @@ -2932,6 +2935,7 @@ Pattern.prototype.modulate = function (type, config, id) { * @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 + * @param {number | Pattern} [config.fxi] FX index to target * @param {string | Pattern} id ID to use for this modulator * @returns Pattern * @@ -2985,6 +2989,7 @@ 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 | Pattern} [config.fxi] FX index to target * @param {string | Pattern} id ID to use for this modulator * @returns Pattern * @@ -3039,6 +3044,7 @@ export const env = (config) => pure({}).env(config); * @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.dc] DC offset prior to application + * @param {number | Pattern} [config.fxi] FX index to target * @param {string | Pattern} id ID to use for this modulator * @returns Pattern * @@ -3065,3 +3071,5 @@ export const bmod = (config) => pure({}).bmod(config); * s("hh*16").bank("tr909").transient("<-1:1 1:-1>") */ export const { transient } = registerControl(['transient', 'transsustain']); + +export const { FXrelease, FXrel, FXr, fxr } = registerControl('FXrelease', 'FXrel', 'FXr', 'fxr'); diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 6f214386..e6169399 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3721,3 +3721,19 @@ Pattern.prototype.phases = function (list) { export const phases = (list) => { return _ensureListPattern(list).as('phases'); }; + +/** + * Establishes an FX chain. Can be called by chaining .FX().FX().. + * calls and/or in a single .FX(, , ..) call. The , .. are _patterns_ which + * establish the controls of the given effect. See examples. + * @name FX + * @memberof Pattern + * @returns Pattern + */ +Pattern.prototype.FX = function (...effects) { + effects = effects.map(reify); + return this.withValue((v) => (vEff) => { + const currFX = v.FX ?? []; + return { ...v, FX: currFX.concat(vEff) }; + }).appLeft(parray(effects)); +}; diff --git a/packages/superdough/feedbackdelay.mjs b/packages/superdough/feedbackdelay.mjs index b8269bc0..38717613 100644 --- a/packages/superdough/feedbackdelay.mjs +++ b/packages/superdough/feedbackdelay.mjs @@ -5,19 +5,18 @@ if (typeof DelayNode !== 'undefined') { wet = Math.abs(wet); this.delayTime.value = time; - const feedbackGain = ac.createGain(); - feedbackGain.gain.value = Math.min(Math.abs(feedback), 0.995); - this.feedback = feedbackGain.gain; + this.feedbackGain = ac.createGain(); + this.feedbackGain.gain.value = Math.min(Math.abs(feedback), 0.995); + this.feedback = this.feedbackGain.gain; - const delayGain = ac.createGain(); - delayGain.gain.value = wet; - this.delayGain = delayGain; + this.delayGain = ac.createGain(); + this.delayGain.gain.value = wet; - this.connect(feedbackGain); - this.connect(delayGain); - feedbackGain.connect(this); + this.connect(this.feedbackGain); + this.connect(this.delayGain); + this.feedbackGain.connect(this); - this.connect = (target) => delayGain.connect(target); + this.connect = (target) => this.delayGain.connect(target); return this; } start(t) { diff --git a/packages/superdough/modulators.mjs b/packages/superdough/modulators.mjs index 7a1fe252..2b6fcf33 100644 --- a/packages/superdough/modulators.mjs +++ b/packages/superdough/modulators.mjs @@ -85,8 +85,19 @@ const getTargetParamsForControl = (control, nodes, subControl) => { }; export const connectLFO = (id, params, nodeTracker) => { - const { rate = 1, sync, cps, cycle, control = 'lfo', subControl, depth = 1, depthabs, ...filteredParams } = params; - const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker, subControl); + const { + rate = 1, + sync, + cps, + cycle, + control = 'lfo', + subControl, + fxi = 'main', + depth = 1, + depthabs, + ...filteredParams + } = params; + const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl); if (!targetParams.length) return; let currentValue = targetParams[0].value; currentValue = currentValue === 0 ? 1 : currentValue; @@ -101,14 +112,14 @@ export const connectLFO = (id, params, nodeTracker) => { max, }; const lfoNode = getLfo(getAudioContext(), modParams); - nodeTracker[`lfo_${id}`] = [lfoNode]; + nodeTracker[0][`lfo_${id}`] = [lfoNode]; targetParams.forEach((t) => lfoNode.connect(t)); return lfoNode; }; export const connectEnvelope = (id, params, nodeTracker) => { - const { control, subControl, acurve, dcurve, rcurve, depth = 1, depthabs, ...filteredParams } = params; - const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker, subControl); + const { control, subControl, acurve, dcurve, rcurve, depth = 1, depthabs, fxi = 'main', ...filteredParams } = params; + const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl); if (!targetParams.length) return; let currentValue = targetParams[0].value; currentValue = currentValue === 0 ? 1 : currentValue; @@ -123,15 +134,15 @@ export const connectEnvelope = (id, params, nodeTracker) => { decayCurve: dcurve, releaseCurve: rcurve, }); - nodeTracker[`env_${id}`] = [envNode]; + nodeTracker[0][`env_${id}`] = [envNode]; targetParams.forEach((t) => envNode.connect(t)); return envNode; }; export const connectBusModulator = (params, nodeTracker, controller) => { const ac = getAudioContext(); - const { control, subControl, depth = 1, depthabs } = params; - const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker, subControl); + const { control, subControl, depth = 1, depthabs, fxi = 'main' } = params; + const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl); if (!targetParams.length) return { toCleanup: [] }; const signal = controller.getBus(params.bus); const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 }); diff --git a/packages/superdough/reverbGen.mjs b/packages/superdough/reverbGen.mjs index bb050874..0949a30d 100644 --- a/packages/superdough/reverbGen.mjs +++ b/packages/superdough/reverbGen.mjs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { releaseAudioNode } from './helpers.mjs'; + var reverbGen = {}; /** Generates a reverb impulse response. @@ -104,8 +106,8 @@ var applyGradualLowpass = function (input, lpFreqStart, lpFreqEnd, lpFreqEndAt, player.start(); context.oncomplete = function (event) { callback(event.renderedBuffer); - filter.disconnect(); - player.disconnect(); + releaseAudioNode(filter); + releaseAudioNode(player); }; context.startRendering(); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 8a3e9f7b..9e9ffa12 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th import './feedbackdelay.mjs'; import './reverb.mjs'; import './vowel.mjs'; -import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs'; +import { clamp, nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs'; import workletsUrl from './worklets.mjs?audioworklet'; import { createFilter, @@ -18,6 +18,7 @@ import { getLfo, getWorklet, releaseAudioNode, + webAudioTimeout, } from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; @@ -193,6 +194,8 @@ let defaultDefaultValues = { i: 1, velocity: 1, fft: 8, + tremolodepth: 1, + tremolophase: 0, }; const defaultDefaultDefaultValues = Object.freeze({ ...defaultDefaultValues }); @@ -402,8 +405,37 @@ function mapChannelNumbers(channels) { return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); } +class Chain { + constructor(head) { + this.audioNodes = [head]; + this.tails = [head]; + } + connect(...nodes) { + nodes.forEach((node) => { + this.tails.forEach((tail) => { + tail.connect(node); + }); + }); + this.tails = nodes; + this.audioNodes.push(...nodes); + return this; + } + connectOne(idx, node) { + this.tails[idx].connect(node); + this.tails[idx] = node; + this.audioNodes.push(node); + return this; + } + releaseNodes() { + this.audioNodes.forEach((n) => releaseAudioNode(n)); + this.audioNodes = []; + this.tails = []; + } +} + export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { - let nodes = {}; + // mapping from main FX and numbered FX chains to nodes + const nodes = { main: {} }; // new: t is always expected to be the absolute target onset time const ac = getAudioContext(); const audioController = getSuperdoughAudioController(); @@ -432,44 +464,17 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } // destructure let { - tremolo, - tremolosync, - tremolodepth = 1, - tremoloskew, - tremolophase = 0, - tremoloshape, s = getDefaultValue('s'), bank, source, - gain = getDefaultValue('gain'), postgain = getDefaultValue('postgain'), - density = getDefaultValue('density'), duckorbit, duckonset, duckattack, duckdepth, djf, - // filters - fanchor = getDefaultValue('fanchor'), - release = 0, - - //phaser - phaserrate, - phaserdepth = getDefaultValue('phaserdepth'), - phasersweep, - phasercenter, - // - coarse, - - crush, + release = 0.01, dry, - shape, - shapevol = getDefaultValue('shapevol'), - distort, - distortvol = getDefaultValue('distortvol'), - distorttype = getDefaultValue('distorttype'), - pan, - vowel, delay = getDefaultValue('delay'), delayfeedback = getDefaultValue('delayfeedback'), delaysync = getDefaultValue('delaysync'), @@ -486,16 +491,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) irspeed, irbegin, i = getDefaultValue('i'), - velocity = getDefaultValue('velocity'), analyze, // analyser wet fft = getDefaultValue('fft'), // fftSize 0 - 10 - compressor: compressorThreshold, - compressorRatio, - compressorKnee, - compressorAttack, - compressorRelease, - transient, - transsustain, + FX = [], + FXrelease, } = value; delaytime = delaytime ?? cycleToSeconds(delaysync, cps); @@ -510,18 +509,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth); } - gain = applyGainCurve(nanFallback(gain, 1)); postgain = applyGainCurve(postgain); - shapevol = applyGainCurve(shapevol); - distortvol = applyGainCurve(distortvol); 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; - const endWithRelease = end + release; + const fullRelease = Math.max(release, FXrelease ?? 0); + const endWithRelease = end + fullRelease; const chainID = Math.round(Math.random() * 1000000); // oldest audio nodes will be destroyed if maximum polyphony is exceeded @@ -535,8 +529,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) activeSoundSources.delete(chainID); } - const audioNodes = []; - if (['-', '~', '_'].includes(s)) { return; } @@ -549,19 +541,16 @@ 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]; + nodes.main['source'] = [sourceNode]; } else if (getSound(s)) { const { onTrigger } = getSound(s); - const onEnded = () => { - audioNodes.forEach((n) => releaseAudioNode(n)); - activeSoundSources.delete(chainID); - }; + const onEnded = () => {}; const soundHandle = await onTrigger(t, value, onEnded, cps); if (soundHandle) { sourceNode = soundHandle.node; activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC - nodes = { ...nodes, ...soundHandle.nodes }; + nodes.main = { ...nodes.main, ...soundHandle.nodes }; } } else { throw new Error(`sound ${s} not found! Is it loaded?`); @@ -576,253 +565,339 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) logger('[webaudio] skip hap: still loading', ac.currentTime - t); return; } - const chain = []; // audio nodes that will be connected to each other sequentially - chain.push(sourceNode); - stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch })); - if (transient !== undefined) { - const transProcessor = getWorklet( - ac, - 'transient-processor', - {}, - { - processorOptions: { - attack: transient, - sustain: transsustain, - begin: t, - end: endWithRelease, + const chain = new Chain(sourceNode); // audio nodes that will be connected to each other sequentially + FX = [...FX, value]; // run through the FX chain and then run through all FX outside of it as well + for (let [idx, fx] of Object.entries(FX)) { + const key = idx == FX.length - 1 ? 'main' : idx; + nodes[key] ??= {}; + const fxNodes = nodes[key]; + let { + gain = getDefaultValue('gain'), + velocity = getDefaultValue('velocity'), + shapevol = getDefaultValue('shapevol'), + distorttype = getDefaultValue('distorttype'), + distortvol = getDefaultValue('distortvol'), + tremolodepth = getDefaultValue('tremolodepth'), + phaserdepth = getDefaultValue('phaserdepth'), + delay = getDefaultValue('delay'), + delayfeedback = getDefaultValue('delayfeedback'), + delaysync = getDefaultValue('delaysync'), + delaytime, + stretch = getDefaultValue('stretch'), + } = fx; + gain = applyGainCurve(nanFallback(gain, 1)); + shapevol = applyGainCurve(shapevol); + distortvol = applyGainCurve(distortvol); + velocity = applyGainCurve(velocity); + tremolodepth = applyGainCurve(tremolodepth); + gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future + delaytime = delaytime ?? cycleToSeconds(delaysync, cps); + + stretch !== undefined && chain.connect(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch })); + + if (fx.transient !== undefined) { + const transProcessor = getWorklet( + ac, + 'transient-processor', + {}, + { + processorOptions: { + attack: fx.transient, + sustain: fx.transsustain, + begin: t, + end: endWithRelease, + }, }, - }, - ); - chain.push(transProcessor); - nodes['transient'] = transProcessor; - } + ); + chain.connect(transProcessor); + fxNodes['transient'] = transProcessor; + } - // gain stage - const initialGain = gainNode(gain); - nodes['gain'] = [initialGain]; - chain.push(initialGain); + // gain stage + const initialGain = gainNode(gain); + fxNodes['gain'] = [initialGain]; + chain.connect(initialGain); - // filter - const ftype = getFilterType(value.ftype); + // filter + const ftype = getFilterType(value.ftype); - const filt = (params) => createFilter(ac, t, end, params, cps, cycle); - if (value.cutoff !== undefined) { - const lpMap = { - frequency: 'cutoff', - q: 'resonance', - attack: 'lpattack', - decay: 'lpdecay', - sustain: 'lpsustain', - release: 'lprelease', - env: 'lpenv', - anchor: 'fanchor', - model: 'ftype', - drive: 'drive', - rate: 'lprate', - sync: 'lpsync', - depth: 'lpdepth', - depthfrequency: 'lpdepthfrequency', - shape: 'lpshape', - dcoffset: 'lpdc', - skew: 'lpskew', - }; - const lpParams = pickAndRename(value, lpMap); - 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); + const filt = (params) => createFilter(ac, t, end, params, cps, cycle); + if (fx.cutoff !== undefined) { + const lpMap = { + frequency: 'cutoff', + q: 'resonance', + attack: 'lpattack', + decay: 'lpdecay', + sustain: 'lpsustain', + release: 'lprelease', + env: 'lpenv', + anchor: 'fanchor', + model: 'ftype', + drive: 'drive', + rate: 'lprate', + sync: 'lpsync', + depth: 'lpdepth', + depthfrequency: 'lpdepthfrequency', + shape: 'lpshape', + dcoffset: 'lpdc', + skew: 'lpskew', + }; + const lpParams = pickAndRename(fx, lpMap); + lpParams.type = 'lowpass'; + const { filter: lpf1, lfo: lfo1 } = filt(lpParams); + fxNodes['lpf'] = [lpf1]; + fxNodes['lpf_lfo'] = [lfo1]; + chain.connect(lpf1); + lfo1 && chain.audioNodes.push(lfo1); + if (ftype === '24db') { + const { filter: lpf2, lfo: lfo2 } = filt(lpParams); + fxNodes['lpf'].push(lpf2); + fxNodes['lpf_lfo'].push(lfo2); + chain.connect(lpf2); + lfo2 && chain.audioNodes.push(lfo2); + } + } + + if (fx.hcutoff !== undefined) { + const hpMap = { + frequency: 'hcutoff', + q: 'hresonance', + attack: 'hpattack', + decay: 'hpdecay', + sustain: 'hpsustain', + release: 'hprelease', + env: 'hpenv', + anchor: 'fanchor', + model: 'ftype', + drive: 'drive', + rate: 'hprate', + sync: 'hpsync', + depth: 'hpdepth', + depthfrequency: 'hpdepthfrequency', + shape: 'hpshape', + dcoffset: 'hpdc', + skew: 'hpskew', + }; + const hpParams = pickAndRename(fx, hpMap); + hpParams.type = 'highpass'; + const { filter: hpf1, lfo: lfo1 } = filt(hpParams); + fxNodes['hpf'] = [hpf1]; + fxNodes['hpf_lfo'] = [lfo1]; + lfo1 && chain.audioNodes.push(lfo1); + chain.connect(hpf1); + if (ftype === '24db') { + const { filter: hpf2, lfo: lfo2 } = filt(hpParams); + fxNodes['hpf'].push(hpf2); + fxNodes['hpf_lfo'].push(lfo2); + chain.connect(hpf2); + lfo2 && chain.audioNodes.push(lfo2); + } + } + + if (fx.bandf !== undefined) { + const bpMap = { + frequency: 'bandf', + q: 'bandq', + attack: 'bpattack', + decay: 'bpdecay', + sustain: 'bpsustain', + release: 'bprelease', + env: 'bpenv', + anchor: 'fanchor', + model: 'ftype', + drive: 'drive', + rate: 'bprate', + sync: 'bpsync', + depth: 'bpdepth', + depthfrequency: 'bpdepthfrequency', + shape: 'bpshape', + dcoffset: 'bpdc', + skew: 'bpskew', + }; + const bpParams = pickAndRename(fx, bpMap); + bpParams.type = 'bandpass'; + const { filter: bpf1, lfo: lfo1 } = filt(bpParams); + fxNodes['bpf'] = [bpf1]; + fxNodes['bpf_lfo'] = [lfo1]; + chain.connect(bpf1); + lfo1 && chain.audioNodes.push(lfo1); + if (ftype === '24db') { + const { filter: bpf2, lfo: lfo2 } = filt(bpParams); + fxNodes['bpf'].push(bpf2); + fxNodes['bpf_lfo'].push(lfo2); + chain.connect(bpf2); + lfo2 && chain.audioNodes.push(lfo2); + } + } + + if (fx.vowel !== undefined) { + const vowelFilter = ac.createVowelFilter(fx.vowel); + fxNodes['vowel'] = [vowelFilter]; + chain.connect(vowelFilter); + } + + // effects + if (fx.coarse !== undefined) { + const coarseNode = getWorklet(ac, 'coarse-processor', { coarse: fx.coarse }); + fxNodes['coarse'] = [coarseNode]; + chain.connect(coarseNode); + } + if (fx.crush !== undefined) { + const crushNode = getWorklet(ac, 'crush-processor', { crush: fx.crush }); + fxNodes['crush'] = [crushNode]; + chain.connect(crushNode); + } + if (fx.shape !== undefined) { + const shapeNode = getWorklet(ac, 'shape-processor', { shape: fx.shape, postgain: shapevol }); + fxNodes['shape'] = [shapeNode]; + chain.connect(shapeNode); + } + if (fx.distort !== undefined) { + const distortNode = getDistortion(fx.distort, distortvol, distorttype); + fxNodes['distort'] = [distortNode]; + chain.connect(distortNode); + } + + let tremolo = fx.tremolo; + if (fx.tremolosync != null) { + tremolo = cps * fx.tremolosync; + } + + if (tremolo !== undefined) { + // Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload + // EX: a triangle waveform will clip like this /-\ when the depth is above 1 + const gain = Math.max(1 - tremolodepth, 0); + const amGain = new GainNode(ac, { gain }); + + const time = cycle / cps; + const lfo = getLfo(ac, { + skew: fx.tremoloskew ?? (fx.tremoloshape != null ? 0.5 : 1), + frequency: tremolo, + depth: tremolodepth, + time, + dcoffset: 0, + shape: fx.tremoloshape, + phaseoffset: fx.tremolophase, + min: 0, + max: 1, + curve: 1.5, + begin: t, + end: endWithRelease, + }); + fxNodes['tremolo'] = [lfo]; + fxNodes['tremolo_gain'] = [amGain]; + lfo.connect(amGain.gain); + chain.audioNodes.push(lfo); + chain.connect(amGain); + } + + if (fx.compressor !== undefined) { + const compressorNode = getCompressor( + ac, + fx.compressor, + fx.compressorRatio, + fx.compressorKnee, + fx.compressorAttack, + fx.compressorRelease, + ); + fxNodes['compressor'] = [compressorNode]; + chain.connect(compressorNode); + } + + // panning + if (fx.pan !== undefined) { + const panner = ac.createStereoPanner(); + panner.pan.value = 2 * fx.pan - 1; + chain.connect(panner); + } + // phaser + if (fx.phaserrate !== undefined && phaserdepth > 0) { + const { filterChain, lfo } = getPhaser( + t, + endWithRelease, + fx.phaserrate, + phaserdepth, + fx.phasercenter, + fx.phasersweep, + ); + fxNodes['phaser'] = [...filterChain]; + fxNodes['phaser_lfo'] = [lfo]; + filterChain.forEach((f) => chain.connect(f)); + chain.audioNodes.push(lfo); + } + // delay + if (delay > 0 && delaytime > 0 && delayfeedback > 0) { + const dry = gainNode(1); + delayfeedback = clamp(delayfeedback, 0, 0.98); + const delayNode = ac.createFeedbackDelay(1, delaytime, delayfeedback); + const wetDelay = gainNode(delay); + const dryDelay = gainNode(fx.dry ?? 1); + const sum = new GainNode(ac, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); + chain + .connect(dry) + .connect(dryDelay, delayNode) + .connectOne(1, wetDelay) // connect delayNode -> wetDelay + .connect(sum); + chain.audioNodes.push(delayNode.feedbackGain, delayNode.delayGain); + fxNodes['delay'] = [delayNode]; + fxNodes['delay_mix'] = [wetDelay]; + } + // reverb + if (fx.room > 0) { + let roomIR; + if (fx.ir !== undefined) { + let url; + let sample = getSound(fx.ir); + if (Array.isArray(sample)) { + url = sample.data.samples[fx.i % sample.data.samples.length]; + } else if (typeof sample === 'object') { + url = Object.values(sample.data.samples).flat()[fx.i % Object.values(sample.data.samples).length]; + } + roomIR = await loadBuffer(url, ac, fx.ir, 0); + } + const dry = gainNode(1); + const reverbNode = ac.createReverb( + fx.roomsize, + fx.roomfade, + fx.roomlp, + fx.roomdim, + roomIR, + fx.irspeed, + fx.irbegin, + ); + const wetReverb = gainNode(fx.room); + const dryReverb = gainNode(fx.dry ?? 1); + const sum = new GainNode(ac, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); + chain + .connect(dry) + .connect(dryReverb, reverbNode) + .connectOne(1, wetReverb) // connect reverbNode -> wetReverb + .connect(sum); + fxNodes['room'] = [reverbNode]; + fxNodes['room_mix'] = [wetReverb]; } } - if (value.hcutoff !== undefined) { - const hpMap = { - frequency: 'hcutoff', - q: 'hresonance', - attack: 'hpattack', - decay: 'hpdecay', - sustain: 'hpsustain', - release: 'hprelease', - env: 'hpenv', - anchor: 'fanchor', - model: 'ftype', - drive: 'drive', - rate: 'hprate', - sync: 'hpsync', - depth: 'hpdepth', - depthfrequency: 'hpdepthfrequency', - shape: 'hpshape', - dcoffset: 'hpdc', - skew: 'hpskew', - }; - const hpParams = pickAndRename(value, hpMap); - 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); - } - } - - if (value.bandf !== undefined) { - const bpMap = { - frequency: 'bandf', - q: 'bandq', - attack: 'bpattack', - decay: 'bpdecay', - sustain: 'bpsustain', - release: 'bprelease', - env: 'bpenv', - anchor: 'fanchor', - model: 'ftype', - drive: 'drive', - rate: 'bprate', - sync: 'bpsync', - depth: 'bpdepth', - depthfrequency: 'bpdepthfrequency', - shape: 'bpshape', - dcoffset: 'bpdc', - skew: 'bpskew', - }; - 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); - } - } - - if (vowel !== undefined) { - const vowelFilter = ac.createVowelFilter(vowel); - nodes['vowel'] = [vowelFilter]; - chain.push(vowelFilter); - } - - // effects - 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, postgain: shapevol }); - nodes['shape'] = [shapeNode]; - chain.push(shapeNode); - } - if (distort !== undefined) { - const distortNode = getDistortion(distort, distortvol, distorttype); - nodes['distort'] = [distortNode]; - chain.push(distortNode); - } - - if (tremolosync != null) { - tremolo = cps * tremolosync; - } - - if (value.wtPosSynced != null) { - value.wtPosRate /= cps; - } - - if (value.wtWarpSynced != null) { - value.wtWarpRate /= cps; - } - - if (tremolo !== undefined) { - // Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload - // EX: a triangle waveform will clip like this /-\ when the depth is above 1 - const gain = Math.max(1 - tremolodepth, 0); - const amGain = new GainNode(ac, { gain }); - - const time = cycle / cps; - const lfo = getLfo(ac, { - skew: tremoloskew ?? (tremoloshape != null ? 0.5 : 1), - frequency: tremolo, - depth: tremolodepth, - time, - dcoffset: 0, - shape: tremoloshape, - phaseoffset: tremolophase, - min: 0, - max: 1, - curve: 1.5, - begin: t, - end: endWithRelease, - }); - nodes['tremolo'] = [lfo]; - nodes['tremolo_gain'] = [amGain]; - lfo.connect(amGain.gain); - audioNodes.push(lfo); - chain.push(amGain); - } - - if (compressorThreshold !== undefined) { - const compressorNode = getCompressor( - ac, - compressorThreshold, - compressorRatio, - compressorKnee, - compressorAttack, - compressorRelease, - ); - nodes['compressor'] = [compressorNode]; - chain.push(compressorNode); - } - - // panning - if (pan !== undefined) { - const panner = ac.createStereoPanner(); - panner.pan.value = 2 * pan - 1; - chain.push(panner); - } - // phaser - if (phaserrate !== undefined && phaserdepth > 0) { - const { filterChain, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep); - nodes['phaser'] = [...filterChain]; - nodes['phaser_lfo'] = [lfo]; - chain.push(...filterChain); - audioNodes.push(lfo); + if (FXrelease !== undefined && FXrelease > release) { + const releaseNode = gainNode(1); + releaseNode.gain.setValueAtTime(1, end + release); + releaseNode.gain.linearRampToValueAtTime(0, endWithRelease); + chain.connect(releaseNode); } // last gain const post = new GainNode(ac, { gain: postgain }); - nodes['post'] = [post]; - chain.push(post); + nodes.main['post'] = [post]; + chain.connect(post); // delay if (delay > 0 && delaytime > 0 && delayfeedback > 0) { const delayNode = orbitBus.getDelay(delaytime, delayfeedback, t); - nodes['delay'] = [delayNode]; + nodes.main['delay'] = [delayNode]; const delaySend = orbitBus.sendDelay(post, delay); - nodes['delay_mix'] = [delaySend]; - audioNodes.push(delaySend); + nodes.main['delay_mix'] = [delaySend]; + chain.audioNodes.push(delaySend); } // reverb if (room > 0) { @@ -838,81 +913,92 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) roomIR = await loadBuffer(url, ac, ir, 0); } const roomNode = orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin); - nodes['room'] = [roomNode]; + nodes.main['room'] = [roomNode]; const reverbSend = orbitBus.sendReverb(post, room); - nodes['room_mix'] = [reverbSend]; - audioNodes.push(reverbSend); + nodes.main['room_mix'] = [reverbSend]; + chain.audioNodes.push(reverbSend); } if (bus != null) { const busNode = audioController.getBus(bus); const busSend = effectSend(post, busNode, busgain); - audioNodes.push(busSend); + chain.audioNodes.push(busSend); } if (djf != null) { const djfNode = orbitBus.getDjf(djf, t); - nodes['djf'] = [djfNode]; + nodes.main['djf'] = [djfNode]; } // analyser if (analyze && !(ac instanceof OfflineAudioContext)) { const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5)); const analyserSend = effectSend(post, analyserNode, 1); - audioNodes.push(analyserSend); + chain.audioNodes.push(analyserSend); } if (dry != null) { dry = applyGainCurve(dry); const dryGain = new GainNode(ac, { gain: dry }); - chain.push(dryGain); + chain.connect(dryGain); orbitBus.connectToOutput(dryGain); } else { orbitBus.connectToOutput(post); } - // connect chain elements together - chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); - audioNodes.push(...chain); - // finally, now that `nodes` is populated, set up modulators - if (value.lfo) { - for (const id of value.lfo.__ids) { - const params = value.lfo[id]; - const lfo = connectLFO( - id, - { - ...params, - cps, - cycle, - begin: t, - end: endWithRelease, - }, - nodes, - ); - lfo && audioNodes.push(lfo); + FX.forEach((fx, idx) => { + const key = idx === FX.length - 1 ? 'main' : idx; + if (fx.lfo) { + for (const id of fx.lfo.__ids) { + const params = fx.lfo[id]; + params.fxi ??= key; + const lfo = connectLFO( + id, + { + ...params, + cps, + cycle, + begin: t, + end: endWithRelease, + }, + nodes, + ); + lfo && chain.audioNodes.push(lfo); + } } - } - if (value.env) { - for (const id of value.env.__ids) { - const params = value.env[id]; - const env = connectEnvelope( - id, - { - ...params, - begin: t, - end: endWithRelease, - }, - nodes, - ); - env && audioNodes.push(env); + if (fx.env) { + for (const id of fx.env.__ids) { + const params = fx.env[id]; + params.fxi ??= key; + const env = connectEnvelope( + id, + { + ...params, + begin: t, + end: endWithRelease, + }, + nodes, + ); + env && chain.audioNodes.push(env); + } } - } - if (value.bmod) { - for (const id of value.bmod.__ids) { - const params = value.bmod[id]; - const { toCleanup } = connectBusModulator({ ...params, begin: t, end: endWithRelease }, nodes, controller); - audioNodes.push(...toCleanup); + if (fx.bmod) { + for (const id of fx.bmod.__ids) { + const params = fx.bmod[id]; + params.fxi ??= key; + const { toCleanup } = connectBusModulator({ ...params, begin: t, end: endWithRelease }, nodes, controller); + chain.audioNodes.push(...toCleanup); + } } - } + }); + webAudioTimeout( + ac, + () => { + chain.releaseNodes(); + activeSoundSources.delete(chainID); + }, + 0, + endWithRelease, + ); }; export const superdoughTrigger = (t, hap, ct, cps) => { From 882bcc513f12d62ab477bd336f09294ed528e2f4 Mon Sep 17 00:00:00 2001 From: Aria Date: Wed, 31 Dec 2025 15:17:06 -0600 Subject: [PATCH 127/130] Typos and some examples --- packages/core/pattern.mjs | 16 ++++++++ packages/superdough/modulators.mjs | 4 +- packages/superdough/superdough.mjs | 27 +++++++------ test/__snapshots__/examples.test.mjs.snap | 46 +++++++++++++++++++++++ 4 files changed, 79 insertions(+), 14 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index e6169399..f9c1852d 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3729,6 +3729,22 @@ export const phases = (list) => { * @name FX * @memberof Pattern * @returns Pattern + * @example + * $: s("[sbd ]*4").dec(.4) + * .FX( + * phaser(0.5).gain(2), + * bpf(800), + * distort(1.3), + * room(0.2), + * delay(0.5).gain(1.25), + * distort(0.3), + * ).fxr(1.7) // sets release time of effects (like delay) + * @example + * $: s("saw").fm(0.5) + * .delay(0.3) // outer effects are applied *last* + * .FX(coarse(4)) // first coarse + * .FX(lpf(500).lpe(4).lpa(1).lpd(2)) // then lpf + * .FX(distort(1)) // then distort */ Pattern.prototype.FX = function (...effects) { effects = effects.map(reify); diff --git a/packages/superdough/modulators.mjs b/packages/superdough/modulators.mjs index 2b6fcf33..ea906710 100644 --- a/packages/superdough/modulators.mjs +++ b/packages/superdough/modulators.mjs @@ -112,7 +112,7 @@ export const connectLFO = (id, params, nodeTracker) => { max, }; const lfoNode = getLfo(getAudioContext(), modParams); - nodeTracker[0][`lfo_${id}`] = [lfoNode]; + nodeTracker.main[`lfo_${id}`] = [lfoNode]; targetParams.forEach((t) => lfoNode.connect(t)); return lfoNode; }; @@ -134,7 +134,7 @@ export const connectEnvelope = (id, params, nodeTracker) => { decayCurve: dcurve, releaseCurve: rcurve, }); - nodeTracker[0][`env_${id}`] = [envNode]; + nodeTracker.main[`env_${id}`] = [envNode]; targetParams.forEach((t) => envNode.connect(t)); return envNode; }; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 9e9ffa12..7546bfab 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -196,6 +196,7 @@ let defaultDefaultValues = { fft: 8, tremolodepth: 1, tremolophase: 0, + release: 0.01, }; const defaultDefaultDefaultValues = Object.freeze({ ...defaultDefaultValues }); @@ -473,7 +474,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) duckattack, duckdepth, djf, - release = 0.01, + release = getDefaultValue('release'), dry, delay = getDefaultValue('delay'), delayfeedback = getDefaultValue('delayfeedback'), @@ -544,7 +545,18 @@ 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 = () => {}; + // 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) { @@ -566,7 +578,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) return; } - const chain = new Chain(sourceNode); // audio nodes that will be connected to each other sequentially + const chain = new Chain(sourceNode); // connection manager which tracks audio nodes for releasing FX = [...FX, value]; // run through the FX chain and then run through all FX outside of it as well for (let [idx, fx] of Object.entries(FX)) { const key = idx == FX.length - 1 ? 'main' : idx; @@ -990,15 +1002,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } } }); - webAudioTimeout( - ac, - () => { - chain.releaseNodes(); - activeSoundSources.delete(chainID); - }, - 0, - endWithRelease, - ); }; export const superdoughTrigger = (t, hap, ct, cps) => { diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 61ab1186..f6547bc6 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -565,6 +565,52 @@ exports[`runs examples > example "_euclidRot" example index 20 1`] = ` ] `; +exports[`runs examples > example "FX" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 1/8 → 1/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 1/4 → 3/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 3/8 → 1/2 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 1/2 → 5/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 5/8 → 3/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 3/4 → 7/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 7/8 → 1/1 | s:lt decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 1/1 → 9/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 9/8 → 5/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 5/4 → 11/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 11/8 → 3/2 | s:oh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 3/2 → 13/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 13/8 → 7/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 7/4 → 15/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 15/8 → 2/1 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 2/1 → 17/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 17/8 → 9/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 9/4 → 19/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 19/8 → 5/2 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 5/2 → 21/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 21/8 → 11/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 11/4 → 23/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 23/8 → 3/1 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 3/1 → 25/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 25/8 → 13/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 13/4 → 27/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 27/8 → 7/2 | s:lt decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 7/2 → 29/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 29/8 → 15/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 15/4 → 31/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", + "[ 31/8 → 4/1 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]", +] +`; + +exports[`runs examples > example "FX" example index 1 1`] = ` +[ + "[ 0/1 → 1/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]", + "[ 1/1 → 2/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]", + "[ 2/1 → 3/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]", + "[ 3/1 → 4/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]", +] +`; + exports[`runs examples > example "accelerate" example index 0 1`] = ` [ "[ 0/1 → 2/1 | s:sax accelerate:0 ]", From ebaf7624f98698d65e53f6706a62b8af8ac3e0aa Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 1 Jan 2026 12:02:54 -0600 Subject: [PATCH 128/130] Fix lfos for vowels, phasers; fix ir index; more informative error message --- packages/superdough/modulators.mjs | 5 ++++- packages/superdough/superdough.mjs | 13 +++++++------ packages/superdough/superdoughdata.mjs | 2 +- packages/superdough/vowel.mjs | 17 +++++++++++------ 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/superdough/modulators.mjs b/packages/superdough/modulators.mjs index ea906710..7d119a06 100644 --- a/packages/superdough/modulators.mjs +++ b/packages/superdough/modulators.mjs @@ -62,7 +62,10 @@ const getTargetParamsForControl = (control, nodes, subControl) => { const lookupKey = subControl ? `${control}_${subControl}` : control; const targetInfo = getControlData(lookupKey) ?? getControlData(control); if (!targetInfo) { - errorLogger(new Error(`Could not find control data for target '${control}'`), 'superdough'); + errorLogger( + new Error(`Could not find control data for target '${control}'. It may not be modulatable.`), + 'superdough', + ); return { targetParams: [], paramName: control }; } const paramName = targetInfo.param; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 7546bfab..2e46b2d7 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -597,6 +597,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) delaysync = getDefaultValue('delaysync'), delaytime, stretch = getDefaultValue('stretch'), + i = getDefaultValue('i'), } = fx; gain = applyGainCurve(nanFallback(gain, 1)); shapevol = applyGainCurve(shapevol); @@ -744,9 +745,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } if (fx.vowel !== undefined) { - const vowelFilter = ac.createVowelFilter(fx.vowel); - fxNodes['vowel'] = [vowelFilter]; - chain.connect(vowelFilter); + const vowelNode = ac.createVowelFilter(fx.vowel); + fxNodes['vowel'] = vowelNode.filters; + chain.connect(vowelNode); } // effects @@ -839,7 +840,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) chain.audioNodes.push(lfo); } // delay - if (delay > 0 && delaytime > 0 && delayfeedback > 0) { + if (key !== 'main' && delay > 0 && delaytime > 0 && delayfeedback > 0) { const dry = gainNode(1); delayfeedback = clamp(delayfeedback, 0, 0.98); const delayNode = ac.createFeedbackDelay(1, delaytime, delayfeedback); @@ -856,7 +857,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) fxNodes['delay_mix'] = [wetDelay]; } // reverb - if (fx.room > 0) { + if (key !== 'main' && fx.room > 0) { let roomIR; if (fx.ir !== undefined) { let url; @@ -864,7 +865,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) if (Array.isArray(sample)) { url = sample.data.samples[fx.i % sample.data.samples.length]; } else if (typeof sample === 'object') { - url = Object.values(sample.data.samples).flat()[fx.i % Object.values(sample.data.samples).length]; + url = Object.values(sample.data.samples).flat()[i % Object.values(sample.data.samples).length]; } roomIR = await loadBuffer(url, ac, fx.ir, 0); } diff --git a/packages/superdough/superdoughdata.mjs b/packages/superdough/superdoughdata.mjs index 2dac194f..e43df9ee 100644 --- a/packages/superdough/superdoughdata.mjs +++ b/packages/superdough/superdoughdata.mjs @@ -87,7 +87,7 @@ const CONTROL_TARGETS = { compressorRelease: { node: 'compressor', param: 'release' }, // PHASER - phaserrate: { node: 'phaser_lfo', param: 'rate' }, + phaserrate: { node: 'phaser_lfo', param: 'frequency' }, phasersweep: { node: 'phaser_lfo', param: 'depth' }, phasercenter: { node: 'phaser', param: 'frequency' }, phaserdepth: { node: 'phaser', param: 'Q' }, diff --git a/packages/superdough/vowel.mjs b/packages/superdough/vowel.mjs index bfc1c9fc..8ecbf9d5 100644 --- a/packages/superdough/vowel.mjs +++ b/packages/superdough/vowel.mjs @@ -1,3 +1,5 @@ +import { releaseAudioNode } from './helpers.mjs'; + // credits to webdirt: https://github.com/dktr0/WebDirt/blob/41342e81d6ad694a2310d491fef7b7e8b0929efe/js-src/Graph.js#L597 export var vowelFormant = { a: { freqs: [660, 1120, 2750, 3000, 3350], gains: [1, 0.5012, 0.0708, 0.0631, 0.0126], qs: [80, 90, 120, 130, 140] }, @@ -46,7 +48,8 @@ if (typeof GainNode !== 'undefined') { } const { gains, qs, freqs } = vowelFormant[letter]; this.makeupGain = ac.createGain(); - this.audioNodes = []; + this.filters = []; + this.gains = []; for (let i = 0; i < 5; i++) { const gain = ac.createGain(); gain.gain.value = gains[i]; @@ -56,9 +59,9 @@ if (typeof GainNode !== 'undefined') { filter.frequency.value = freqs[i]; super.connect(filter); filter.connect(gain); - this.audioNodes.push(filter); + this.filters.push(filter); gain.connect(this.makeupGain); - this.audioNodes.push(gain); + this.gains.push(gain); } this.makeupGain.gain.value = 8; // how much makeup gain to add? return this; @@ -67,11 +70,13 @@ if (typeof GainNode !== 'undefined') { this.makeupGain.connect(target); } disconnect() { - this.makeupGain.disconnect(); - this.audioNodes.forEach((n) => n.disconnect()); + releaseAudioNode(this.makeupGain); + this.filters.forEach(releaseAudioNode); + this.gains.forEach(releaseAudioNode); super.disconnect(); this.makeupGain = null; - this.audioNodes = null; + this.filters = null; + this.gains = null; } } From e54b9c3a32c245cd06392ba908c9974c51e2cd3b Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 1 Jan 2026 13:07:38 -0600 Subject: [PATCH 129/130] Fix formatting of docstring --- packages/core/pattern.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index f9c1852d..02cf9188 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -3723,8 +3723,8 @@ export const phases = (list) => { }; /** - * Establishes an FX chain. Can be called by chaining .FX().FX().. - * calls and/or in a single .FX(, , ..) call. The , .. are _patterns_ which + * Establishes an FX chain. Can be called by chaining .FX(fx1).FX(fx2).. + * calls and/or in a single .FX(fx1, fx2, ..) call. The fx1, .. are _patterns_ which * establish the controls of the given effect. See examples. * @name FX * @memberof Pattern From 7b1e02e8cfbc71a00777d597bb6ec2089b0cbbc4 Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 2 Jan 2026 11:43:27 -0600 Subject: [PATCH 130/130] Make pan modulatable --- packages/superdough/superdough.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 2e46b2d7..c2515e32 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -821,6 +821,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // panning if (fx.pan !== undefined) { const panner = ac.createStereoPanner(); + fxNodes['pan'] = [panner]; panner.pan.value = 2 * fx.pan - 1; chain.connect(panner); }