From fe18aa770d566cccf7cf57618e6e00e3d4520d0b Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 23 Jul 2025 18:34:34 -0400 Subject: [PATCH 1/7] test --- packages/core/controls.mjs | 53 ++++--- packages/core/logger.mjs | 2 +- packages/superdough/superdough.mjs | 108 +++++++++----- test/__snapshots__/examples.test.mjs.snap | 171 ++++++++++++++++++++++ 4 files changed, 276 insertions(+), 58 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 326343e10..631896e94 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -526,28 +526,39 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape'); * */ -// TODO: SUPRADOUGH implement post orbit "pump" sidechain effect -// /** -// * modulate the amplitude of an orbit to create a "sidechain" like effect -// * -// * @name pump -// * @param {number | Pattern} speed modulation speed in cycles -// * @example -// * note("{f g c d}%16").s("sawtooth").pump(".25:.75") -// * -// */ -// export const { pump } = registerControl(['pump', 'pumpdepth']); +/** + * modulate the amplitude of an orbit to create a "sidechain" like effect + * + * @name duckorbit + * @param {number | Pattern} orbit target orbit + * @example + * stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1)) + * + */ +export const { duck } = registerControl(['duckorbit', 'duckattack', 'duckdepth'], 'duck'); -// /** -// * modulate the amplitude of an orbit to create a "sidechain" like effect -// * -// * @name pumpdepth -// * @param {number | Pattern} depth depth of modulation from 0 to 1 -// * @example -// * note("{f g c d}%16").s("sawtooth").pump(".25").depth("<.25 .5 .75 1>") -// * -// */ -// export const { pumpdepth } = registerControl('pumpdepth'); +/** + * the amount of ducking applied to target orbit + * + * @name duckdepth + * @param {number | Pattern} depth depth of modulation from 0 to 1 + * @example + * stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth("<1 .9 .6 0>")) + * + */ + +export const { duckdepth } = registerControl('duckdepth'); + +/** + * the attack time of the duck effect + * + * @name duckattack + * @param {number | Pattern} time + * @example + * stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1)) + * + */ +export const { duckattack } = registerControl('duckattack', 'duckatt'); export const { drive } = registerControl('drive'); diff --git a/packages/core/logger.mjs b/packages/core/logger.mjs index 635505c59..4f2002319 100644 --- a/packages/core/logger.mjs +++ b/packages/core/logger.mjs @@ -6,7 +6,7 @@ let debounce = 1000, export function errorLogger(e, origin = 'cyclist') { //TODO: add some kind of debug flag that enables this while in dev mode - // console.error(e) + // console.error(e); logger(`[${origin}] error: ${e.message}`); } diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 7d90eb15d..c3054c1ba 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -13,6 +13,7 @@ import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; +import { errorLogger } from '@strudel/core'; export const DEFAULT_MAX_POLYPHONY = 128; const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; @@ -331,16 +332,18 @@ function getDelay(orbit, delaytime, delayfeedback, t, channels) { //logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`); } delayfeedback = clamp(delayfeedback, 0, 0.98); - if (!delays[orbit]) { + if (!orbits[orbit].delayNode) { const ac = getAudioContext(); const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback); dly.start?.(t); // for some reason, this throws when audion extension is installed.. - connectToDestination(dly, channels); - delays[orbit] = dly; + connectToOrbit(dly, orbit); + orbits[orbit].delayNode = dly; } - delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t); - delays[orbit].feedback.value !== delayfeedback && delays[orbit].feedback.setValueAtTime(delayfeedback, t); - return delays[orbit]; + orbits[orbit].delayNode.delayTime.value !== delaytime && + orbits[orbit].delayNode.delayTime.setValueAtTime(delaytime, t); + orbits[orbit].delayNode.feedback.value !== delayfeedback && + orbits[orbit].delayNode.feedback.setValueAtTime(delayfeedback, t); + return orbits[orbit].delayNode; } export function getLfo(audioContext, begin, end, properties = {}) { @@ -365,22 +368,6 @@ export function getLfo(audioContext, begin, end, properties = {}) { return getWorklet(audioContext, 'lfo-processor', lfoprops); } -export function getSyncedLfo(audioContext, time, end, cps, cycle, properties = {}) { - const frequency = cycle / cps; - - return getWorklet(audioContext, 'lfo-processor', { - frequency, - depth: 1, - skew: 0, - phaseoffset: 0, - time, - end, - shape: 1, - dcoffset: -0.5, - ...properties, - }); -} - function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) { const ac = getAudioContext(); const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 }); @@ -412,31 +399,63 @@ function getFilterType(ftype) { return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype; } -let reverbs = {}; +//type orbit { +// gain: number, +// reverb: reverbNode +// delay: +//} +const orbits = {}; +function connectToOrbit(node, orbit) { + if (orbits[orbit] == null) { + errorLogger(new Error('target orbit does not exist'), 'superdough'); + } + node.connect(orbits[orbit].gain); +} + +function setOrbit(audioContext, orbit, channels) { + if (orbits[orbit] == null) { + orbits[orbit] = { + gain: new GainNode(audioContext, { gain: 1 }), + }; + connectToDestination(orbits[orbit].gain, channels); + } +} +function duckOrbit(target, t, attacktime = 0.1, duckdepth = 1) { + if (orbits[target] == null) { + errorLogger(new Error('duck target orbit does not exist'), 'superdough'); + } + + orbits[target].gain.gain.cancelAndHoldAtTime(t); + const currVal = orbits[target].gain.gain.value; + orbits[target].gain.gain.setValueAtTime(currVal, t); + orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - duckdepth, 0.01, currVal), t + 0.002); + orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime)); +} + let hasChanged = (now, before) => now !== undefined && now !== before; -function getReverb(orbit, duration, fade, lp, dim, ir, channels) { +function getReverb(orbit, duration, fade, lp, dim, ir) { // If no reverb has been created for a given orbit, create one - if (!reverbs[orbit]) { + if (!orbits[orbit].reverbNode) { const ac = getAudioContext(); const reverb = ac.createReverb(duration, fade, lp, dim, ir); - connectToDestination(reverb, channels); - reverbs[orbit] = reverb; + connectToOrbit(reverb, orbit); + orbits[orbit].reverbNode = reverb; } if ( - hasChanged(duration, reverbs[orbit].duration) || - hasChanged(fade, reverbs[orbit].fade) || - hasChanged(lp, reverbs[orbit].lp) || - hasChanged(dim, reverbs[orbit].dim) || - reverbs[orbit].ir !== ir + hasChanged(duration, orbits[orbit].reverbNode.duration) || + hasChanged(fade, orbits[orbit].reverbNode.fade) || + hasChanged(lp, orbits[orbit].reverbNode.lp) || + hasChanged(dim, orbits[orbit].reverbNode.dim) || + orbits[orbit].reverbNode.ir !== ir ) { // only regenerate when something has changed // avoids endless regeneration on things like // stack(s("a"), s("b").rsize(8)).room(.5) // this only works when args may stay undefined until here // setting default values breaks this - reverbs[orbit].generate(duration, fade, lp, dim, ir); + orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir); } - return reverbs[orbit]; + return orbits[orbit].reverbNode; } export let analysers = {}, @@ -533,6 +552,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) gain = getDefaultValue('gain'), postgain = getDefaultValue('postgain'), density = getDefaultValue('density'), + duckorbit, + duckattack, + duckdepth, // filters fanchor = getDefaultValue('fanchor'), drive = 0.69, @@ -571,6 +593,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) coarse, crush, + dry, shape, shapevol = getDefaultValue('shapevol'), distort, @@ -604,7 +627,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) const orbitChannels = mapChannelNumbers( multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'), ); + const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels; + setOrbit(ac, orbit, channels, t, cycle, cps); + + if (duckorbit != null) { + duckOrbit(duckorbit, t, duckattack, duckdepth); + } gain = applyGainCurve(nanFallback(gain, 1)); postgain = applyGainCurve(postgain); @@ -669,7 +698,7 @@ 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 + let 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 })); @@ -791,7 +820,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // last gain const post = new GainNode(ac, { gain: postgain }); chain.push(post); - connectToDestination(post, channels); // delay let delaySend; @@ -826,6 +854,14 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) analyserSend = effectSend(post, analyserNode, 1); audioNodes.push(analyserSend); } + if (dry != null) { + dry = applyGainCurve(dry); + const dryGain = new GainNode(ac, { gain: dry }); + chain.push(dryGain); + connectToOrbit(dryGain, orbit); + } else { + connectToOrbit(post, orbit); + } // connect chain elements together chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index e6bfa44aa..a5dc0f415 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3036,6 +3036,177 @@ exports[`runs examples > example "dry" example index 0 1`] = ` ] `; +exports[`runs examples > example "duckattack" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 0/1 → 1/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/8 → 1/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/4 → 3/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 3/8 → 1/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/2 → 5/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 5/8 → 3/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 3/4 → 7/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 7/8 → 1/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 1/1 → 9/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 9/8 → 5/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 5/4 → 11/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 11/8 → 3/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 3/2 → 13/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 13/8 → 7/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 7/4 → 15/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 15/8 → 2/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 2/1 → 17/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 17/8 → 9/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 9/4 → 19/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 19/8 → 5/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 5/2 → 21/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 21/8 → 11/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 11/4 → 23/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 23/8 → 3/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 3/1 → 25/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 25/8 → 13/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 13/4 → 27/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 27/8 → 7/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 7/2 → 29/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 29/8 → 15/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 15/4 → 31/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 31/8 → 4/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", +] +`; + +exports[`runs examples > example "duckdepth" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 0/1 → 1/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/8 → 1/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/4 → 3/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 3/8 → 1/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/2 → 5/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 5/8 → 3/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 3/4 → 7/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 7/8 → 1/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]", + "[ 1/1 → 9/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 9/8 → 5/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]", + "[ 5/4 → 11/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 11/8 → 3/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]", + "[ 3/2 → 13/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 13/8 → 7/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]", + "[ 7/4 → 15/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]", + "[ 15/8 → 2/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]", + "[ 2/1 → 17/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 17/8 → 9/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]", + "[ 9/4 → 19/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 19/8 → 5/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]", + "[ 5/2 → 21/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 21/8 → 11/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]", + "[ 11/4 → 23/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]", + "[ 23/8 → 3/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]", + "[ 3/1 → 25/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 25/8 → 13/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]", + "[ 13/4 → 27/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 27/8 → 7/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]", + "[ 7/2 → 29/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 29/8 → 15/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]", + "[ 15/4 → 31/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]", + "[ 31/8 → 4/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", +] +`; + +exports[`runs examples > example "duckorbit" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 0/1 → 1/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/8 → 1/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/4 → 3/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 3/8 → 1/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/2 → 5/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 5/8 → 3/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 3/4 → 7/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 7/8 → 1/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/1 → 9/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 9/8 → 5/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 5/4 → 11/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 11/8 → 3/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 3/2 → 13/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 13/8 → 7/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 7/4 → 15/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 15/8 → 2/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 2/1 → 17/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 17/8 → 9/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 9/4 → 19/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 19/8 → 5/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 5/2 → 21/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 21/8 → 11/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 11/4 → 23/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 23/8 → 3/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 3/1 → 25/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 25/8 → 13/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 13/4 → 27/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 27/8 → 7/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 7/2 → 29/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 29/8 → 15/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 15/4 → 31/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 31/8 → 4/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", +] +`; + exports[`runs examples > example "duration" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c s:piano duration:0.5 ]", From 2415210be6e02c46e3c09c661598d21103b1371f Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 23 Jul 2025 18:41:20 -0400 Subject: [PATCH 2/7] fix lint --- packages/superdough/logger.mjs | 6 ++++++ packages/superdough/superdough.mjs | 12 +++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/superdough/logger.mjs b/packages/superdough/logger.mjs index a20af1b3c..b3c9c34f3 100644 --- a/packages/superdough/logger.mjs +++ b/packages/superdough/logger.mjs @@ -1,5 +1,11 @@ let log = (msg) => console.log(msg); +export function errorLogger(e, origin = 'cyclist') { + //TODO: add some kind of debug flag that enables this while in dev mode + // console.error(e); + logger(`[${origin}] error: ${e.message}`); +} + export const logger = (...args) => log(...args); export const setLogger = (fn) => { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index c3054c1ba..a98f55f3e 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -11,9 +11,8 @@ import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util import workletsUrl from './worklets.mjs?audioworklet'; import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs'; import { map } from 'nanostores'; -import { logger } from './logger.mjs'; +import { logger, errorLogger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; -import { errorLogger } from '@strudel/core'; export const DEFAULT_MAX_POLYPHONY = 128; const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; @@ -401,10 +400,10 @@ function getFilterType(ftype) { //type orbit { // gain: number, -// reverb: reverbNode -// delay: +// reverbNode: reverbNode +// delayNode: //} -const orbits = {}; +let orbits = {}; function connectToOrbit(node, orbit) { if (orbits[orbit] == null) { errorLogger(new Error('target orbit does not exist'), 'superdough'); @@ -498,8 +497,7 @@ function effectSend(input, effect, wet) { } export function resetGlobalEffects() { - delays = {}; - reverbs = {}; + orbits = {}; analysers = {}; analysersData = {}; } From 51f0bab1f84922474354d4b289a12bdd0aeb1dd5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 26 Jul 2025 12:54:59 -0400 Subject: [PATCH 3/7] duckarray --- packages/core/controls.mjs | 2 +- packages/superdough/superdough.mjs | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 631896e94..741b66488 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -535,7 +535,7 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape'); * stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1)) * */ -export const { duck } = registerControl(['duckorbit', 'duckattack', 'duckdepth'], 'duck'); +export const { duck } = registerControl('duckorbit', 'duck'); /** * the amount of ducking applied to target orbit diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index a98f55f3e..011326c6a 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -419,16 +419,20 @@ function setOrbit(audioContext, orbit, channels) { connectToDestination(orbits[orbit].gain, channels); } } -function duckOrbit(target, t, attacktime = 0.1, duckdepth = 1) { - if (orbits[target] == null) { - errorLogger(new Error('duck target orbit does not exist'), 'superdough'); - } +function duckOrbit(targetOrbit, t, attacktime = 0.1, duckdepth = 1) { + const targetArr = [targetOrbit].flat(); - orbits[target].gain.gain.cancelAndHoldAtTime(t); - const currVal = orbits[target].gain.gain.value; - orbits[target].gain.gain.setValueAtTime(currVal, t); - orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - duckdepth, 0.01, currVal), t + 0.002); - orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime)); + targetArr.forEach((target) => { + if (orbits[target] == null) { + errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough'); + return; + } + orbits[target].gain.gain.cancelAndHoldAtTime(t); + const currVal = orbits[target].gain.gain.value; + orbits[target].gain.gain.setValueAtTime(currVal, t); + orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - Math.pow(duckdepth, 0.5), 0.01, currVal), t + 0.002); + orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime)); + }); } let hasChanged = (now, before) => now !== undefined && now !== before; From c2b8cc4b824c31590ffdfd7f7dda4f7032621264 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 1 Aug 2025 23:29:50 -0400 Subject: [PATCH 4/7] webtimeout --- packages/superdough/helpers.mjs | 3 ++- packages/superdough/superdough.mjs | 22 ++++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 6bde69373..8a32fbf41 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -206,7 +206,8 @@ export function getVibratoOscillator(param, value, t) { // ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities // a bit of a hack, but it works very well :) export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { - const constantNode = audioContext.createConstantSource(); + const constantNode = new ConstantSourceNode(audioContext); + constantNode.start(startTime); constantNode.stop(stopTime); constantNode.onended = () => { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 011326c6a..e1c1b30ea 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 } from './helpers.mjs'; +import { createFilter, gainNode, getCompressor, getWorklet, webAudioTimeout } from './helpers.mjs'; import { map } from 'nanostores'; import { logger, errorLogger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; @@ -419,7 +419,7 @@ function setOrbit(audioContext, orbit, channels) { connectToDestination(orbits[orbit].gain, channels); } } -function duckOrbit(targetOrbit, t, attacktime = 0.1, duckdepth = 1) { +function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1) { const targetArr = [targetOrbit].flat(); targetArr.forEach((target) => { @@ -427,11 +427,17 @@ function duckOrbit(targetOrbit, t, attacktime = 0.1, duckdepth = 1) { errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough'); return; } - orbits[target].gain.gain.cancelAndHoldAtTime(t); - const currVal = orbits[target].gain.gain.value; - orbits[target].gain.gain.setValueAtTime(currVal, t); - orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - Math.pow(duckdepth, 0.5), 0.01, currVal), t + 0.002); - orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime)); + webAudioTimeout( + audioContext, + () => { + orbits[target].gain.gain.cancelScheduledValues(t); + const currVal = orbits[target].gain.gain.value; + orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - Math.pow(duckdepth, 0.5), 0.01, currVal), t + 0.002); + orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime)); + }, + 0, + t, + ); }); } @@ -634,7 +640,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) setOrbit(ac, orbit, channels, t, cycle, cps); if (duckorbit != null) { - duckOrbit(duckorbit, t, duckattack, duckdepth); + duckOrbit(ac, duckorbit, t, duckattack, duckdepth); } gain = applyGainCurve(nanFallback(gain, 1)); From a20468b85f2fc8ccb869f2d5bf13c36929e00531 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 16 Aug 2025 23:19:34 -0400 Subject: [PATCH 5/7] minor changes --- packages/superdough/superdough.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index e1c1b30ea..1dd73fe03 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -401,7 +401,7 @@ function getFilterType(ftype) { //type orbit { // gain: number, // reverbNode: reverbNode -// delayNode: +// delayNode: delayNode //} let orbits = {}; function connectToOrbit(node, orbit) { @@ -706,7 +706,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) logger('[webaudio] skip hap: still loading', ac.currentTime - t); return; } - let chain = []; // audio nodes that will be connected to each other sequentially + 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 })); From 5ff59a878d8c48da0b803ae0d6e493fd48e18d18 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 16 Aug 2025 23:38:03 -0400 Subject: [PATCH 6/7] fix lookahead --- packages/core/controls.mjs | 3 ++- packages/superdough/superdough.mjs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 174c6ecfd..e6181d453 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -547,7 +547,8 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape'); * @name duckorbit * @param {number | Pattern} orbit target orbit * @example - * stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1)) + * $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2) + * $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1) * */ export const { duck } = registerControl('duckorbit', 'duck'); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 1dd73fe03..eb1466d91 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -432,11 +432,11 @@ function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1 () => { orbits[target].gain.gain.cancelScheduledValues(t); const currVal = orbits[target].gain.gain.value; - orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - Math.pow(duckdepth, 0.5), 0.01, currVal), t + 0.002); + orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - Math.pow(duckdepth, 0.5), 0.01, currVal), t); orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime)); }, 0, - t, + t - 0.01, ); }); } From 018649c4f3759d5e315cefd1295848a608192c6b Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 16 Aug 2025 23:42:14 -0400 Subject: [PATCH 7/7] fix test --- test/__snapshots__/examples.test.mjs.snap | 32 ----------------------- 1 file changed, 32 deletions(-) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 16369872d..bb0489a91 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3153,57 +3153,25 @@ exports[`runs examples > example "duckdepth" example index 0 1`] = ` exports[`runs examples > example "duckorbit" example index 0 1`] = ` [ "[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 0/1 → 1/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", - "[ 1/8 → 1/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", "[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 1/4 → 3/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", - "[ 3/8 → 1/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", "[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 1/2 → 5/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", - "[ 5/8 → 3/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", "[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 3/4 → 7/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", "[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 7/8 → 1/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", "[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 1/1 → 9/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", - "[ 9/8 → 5/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", "[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 5/4 → 11/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", - "[ 11/8 → 3/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", "[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 3/2 → 13/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", - "[ 13/8 → 7/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", "[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 7/4 → 15/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", "[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 15/8 → 2/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", "[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 2/1 → 17/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", - "[ 17/8 → 9/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", "[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 9/4 → 19/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", - "[ 19/8 → 5/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", "[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 5/2 → 21/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", - "[ 21/8 → 11/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", "[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 11/4 → 23/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", "[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 23/8 → 3/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", "[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 3/1 → 25/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", - "[ 25/8 → 13/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", "[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 13/4 → 27/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", - "[ 27/8 → 7/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", "[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 7/2 → 29/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", - "[ 29/8 → 15/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", "[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 15/4 → 31/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", "[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", - "[ 31/8 → 4/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", ] `;