From fef50e654c0ef9827a137ba1229790a1c4c67de1 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 27 Jun 2025 02:38:43 -0400 Subject: [PATCH 01/65] working --- website/public/EmuSP12.json | 69 ++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 16 deletions(-) diff --git a/website/public/EmuSP12.json b/website/public/EmuSP12.json index b129ee57a..102baf0b8 100644 --- a/website/public/EmuSP12.json +++ b/website/public/EmuSP12.json @@ -1,17 +1,54 @@ { -"_base": "https://raw.githubusercontent.com/ritchse/tidal-drum-machines/main/machines/EmuSP12/", -"bd": ["emusp12-bd/Bassdrum-01.wav","emusp12-bd/Bassdrum-02.wav","emusp12-bd/Bassdrum-03.wav","emusp12-bd/Bassdrum-04.wav","emusp12-bd/Bassdrum-05.wav","emusp12-bd/Bassdrum-06.wav","emusp12-bd/Bassdrum-07.wav","emusp12-bd/Bassdrum-08.wav","emusp12-bd/Bassdrum-09.wav","emusp12-bd/Bassdrum-10.wav","emusp12-bd/Bassdrum-11.wav","emusp12-bd/Bassdrum-12.wav","emusp12-bd/Bassdrum-13.wav","emusp12-bd/Bassdrum-14.wav"], -"cb": ["emusp12-cb/Cowbell.wav"], -"cp": ["emusp12-cp/Clap.wav"], -"cr": ["emusp12-cr/Crash.wav"], -"hh": ["emusp12-hh/Hat Closed-01.wav","emusp12-hh/Hat Closed-02.wav"], -"ht": ["emusp12-ht/Tom H-01.wav","emusp12-ht/Tom H-02.wav","emusp12-ht/Tom H-03.wav","emusp12-ht/Tom H-04.wav","emusp12-ht/Tom H-05.wav","emusp12-ht/Tom H-06.wav"], -"lt": ["emusp12-lt/Tom L-01.wav","emusp12-lt/Tom L-02.wav","emusp12-lt/Tom L-03.wav","emusp12-lt/Tom L-04.wav","emusp12-lt/Tom L-05.wav","emusp12-lt/Tom L-06.wav"], -"misc": ["emusp12-misc/Metal-01.wav","emusp12-misc/Metal-02.wav","emusp12-misc/Metal-03.wav","emusp12-misc/Scratch.wav","emusp12-misc/Shot-01.wav","emusp12-misc/Shot-02.wav","emusp12-misc/Shot-03.wav"], -"mt": ["emusp12-mt/Tom M-01.wav","emusp12-mt/Tom M-02.wav","emusp12-mt/Tom M-03.wav","emusp12-mt/Tom M-05.wav"], -"oh": ["emusp12-oh/Hhopen1.wav"], -"perc": ["emusp12-perc/Blow1.wav"], -"rd": ["emusp12-rd/Ride.wav"], -"rim": ["emusp12-rim/zRim Shot-01.wav","emusp12-rim/zRim Shot-02.wav"], -"sd": ["emusp12-sd/Snaredrum-01.wav","emusp12-sd/Snaredrum-02.wav","emusp12-sd/Snaredrum-03.wav","emusp12-sd/Snaredrum-04.wav","emusp12-sd/Snaredrum-05.wav","emusp12-sd/Snaredrum-06.wav","emusp12-sd/Snaredrum-07.wav","emusp12-sd/Snaredrum-08.wav","emusp12-sd/Snaredrum-09.wav","emusp12-sd/Snaredrum-10.wav","emusp12-sd/Snaredrum-11.wav","emusp12-sd/Snaredrum-12.wav","emusp12-sd/Snaredrum-13.wav","emusp12-sd/Snaredrum-14.wav","emusp12-sd/Snaredrum-15.wav","emusp12-sd/Snaredrum-16.wav","emusp12-sd/Snaredrum-17.wav","emusp12-sd/Snaredrum-18.wav","emusp12-sd/Snaredrum-19.wav","emusp12-sd/Snaredrum-20.wav","emusp12-sd/Snaredrum-21.wav"] -} + "_base": "https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/", + "bd": [ + "bd/10_bd_switchangel.wav", + "bd/11_bd_mot4i.wav", + "bd/12_bd_mot4i.wav", + "bd/13_bd_mot4i.wav", + "bd/14_bd_switchangel.wav", + "bd/15_bd_switchangel.wav", + "bd/16_bd_switchangel.wav" + ], + "breaks": [ + "breaks/10_break_amen_pprocessed.wav" + ], + "cp": [ + "cp/10_cp_switchangel.wav", + "cp/11_cp_mot4i.wav" + ], + "cr": [ + "cr/10_cr_mot4i.wav" + ], + "hh": [ + "hh/10_hh_mot4i.wav", + "hh/11_hh_switchangel.wav", + "hh/12_hh_switchangel.wav", + "hh/13_hh_mot4i.wav" + ], + "ht": [ + "ht/10_ht_mot4i.wav" + ], + "lt": [ + "lt/10_lt_mot4i.wav" + ], + "mt": [ + "mt/10_mt_mot4i.wav" + ], + "oh": [ + "oh/10_oh_switchangel.wav", + "oh/11_oh_switchangel.wav", + "oh/12_oh_switchangel.wav" + ], + "perc": [ + "perc/10_perc_switchangel.wav" + ], + "rim": [ + "rim/10_rim_switch_angel.wav" + ], + "sd": [ + "sd/10_sd_switchangel_3.wav", + "sd/11_sd_switchangel_2.wav", + "sd/12_sd_switchangel_2.wav", + "sd/13_sd.wav" + ] + } \ No newline at end of file From 2dd528134a37662f7655721b8b7f69adaa0b9095 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 27 Jun 2025 02:52:04 -0400 Subject: [PATCH 02/65] fix json --- packages/repl/prebake.mjs | 2 +- website/public/{EmuSP12.json => uzudrumkit.json} | 0 website/src/repl/prebake.mjs | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename website/public/{EmuSP12.json => uzudrumkit.json} (100%) diff --git a/packages/repl/prebake.mjs b/packages/repl/prebake.mjs index dd0023fb5..5e524bbc8 100644 --- a/packages/repl/prebake.mjs +++ b/packages/repl/prebake.mjs @@ -36,7 +36,7 @@ export async function prebake() { samples(`${ds}/tidal-drum-machines.json`), samples(`${ds}/piano.json`), samples(`${ds}/Dirt-Samples.json`), - samples(`${ds}/EmuSP12.json`), + samples(`${ds}/uzudrumkit.json`), samples(`${ds}/vcsl.json`), samples(`${ds}/mridangam.json`), ]); diff --git a/website/public/EmuSP12.json b/website/public/uzudrumkit.json similarity index 100% rename from website/public/EmuSP12.json rename to website/public/uzudrumkit.json diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index 68befdd96..21034dc66 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -28,7 +28,7 @@ export async function prebake() { prebake: true, tag: 'drum-machines', }), - samples(`${baseNoTrailing}/EmuSP12.json`, undefined, { prebake: true, tag: 'drum-machines' }), + samples(`${baseNoTrailing}/uzudrumkit.json`, undefined, { prebake: true, tag: 'drum-machines' }), samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }), samples( { From c8cf1dc712847d9802b3a527ed337a8e75303943 Mon Sep 17 00:00:00 2001 From: Chandler Abraham Date: Tue, 22 Jul 2025 21:49:40 -0700 Subject: [PATCH 03/65] fix(midi): ensure midin initializes device state correctly The midin function only initialized the refs object for a device on the initial MIDI setup. This caused an error if a second MIDI device was connected in the same session, as its refs object would not exist, leading to a 'cannot read properties of undefined' error when accessed. This commit fixes the issue by ensuring that the refs object for a given MIDI input is initialized every time is called, not just on the first call. --- packages/midi/midi.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index ce7cdb0e2..faa45f32d 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -493,6 +493,9 @@ export async function midin(input) { otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : '' }`, ); + } + // ensure refs for this input are initialized + if (!refs[input]) { refs[input] = {}; } const cc = (cc) => ref(() => refs[input][cc] || 0); From fe18aa770d566cccf7cf57618e6e00e3d4520d0b Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 23 Jul 2025 18:34:34 -0400 Subject: [PATCH 04/65] 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 05/65] 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 cf5c502a05e1f74e62dc8bf18effb2fc35250e10 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 23 Jul 2025 23:16:14 -0400 Subject: [PATCH 06/65] rm unessecary json --- packages/repl/prebake.mjs | 2 +- website/public/uzudrumkit.json | 54 ---------------------------------- website/src/repl/prebake.mjs | 2 +- 3 files changed, 2 insertions(+), 56 deletions(-) delete mode 100644 website/public/uzudrumkit.json diff --git a/packages/repl/prebake.mjs b/packages/repl/prebake.mjs index 5e524bbc8..13027d2d8 100644 --- a/packages/repl/prebake.mjs +++ b/packages/repl/prebake.mjs @@ -36,7 +36,7 @@ export async function prebake() { samples(`${ds}/tidal-drum-machines.json`), samples(`${ds}/piano.json`), samples(`${ds}/Dirt-Samples.json`), - samples(`${ds}/uzudrumkit.json`), + samples(`https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/strudel.json`), samples(`${ds}/vcsl.json`), samples(`${ds}/mridangam.json`), ]); diff --git a/website/public/uzudrumkit.json b/website/public/uzudrumkit.json deleted file mode 100644 index 102baf0b8..000000000 --- a/website/public/uzudrumkit.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "_base": "https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/", - "bd": [ - "bd/10_bd_switchangel.wav", - "bd/11_bd_mot4i.wav", - "bd/12_bd_mot4i.wav", - "bd/13_bd_mot4i.wav", - "bd/14_bd_switchangel.wav", - "bd/15_bd_switchangel.wav", - "bd/16_bd_switchangel.wav" - ], - "breaks": [ - "breaks/10_break_amen_pprocessed.wav" - ], - "cp": [ - "cp/10_cp_switchangel.wav", - "cp/11_cp_mot4i.wav" - ], - "cr": [ - "cr/10_cr_mot4i.wav" - ], - "hh": [ - "hh/10_hh_mot4i.wav", - "hh/11_hh_switchangel.wav", - "hh/12_hh_switchangel.wav", - "hh/13_hh_mot4i.wav" - ], - "ht": [ - "ht/10_ht_mot4i.wav" - ], - "lt": [ - "lt/10_lt_mot4i.wav" - ], - "mt": [ - "mt/10_mt_mot4i.wav" - ], - "oh": [ - "oh/10_oh_switchangel.wav", - "oh/11_oh_switchangel.wav", - "oh/12_oh_switchangel.wav" - ], - "perc": [ - "perc/10_perc_switchangel.wav" - ], - "rim": [ - "rim/10_rim_switch_angel.wav" - ], - "sd": [ - "sd/10_sd_switchangel_3.wav", - "sd/11_sd_switchangel_2.wav", - "sd/12_sd_switchangel_2.wav", - "sd/13_sd.wav" - ] - } \ No newline at end of file diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index 21034dc66..ac3b1926f 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -28,7 +28,7 @@ export async function prebake() { prebake: true, tag: 'drum-machines', }), - samples(`${baseNoTrailing}/uzudrumkit.json`, undefined, { prebake: true, tag: 'drum-machines' }), + samples(`https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/strudel.json`, undefined, { prebake: true, tag: 'drum-machines' }), samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }), samples( { From 4a894b0be2b31d10a28efc286d5a5b5d378000a4 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 23 Jul 2025 23:25:14 -0400 Subject: [PATCH 07/65] frmt --- website/src/repl/prebake.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index ac3b1926f..af4bdaed8 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -28,7 +28,10 @@ export async function prebake() { prebake: true, tag: 'drum-machines', }), - samples(`https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/strudel.json`, undefined, { prebake: true, tag: 'drum-machines' }), + samples(`https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/strudel.json`, undefined, { + prebake: true, + tag: 'drum-machines', + }), samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }), samples( { From fb7d76a2aba2c5aaa2d7415adc8c90fd510d6ef1 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 24 Jul 2025 23:50:27 -0400 Subject: [PATCH 08/65] working --- packages/core/controls.mjs | 12 ++++ packages/superdough/helpers.mjs | 17 +++++- packages/superdough/noise.mjs | 2 +- packages/superdough/synth.mjs | 2 +- test/__snapshots__/examples.test.mjs.snap | 69 +++++++++++++++++++++++ 5 files changed, 97 insertions(+), 5 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 326343e10..af0136e54 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -252,6 +252,18 @@ export const { fmenv } = registerControl('fmenv'); * */ export const { fmattack } = registerControl('fmattack'); + +/** + * Attack time for the FM envelope: time it takes to reach maximum modulation + * + * @name fmwave + * @param {number | Pattern} wave waveform + * @example + * n("0 1 2 3".fast(4)).chord("").voicing().s("sawtooth").fmwave("brown").fm(.6) + * + */ +export const { fmwave } = registerControl('fmwave'); + /** * Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase. * diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 6bde69373..63643ebbc 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,5 +1,8 @@ import { getAudioContext } from './superdough.mjs'; import { clamp, nanFallback } from './util.mjs'; +import { getNoiseBuffer } from './noise.mjs'; + +export const noises = ['pink', 'white', 'brown', 'crackle']; export function gainNode(value) { const node = getAudioContext().createGain(); @@ -216,9 +219,17 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { } const mod = (freq, range = 1, type = 'sine') => { const ctx = getAudioContext(); - const osc = ctx.createOscillator(); - osc.type = type; - osc.frequency.value = freq; + let osc; + if (noises.includes(type)) { + osc = ctx.createBufferSource(); + osc.buffer = getNoiseBuffer(type, 2); + osc.loop = true; + } else { + osc = ctx.createOscillator(); + osc.type = type; + osc.frequency.value = freq; + } + osc.start(); const g = new GainNode(ctx, { gain: range }); osc.connect(g); // -range, range diff --git a/packages/superdough/noise.mjs b/packages/superdough/noise.mjs index 247794702..5411f2f2f 100644 --- a/packages/superdough/noise.mjs +++ b/packages/superdough/noise.mjs @@ -4,7 +4,7 @@ import { getAudioContext } from './superdough.mjs'; let noiseCache = {}; // lazy generates noise buffers and keeps them forever -function getNoiseBuffer(type, density) { +export function getNoiseBuffer(type, density) { const ac = getAudioContext(); if (noiseCache[type]) { return noiseCache[type]; diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 88e14e5ab..255e0342b 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -9,6 +9,7 @@ import { getVibratoOscillator, webAudioTimeout, getWorklet, + noises, } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; @@ -40,7 +41,6 @@ const waveformAliases = [ ['saw', 'sawtooth'], ['sin', 'sine'], ]; -const noises = ['pink', 'white', 'brown', 'crackle']; export function registerSynthSounds() { [...waveforms].forEach((s) => { diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index e6bfa44aa..2725e1d99 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3706,6 +3706,75 @@ exports[`runs examples > example "fmsustain" example index 0 1`] = ` ] `; +exports[`runs examples > example "fmwave" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/16 → 1/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/8 → 3/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/16 → 1/4 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/4 → 5/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 5/16 → 3/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/8 → 7/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 7/16 → 1/2 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/2 → 9/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 9/16 → 5/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 5/8 → 11/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 11/16 → 3/4 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/4 → 13/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 13/16 → 7/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 7/8 → 15/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 15/16 → 1/1 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/1 → 17/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 17/16 → 9/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 9/8 → 19/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 19/16 → 5/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 5/4 → 21/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 21/16 → 11/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 11/8 → 23/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 23/16 → 3/2 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/2 → 25/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 25/16 → 13/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 13/8 → 27/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 27/16 → 7/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 7/4 → 29/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 29/16 → 15/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 15/8 → 31/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 31/16 → 2/1 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 2/1 → 33/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 33/16 → 17/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 17/8 → 35/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 35/16 → 9/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 9/4 → 37/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 37/16 → 19/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 19/8 → 39/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 39/16 → 5/2 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 5/2 → 41/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 41/16 → 21/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 21/8 → 43/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 43/16 → 11/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 11/4 → 45/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 45/16 → 23/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 23/8 → 47/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 47/16 → 3/1 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/1 → 49/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 49/16 → 25/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 25/8 → 51/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 51/16 → 13/4 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 13/4 → 53/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 53/16 → 27/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 27/8 → 55/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 55/16 → 7/2 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 7/2 → 57/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 57/16 → 29/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 29/8 → 59/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 59/16 → 15/4 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 15/4 → 61/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 61/16 → 31/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 31/8 → 63/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 63/16 → 4/1 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]", +] +`; + exports[`runs examples > example "focus" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:sd ]", From 82f0dbb750c40ab04197b6d27666341fa59a0bc2 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Thu, 24 Jul 2025 23:59:38 -0400 Subject: [PATCH 09/65] add another example --- packages/core/controls.mjs | 4 +- test/__snapshots__/examples.test.mjs.snap | 69 +++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index af0136e54..24c75970c 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -254,11 +254,13 @@ export const { fmenv } = registerControl('fmenv'); export const { fmattack } = registerControl('fmattack'); /** - * Attack time for the FM envelope: time it takes to reach maximum modulation + * waveform of the modulator * * @name fmwave * @param {number | Pattern} wave waveform * @example + * n("0 1 2 3".fast(4)).scale("d:minor").s("sine").fmwave("").fm(4).fmh(2.01) + * @example * n("0 1 2 3".fast(4)).chord("").voicing().s("sawtooth").fmwave("brown").fm(.6) * */ diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 2725e1d99..eb6c238d5 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3707,6 +3707,75 @@ exports[`runs examples > example "fmsustain" example index 0 1`] = ` `; exports[`runs examples > example "fmwave" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/16 → 1/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/8 → 3/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 3/16 → 1/4 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/4 → 5/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 5/16 → 3/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 3/8 → 7/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 7/16 → 1/2 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/2 → 9/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 9/16 → 5/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 5/8 → 11/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 11/16 → 3/4 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 3/4 → 13/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 13/16 → 7/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 7/8 → 15/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 15/16 → 1/1 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/1 → 17/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 17/16 → 9/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 9/8 → 19/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 19/16 → 5/4 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 5/4 → 21/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 21/16 → 11/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 11/8 → 23/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 23/16 → 3/2 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 3/2 → 25/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 25/16 → 13/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 13/8 → 27/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 27/16 → 7/4 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 7/4 → 29/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 29/16 → 15/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 15/8 → 31/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 31/16 → 2/1 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 2/1 → 33/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 33/16 → 17/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 17/8 → 35/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 35/16 → 9/4 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 9/4 → 37/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 37/16 → 19/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 19/8 → 39/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 39/16 → 5/2 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 5/2 → 41/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 41/16 → 21/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 21/8 → 43/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 43/16 → 11/4 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 11/4 → 45/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 45/16 → 23/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 23/8 → 47/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 47/16 → 3/1 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 3/1 → 49/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 49/16 → 25/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 25/8 → 51/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 51/16 → 13/4 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 13/4 → 53/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 53/16 → 27/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 27/8 → 55/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 55/16 → 7/2 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 7/2 → 57/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 57/16 → 29/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 29/8 → 59/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 59/16 → 15/4 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 15/4 → 61/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 61/16 → 31/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 31/8 → 63/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 63/16 → 4/1 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", +] +`; + +exports[`runs examples > example "fmwave" example index 1 1`] = ` [ "[ 0/1 → 1/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]", "[ 1/16 → 1/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", From dae1560c409dab31828ecfe0e851348a524ed76b Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 25 Jul 2025 00:00:23 -0400 Subject: [PATCH 10/65] description --- 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 24c75970c..7071387af 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -254,7 +254,7 @@ export const { fmenv } = registerControl('fmenv'); export const { fmattack } = registerControl('fmattack'); /** - * waveform of the modulator + * waveform of the fm modulator * * @name fmwave * @param {number | Pattern} wave waveform From 51f0bab1f84922474354d4b289a12bdd0aeb1dd5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 26 Jul 2025 12:54:59 -0400 Subject: [PATCH 11/65] 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 3b6e3624be36dc66482c03be0165215628b37cd0 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 27 Jul 2025 01:07:45 -0400 Subject: [PATCH 12/65] working --- packages/superdough/synth.mjs | 79 ++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 88e14e5ab..34f958f11 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -12,9 +12,9 @@ import { } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; -const getFrequencyFromValue = (value) => { +const getFrequencyFromValue = (value, defaultNote = 36) => { let { note, freq } = value; - note = note || 36; + note = note || defaultNote; if (typeof note === 'string') { note = noteToMidi(note); // e.g. c3 => 48 } @@ -42,6 +42,19 @@ const waveformAliases = [ ]; const noises = ['pink', 'white', 'brown', 'crackle']; +function makeDistortionCurve(amount) { + const k = typeof amount === 'number' ? amount : 50; + const n_samples = 44100; + const curve = new Float32Array(n_samples); + const deg = Math.PI / 180; + + for (let i = 0; i < n_samples; i++) { + const x = (i * 2) / n_samples - 1; + curve[i] = Math.tanh(x * k); + } + return curve; +} + export function registerSynthSounds() { [...waveforms].forEach((s) => { registerSound( @@ -84,6 +97,68 @@ export function registerSynthSounds() { { type: 'synth', prebake: true }, ); }); + + registerSound( + '909bd', + (t, value, onended) => { + const { duration, decay = 0.5, pdecay = 0.5, penv = 36, clip } = value; + const ctx = getAudioContext(); + const attackhold = 0.02; + const noiselvl = 1.2; + const noisedecay = 0.025; + + const o = ctx.createOscillator(); + o.type = 'triangle'; + o.frequency.value = getFrequencyFromValue(value, 29); + o.detune.setValueAtTime(penv * 100, 0); + o.detune.setValueAtTime(penv * 100, t); + o.detune.exponentialRampToValueAtTime(0.001, t + pdecay); + const g = gainNode(1); + g.gain.setValueAtTime(1, t + attackhold); + g.gain.exponentialRampToValueAtTime(0.001, t + attackhold + decay); + o.start(t); + + const noise = getNoiseOscillator('brown', t, 2); + const noiseGain = gainNode(1); + noiseGain.gain.setValueAtTime(noiselvl, t); + noiseGain.gain.exponentialRampToValueAtTime(0.001, t + noisedecay); + + const sat = new WaveShaperNode(ctx); + // tri to sine diode shaper emulation + sat.curve = makeDistortionCurve(2); + + const mix = gainNode(1); + o.onended = () => { + o.disconnect(); + g.disconnect(); + sat.disconnect(); + noise.node.disconnect(); + noiseGain.disconnect(); + mix.disconnect(); + onended(); + }; + + const node = o.connect(sat).connect(g).connect(mix); + noise.node.connect(noiseGain).connect(mix); + const holdEnd = t + decay; + + let end = holdEnd + 0.01; + if (clip != null) { + end = Math.min(t + clip * duration, end); + } + o.stop(end); + noise.stop(end); + + return { + node, + stop: (endTime) => { + o.stop(endTime); + }, + }; + }, + { type: 'synth', prebake: true }, + ); + registerSound( 'supersaw', (begin, value, onended) => { From 7691403fd08f23de09604e1e277619de6db4f148 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 27 Jul 2025 01:22:59 -0400 Subject: [PATCH 13/65] prevent clicks --- packages/superdough/synth.mjs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 34f958f11..e72e669eb 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -106,6 +106,7 @@ export function registerSynthSounds() { const attackhold = 0.02; const noiselvl = 1.2; const noisedecay = 0.025; + const mixGain = 1; const o = ctx.createOscillator(); o.type = 'triangle'; @@ -127,7 +128,8 @@ export function registerSynthSounds() { // tri to sine diode shaper emulation sat.curve = makeDistortionCurve(2); - const mix = gainNode(1); + const mix = gainNode(mixGain); + o.onended = () => { o.disconnect(); g.disconnect(); @@ -140,12 +142,17 @@ export function registerSynthSounds() { const node = o.connect(sat).connect(g).connect(mix); noise.node.connect(noiseGain).connect(mix); - const holdEnd = t + decay; + const holdEnd = t + decay; let end = holdEnd + 0.01; if (clip != null) { end = Math.min(t + clip * duration, end); } + + // prevent clicking + mix.gain.setValueAtTime(mixGain, end - 0.01); + mix.gain.linearRampToValueAtTime(0, end); + o.stop(end); noise.stop(end); From 4d13b981f16db4c281505708f8cfe09e904efe09 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 27 Jul 2025 01:28:50 -0400 Subject: [PATCH 14/65] variable srate --- packages/superdough/synth.mjs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index e72e669eb..8f4686a80 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -42,11 +42,9 @@ const waveformAliases = [ ]; const noises = ['pink', 'white', 'brown', 'crackle']; -function makeDistortionCurve(amount) { +function makeSaturationCurve(amount, n_samples) { const k = typeof amount === 'number' ? amount : 50; - const n_samples = 44100; const curve = new Float32Array(n_samples); - const deg = Math.PI / 180; for (let i = 0; i < n_samples; i++) { const x = (i * 2) / n_samples - 1; @@ -126,7 +124,7 @@ export function registerSynthSounds() { const sat = new WaveShaperNode(ctx); // tri to sine diode shaper emulation - sat.curve = makeDistortionCurve(2); + sat.curve = makeSaturationCurve(2, ctx.sampleRate); const mix = gainNode(mixGain); From 973ae4c7da4a036ea9147d61377c62f4513e5c28 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 27 Jul 2025 15:02:00 -0400 Subject: [PATCH 15/65] change 909bd to sbd --- packages/superdough/synth.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 8f4686a80..9446103f9 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -97,7 +97,7 @@ export function registerSynthSounds() { }); registerSound( - '909bd', + 'sbd', (t, value, onended) => { const { duration, decay = 0.5, pdecay = 0.5, penv = 36, clip } = value; const ctx = getAudioContext(); From 30c80119e901a3afcd07491fd8f8ea77a8a3d8f4 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 28 Jul 2025 01:20:42 -0400 Subject: [PATCH 16/65] works --- website/src/repl/components/panel/PatternsTab.jsx | 2 +- website/src/user_pattern_utils.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/repl/components/panel/PatternsTab.jsx b/website/src/repl/components/panel/PatternsTab.jsx index 8ced8a990..8a43e4a1b 100644 --- a/website/src/repl/components/panel/PatternsTab.jsx +++ b/website/src/repl/components/panel/PatternsTab.jsx @@ -125,7 +125,7 @@ function UserPatterns({ context }) { style={{ display: 'none' }} type="file" multiple - accept="text/plain,application/json" + accept="text/plain,text/x-markdown,application/json" onChange={(e) => importPatterns(e.target.files)} /> import diff --git a/website/src/user_pattern_utils.mjs b/website/src/user_pattern_utils.mjs index 791c6a8f9..f087c69e0 100644 --- a/website/src/user_pattern_utils.mjs +++ b/website/src/user_pattern_utils.mjs @@ -197,7 +197,7 @@ export async function importPatterns(fileList) { if (file.type === 'application/json') { const userPatterns = userPattern.getAll(); setUserPatterns({ ...userPatterns, ...parseJSON(content) }); - } else if (file.type === 'text/plain') { + } else if (['text/x-markdown', 'text/plain'].includes(file.type)) { const id = file.name.replace(/\.[^/.]+$/, ''); userPattern.update(id, { code: content }); } From ee3e1217b40f1f2b884e925602308df2a4ea9be9 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 28 Jul 2025 15:24:19 +0200 Subject: [PATCH 17/65] add euclidish --- packages/core/euclid.mjs | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 25e12b07c..38ae6f673 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -10,7 +10,7 @@ https://rohandrape.net/?t=hmt 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 { timeCat, register, silence } from './pattern.mjs'; +import { timeCat, register, silence, fastGap } from './pattern.mjs'; import { rotate, flatten, splitAt, zipWith } from './util.mjs'; import Fraction, { lcm } from './fraction.mjs'; @@ -196,3 +196,40 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps, export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) { return _euclidLegato(pulses, steps, rotation, pat); }); + +/** + * A 'euclid' variant with an additional parameter that morphs the resulting + * rhythm from 0 (no morphing) to 1 (completely 'even'). For example + * `sound("bd").euclidish(3,8,0)` would be the same as + * `sound("bd").euclid(3,8)`, and `sound("bd").euclidish(3,8,1)` would be the + * same as `sound("bd bd bd")`. `sound("bd").euclidish(3,8,0.5)` would have a + * groove somewhere between. + * Inspired by the work of Malcom Braff. + * @name euclidish + * @synonyms eish + * @memberof Pattern + * @param {number} pulses the number of onsets + * @param {number} steps the number of steps to fill + * @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse) + * @example + * sound("hh").euclidish(7,12,tri.slow(8)) + * .pan(tri.slow(8)) + * @example + * sound("bd").euclidish(7,12,slider(0,0.1,1)) + */ +export const [euclidish, eish] = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) { + const b = bjork(pulses, steps); + let trues = 0; + const offs = []; + for (const [pos, step] of b.entries()) { + if (step) { + offs.push([trues++, pos]); + } + } + const tweened = offs.map(([n, pos]) => + Fraction(pos) + .div(steps) + .add(Fraction(n).div(pulses).sub(Fraction(pos).div(steps)).mul(perc)), + ); + return pat.struct(stack(...tweened.map((pos) => pure(true)._fastGap(steps)._late(pos)))).setSteps(steps); +}); From 784ee576da9be9114ae1fad2ac9317484f898c06 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 28 Jul 2025 15:33:18 +0200 Subject: [PATCH 18/65] fixes --- packages/core/euclid.mjs | 4 +-- test/__snapshots__/examples.test.mjs.snap | 33 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 38ae6f673..3dfb152c2 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -214,10 +214,8 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s * @example * sound("hh").euclidish(7,12,tri.slow(8)) * .pan(tri.slow(8)) - * @example - * sound("bd").euclidish(7,12,slider(0,0.1,1)) */ -export const [euclidish, eish] = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) { +export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) { const b = bjork(pulses, steps); let trues = 0; const offs = []; diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index e6bfa44aa..2f8404d05 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3271,6 +3271,39 @@ exports[`runs examples > example "euclidRot" example index 0 1`] = ` ] `; +exports[`runs examples > example "euclidish" example index 0 1`] = ` +[ + "[ 0/1 → 1/12 | s:hh pan:0 ]", + "[ 1/6 → 1/4 | s:hh pan:0.041666666666666664 ]", + "[ 1/4 → 1/3 | s:hh pan:0.0625 ]", + "[ 5/12 → 1/2 | s:hh pan:0.10416666666666667 ]", + "[ 7/12 → 2/3 | s:hh pan:0.14583333333333334 ]", + "[ 2/3 → 3/4 | s:hh pan:0.16666666666666666 ]", + "[ 5/6 → 11/12 | s:hh pan:0.20833333333333334 ]", + "[ 1/1 → 13/12 | s:hh pan:0.25 ]", + "[ 65/56 → 209/168 | s:hh pan:0.29017857142857145 ]", + "[ 141/112 → 451/336 | s:hh pan:0.31473214285714285 ]", + "[ 159/112 → 505/336 | s:hh pan:0.3549107142857143 ]", + "[ 177/112 → 559/336 | s:hh pan:0.3950892857142857 ]", + "[ 47/28 → 37/21 | s:hh pan:0.41964285714285715 ]", + "[ 103/56 → 323/168 | s:hh pan:0.45982142857142855 ]", + "[ 2/1 → 25/12 | s:hh pan:0.5 ]", + "[ 181/84 → 47/21 | s:hh pan:0.5386904761904762 ]", + "[ 127/56 → 395/168 | s:hh pan:0.5669642857142857 ]", + "[ 407/168 → 421/168 | s:hh pan:0.6056547619047619 ]", + "[ 433/168 → 149/56 | s:hh pan:0.6443452380952381 ]", + "[ 113/42 → 233/84 | s:hh pan:0.6726190476190477 ]", + "[ 239/84 → 41/14 | s:hh pan:0.7113095238095238 ]", + "[ 3/1 → 37/12 | s:hh pan:0.75 ]", + "[ 529/168 → 181/56 | s:hh pan:0.7872023809523809 ]", + "[ 367/112 → 1129/336 | s:hh pan:0.8191964285714286 ]", + "[ 1151/336 → 393/112 | s:hh pan:0.8563988095238095 ]", + "[ 1201/336 → 1229/336 | s:hh pan:0.8936011904761905 ]", + "[ 311/84 → 53/14 | s:hh pan:0.9255952380952381 ]", + "[ 647/168 → 661/168 | s:hh pan:0.9627976190476191 ]", +] +`; + exports[`runs examples > example "every" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:g3 ]", From 03cbe4b6d4f567e763328b595ed5ef617d2a6e90 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 28 Jul 2025 15:48:09 +0200 Subject: [PATCH 19/65] delint --- packages/core/euclid.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 3dfb152c2..bf5a82b00 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -10,7 +10,7 @@ https://rohandrape.net/?t=hmt 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 { timeCat, register, silence, fastGap } from './pattern.mjs'; +import { timeCat, register, silence, stack, pure } from './pattern.mjs'; import { rotate, flatten, splitAt, zipWith } from './util.mjs'; import Fraction, { lcm } from './fraction.mjs'; From 6ee9dea2ff44591c191236aa495d141cec601caf Mon Sep 17 00:00:00 2001 From: samyk Date: Mon, 28 Jul 2025 20:26:34 +0200 Subject: [PATCH 20/65] Fix incorrect stack Mini Notation --- packages/core/pattern.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index d59a1285d..0a9eccbe0 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1288,7 +1288,7 @@ export function sequenceP(pats) { * @synonyms polyrhythm, pr * @example * stack("g3", "b3", ["e4", "d4"]).note() - * // "g3,b3,[e4,d4]".note() + * // "g3,b3,[e4 d4]".note() * * @example * // As a chained function: From d6045c943b188e780937c91222568edd7ecd1cfa Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 29 Jul 2025 18:20:13 -0400 Subject: [PATCH 21/65] working --- packages/repl/prebake.mjs | 2 +- website/public/uzu-drumkit.json | 76 +++++++++++++++++++++++++++++++++ website/src/repl/prebake.mjs | 2 +- 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 website/public/uzu-drumkit.json diff --git a/packages/repl/prebake.mjs b/packages/repl/prebake.mjs index 13027d2d8..3f421f985 100644 --- a/packages/repl/prebake.mjs +++ b/packages/repl/prebake.mjs @@ -36,7 +36,7 @@ export async function prebake() { samples(`${ds}/tidal-drum-machines.json`), samples(`${ds}/piano.json`), samples(`${ds}/Dirt-Samples.json`), - samples(`https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/strudel.json`), + samples(`${ds}/uzu-drumkit.json`), samples(`${ds}/vcsl.json`), samples(`${ds}/mridangam.json`), ]); diff --git a/website/public/uzu-drumkit.json b/website/public/uzu-drumkit.json new file mode 100644 index 000000000..b9c7250d5 --- /dev/null +++ b/website/public/uzu-drumkit.json @@ -0,0 +1,76 @@ +{ + "_base": "https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/", + "bd": [ + "bd/10_bd_switchangel.wav", + "bd/11_bd_mot4i.wav", + "bd/12_bd_mot4i.wav", + "bd/13_bd_mot4i.wav", + "bd/14_bd_switchangel.wav", + "bd/15_bd_switchangel.wav", + "bd/16_bd_switchangel.wav", + "bd/17_bd_switchangel.wav" + ], + "brk": [ + "brk/10_break_amen_pprocessed.wav" + ], + "cb": [ + "cb/10_perc_switchangel.wav" + ], + "cp": [ + "cp/10_cp_switchangel.wav", + "cp/11_cp_mot4i.wav" + ], + "cr": [ + "cr/10_cr_switchangel.wav", + "cr/11_cr_mot4i.wav" + ], + "hh": [ + "hh/10_hh_switchangel.wav", + "hh/11_hh_mot4i.wav", + "hh/12_hh_switchangel.wav", + "hh/13_hh_switchangel.wav", + "hh/14_hh_mot4i.wav" + ], + "ht": [ + "ht/10_ht_mot4i.wav" + ], + "lt": [ + "lt/10_lt_mot4i.wav" + ], + "misc": [ + "misc/10_misc_switchangel_ludens.wav", + "misc/11_misc_switchangel_ludens.wav", + "misc/12_misc_switchangel_ludens.wav", + "misc/13_misc_switchangel_ludens.wav", + "misc/14_misc_switchangel_ludens.wav" + ], + "mt": [ + "mt/10_mt_mot4i.wav" + ], + "oh": [ + "oh/10_oh_switchangel.wav", + "oh/11_oh_switchangel.wav", + "oh/12_oh_switchangel.wav", + "oh/13_oh_switchangel.wav" + ], + "rd": [ + "rd/10_rd_switchangel.wav" + ], + "rim": [ + "rim/10_rim_switchangel.wav", + "rim/11_rim_switch_angel.wav" + ], + "sd": [ + "sd/10_sd_switchangel-bounce-2.wav", + "sd/11_sd_switchangel_3.wav", + "sd/12_sd_switchangel_2.wav", + "sd/13_sd_switchangel_2.wav", + "sd/14_sd.wav" + ], + "sh": [ + "sh/10_sh_switchangel.wav" + ], + "tb": [ + "tb/10_tb.wav" + ] + } \ No newline at end of file diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index af4bdaed8..fac6f5bb6 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -28,7 +28,7 @@ export async function prebake() { prebake: true, tag: 'drum-machines', }), - samples(`https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/strudel.json`, undefined, { + samples(`${baseNoTrailing}/uzu-drumkit.json`, undefined, { prebake: true, tag: 'drum-machines', }), From 9017085fa301a549889b89412dba161315a6821b Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Fri, 1 Aug 2025 09:36:36 +0100 Subject: [PATCH 22/65] add trans alias for transpose --- packages/tonal/tonal.mjs | 73 +++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index a425782d2..5ac894f56 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -61,40 +61,7 @@ function scaleOffset(scale, offset, note) { return n + o; } -// Pattern.prototype._transpose = function (intervalOrSemitones: string | number) { -/** - * Change the pitch of each value by the given amount. Expects numbers or note strings as values. - * The amount can be given as a number of semitones or as a string in interval short notation. - * If you don't care about enharmonic correctness, just use numbers. Otherwise, pass the interval of - * the form: ST where S is the degree number and T the type of interval with - * - * - M = major - * - m = minor - * - P = perfect - * - A = augmented - * - d = diminished - * - * Examples intervals: - * - * - 1P = unison - * - 3M = major third - * - 3m = minor third - * - 4P = perfect fourth - * - 4A = augmented fourth - * - 5P = perfect fifth - * - 5d = diminished fifth - * - * @param {string | number} amount Either number of semitones or interval string. - * @returns Pattern - * @memberof Pattern - * @name transpose - * @example - * "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note() - * @example - * "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note() - */ - -export const transpose = register('transpose', function (intervalOrSemitones, pat) { +function transposeFn(intervalOrSemitones, pat) { return pat.withHap((hap) => { const note = hap.value.note ?? hap.value; if (typeof note === 'number') { @@ -128,7 +95,43 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa } return hap.withValue(() => targetNote); }); -}); +} + +// Pattern.prototype._transpose = function (intervalOrSemitones: string | number) { +/** + * Change the pitch of each value by the given amount. Expects numbers or note strings as values. + * The amount can be given as a number of semitones or as a string in interval short notation. + * If you don't care about enharmonic correctness, just use numbers. Otherwise, pass the interval of + * the form: ST where S is the degree number and T the type of interval with + * + * - M = major + * - m = minor + * - P = perfect + * - A = augmented + * - d = diminished + * + * Examples intervals: + * + * - 1P = unison + * - 3M = major third + * - 3m = minor third + * - 4P = perfect fourth + * - 4A = augmented fourth + * - 5P = perfect fifth + * - 5d = diminished fifth + * + * @param {string | number} amount Either number of semitones or interval string. + * @returns Pattern + * @memberof Pattern + * @name transpose + * @example + * "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note() + * @example + * "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note() + */ + +export const transpose = register('transpose', transposeFn); +export const trans = register('trans', transposeFn); // example: transpose(3).late(0.2) will be equivalent to compose(transpose(3), late(0.2)) // e.g. `stack(c3).superimpose(transpose(slowcat(7, 5)))` or From ef3cd56b0dca8f6f677f8b1e72386c2763464fa6 Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Fri, 1 Aug 2025 09:39:21 +0100 Subject: [PATCH 23/65] add synonym to docs --- packages/tonal/tonal.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 5ac894f56..da3a4d507 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -124,6 +124,7 @@ function transposeFn(intervalOrSemitones, pat) { * @returns Pattern * @memberof Pattern * @name transpose + * @synonyms trans * @example * "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note() * @example From 051efdc13bdd184daf328fcd146eaf109ea97243 Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Fri, 1 Aug 2025 09:39:33 +0100 Subject: [PATCH 24/65] flyby improvement: add dec synonym to docs --- packages/core/controls.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 326343e10..6b3433c31 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -306,6 +306,7 @@ export const { fft } = registerControl('fft'); * * @name decay * @param {number | Pattern} time decay time in seconds + * @synonyms dec * @example * note("c3 e3 f3 g3").decay("<.1 .2 .3 .4>").sustain(0) * From c2b8cc4b824c31590ffdfd7f7dda4f7032621264 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Fri, 1 Aug 2025 23:29:50 -0400 Subject: [PATCH 25/65] 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 39c45bc2b2077a2b7cdcd6365af85c2cc15e8395 Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Sat, 2 Aug 2025 10:17:33 +0100 Subject: [PATCH 26/65] pass an array of names instead --- packages/tonal/tonal.mjs | 73 +++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index da3a4d507..7cb987394 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -61,42 +61,6 @@ function scaleOffset(scale, offset, note) { return n + o; } -function transposeFn(intervalOrSemitones, pat) { - return pat.withHap((hap) => { - const note = hap.value.note ?? hap.value; - if (typeof note === 'number') { - // note is a number, so just add the number semitones of the interval - let semitones; - if (typeof intervalOrSemitones === 'number') { - semitones = intervalOrSemitones; - } else if (typeof intervalOrSemitones === 'string') { - semitones = Interval.semitones(intervalOrSemitones) || 0; - } - const targetNote = note + semitones; - if (typeof hap.value === 'object') { - return hap.withValue(() => ({ ...hap.value, note: targetNote })); - } - return hap.withValue(() => targetNote); - } - if (typeof note !== 'string' || !isNote(note)) { - logger(`[tonal] transpose: not a note "${note}"`, 'warning'); - return hap; - } - // note is a string, so we might be able to preserve harmonics if interval is a string as well - const interval = !isNaN(Number(intervalOrSemitones)) - ? Interval.fromSemitones(intervalOrSemitones) - : String(intervalOrSemitones); - // TODO: move simplify to player to preserve enharmonics - // tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3 - // TODO: check if this is still relevant.. - const targetNote = Note.simplify(Note.transpose(note, interval)); - if (typeof hap.value === 'object') { - return hap.withValue(() => ({ ...hap.value, note: targetNote })); - } - return hap.withValue(() => targetNote); - }); -} - // Pattern.prototype._transpose = function (intervalOrSemitones: string | number) { /** * Change the pitch of each value by the given amount. Expects numbers or note strings as values. @@ -131,8 +95,41 @@ function transposeFn(intervalOrSemitones, pat) { * "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note() */ -export const transpose = register('transpose', transposeFn); -export const trans = register('trans', transposeFn); +export const transpose = register(['transpose', 'trans'], function transposeFn(intervalOrSemitones, pat) { + return pat.withHap((hap) => { + const note = hap.value.note ?? hap.value; + if (typeof note === 'number') { + // note is a number, so just add the number semitones of the interval + let semitones; + if (typeof intervalOrSemitones === 'number') { + semitones = intervalOrSemitones; + } else if (typeof intervalOrSemitones === 'string') { + semitones = Interval.semitones(intervalOrSemitones) || 0; + } + const targetNote = note + semitones; + if (typeof hap.value === 'object') { + return hap.withValue(() => ({ ...hap.value, note: targetNote })); + } + return hap.withValue(() => targetNote); + } + if (typeof note !== 'string' || !isNote(note)) { + logger(`[tonal] transpose: not a note "${note}"`, 'warning'); + return hap; + } + // note is a string, so we might be able to preserve harmonics if interval is a string as well + const interval = !isNaN(Number(intervalOrSemitones)) + ? Interval.fromSemitones(intervalOrSemitones) + : String(intervalOrSemitones); + // TODO: move simplify to player to preserve enharmonics + // tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3 + // TODO: check if this is still relevant.. + const targetNote = Note.simplify(Note.transpose(note, interval)); + if (typeof hap.value === 'object') { + return hap.withValue(() => ({ ...hap.value, note: targetNote })); + } + return hap.withValue(() => targetNote); + }); +}); // example: transpose(3).late(0.2) will be equivalent to compose(transpose(3), late(0.2)) // e.g. `stack(c3).superimpose(transpose(slowcat(7, 5)))` or From ad113b3888888fbc34b1742219fab93dadcd4b8d Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Sat, 2 Aug 2025 10:17:48 +0100 Subject: [PATCH 27/65] document ability to specify an array of names --- packages/core/pattern.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index d59a1285d..4d0274c7a 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1569,7 +1569,7 @@ export const func = curry((a, b) => reify(b).func(a)); /** * Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register. * - * @param {string} name name of the function + * @param {string | string[]} name name of the function, or an array of names to be used as synonyms * @param {function} func function with 1 or more params, where last is the current pattern * @noAutocomplete * From efc2333360dd1528f471f1a131cc5f1ebfbe85b0 Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Sat, 2 Aug 2025 10:25:28 +0100 Subject: [PATCH 28/65] add synonyms for scaleTranspose --- packages/tonal/tonal.mjs | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index a425782d2..dbd9eb8b0 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -142,6 +142,7 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa * @name scaleTranspose * @param {offset} offset number of steps inside the scale * @returns Pattern + * @synonyms scaleTrans * @example * "-8 [2,4,6]" * .scale('C4 bebop major') @@ -149,22 +150,25 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa * .note() */ -export const scaleTranspose = register('scaleTranspose', function (offset /* : number | string */, pat) { - return pat.withHap((hap) => { - if (!hap.context.scale) { - throw new Error('can only use scaleTranspose after .scale'); - } - if (typeof hap.value === 'object') - return hap.withValue(() => ({ - ...hap.value, - note: scaleOffset(hap.context.scale, Number(offset), hap.value.note), - })); - if (typeof hap.value !== 'string') { - throw new Error('can only use scaleTranspose with notes'); - } - return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value)); - }); -}); +export const scaleTranspose = register( + ['scaleTranspose', 'scaleTrans', 'strans'], + function (offset /* : number | string */, pat) { + return pat.withHap((hap) => { + if (!hap.context.scale) { + throw new Error('can only use scaleTranspose after .scale'); + } + if (typeof hap.value === 'object') + return hap.withValue(() => ({ + ...hap.value, + note: scaleOffset(hap.context.scale, Number(offset), hap.value.note), + })); + if (typeof hap.value !== 'string') { + throw new Error('can only use scaleTranspose with notes'); + } + return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value)); + }); + }, +); /** * Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}. From d55ef8e84383f201585d0972de17d68c0b00b6cb Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Sat, 2 Aug 2025 10:26:36 +0100 Subject: [PATCH 29/65] add strans too --- packages/tonal/tonal.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index dbd9eb8b0..6d18f27b8 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -142,7 +142,7 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa * @name scaleTranspose * @param {offset} offset number of steps inside the scale * @returns Pattern - * @synonyms scaleTrans + * @synonyms scaleTrans, strans * @example * "-8 [2,4,6]" * .scale('C4 bebop major') From 450f9685f344f012281b8a87fddc26a20eb28dd9 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 3 Aug 2025 12:54:19 -0400 Subject: [PATCH 30/65] fixed export --- packages/tonal/tonal.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index a2418c68a..0a5a9eefb 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -95,7 +95,7 @@ function scaleOffset(scale, offset, note) { * "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note() */ -export const transpose = register(['transpose', 'trans'], function transposeFn(intervalOrSemitones, pat) { +export const {transpose, trans} = register(['transpose', 'trans'], function transposeFn(intervalOrSemitones, pat) { return pat.withHap((hap) => { const note = hap.value.note ?? hap.value; if (typeof note === 'number') { @@ -151,7 +151,7 @@ export const transpose = register(['transpose', 'trans'], function transposeFn(i * .note() */ -export const scaleTranspose = register( +export const {scaleTranspose, scaleTrans, strans} = register( ['scaleTranspose', 'scaleTrans', 'strans'], function (offset /* : number | string */, pat) { return pat.withHap((hap) => { From 34b10b6c844bb4b5c75e74eef4f49937a8b0d0bf Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 3 Aug 2025 12:56:16 -0400 Subject: [PATCH 31/65] codeformat --- packages/tonal/tonal.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 0a5a9eefb..c4dd54880 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -95,7 +95,7 @@ function scaleOffset(scale, offset, note) { * "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note() */ -export const {transpose, trans} = register(['transpose', 'trans'], function transposeFn(intervalOrSemitones, pat) { +export const { transpose, trans } = register(['transpose', 'trans'], function transposeFn(intervalOrSemitones, pat) { return pat.withHap((hap) => { const note = hap.value.note ?? hap.value; if (typeof note === 'number') { @@ -151,7 +151,7 @@ export const {transpose, trans} = register(['transpose', 'trans'], function tran * .note() */ -export const {scaleTranspose, scaleTrans, strans} = register( +export const { scaleTranspose, scaleTrans, strans } = register( ['scaleTranspose', 'scaleTrans', 'strans'], function (offset /* : number | string */, pat) { return pat.withHap((hap) => { From 3b6777e25965fa6682b5f02574bbc7468b0f9208 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 5 Aug 2025 19:05:17 -0500 Subject: [PATCH 32/65] Fix signature of functions in log and logValues and add tests --- packages/core/pattern.mjs | 4 +-- packages/core/test/pattern.test.mjs | 40 ++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 4168645b8..d724bb853 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -852,14 +852,14 @@ export class Pattern { ); } - log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`, getData = (_, hap) => ({ hap })) { + log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) { return this.onTrigger((...args) => { logger(func(...args), undefined, getData(...args)); }, false); } logValues(func = id) { - return this.log((_, hap) => func(hap.value)); + return this.log((hap) => func(hap.value)); } ////////////////////////////////////////////////////////////////////// diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 93c4168c9..51e0acaef 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import Fraction from 'fraction.js'; -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { TimeSpan, @@ -55,6 +55,8 @@ import { expand, } from '../index.mjs'; +import { log, logValues } from '../pattern.mjs' + import { steady } from '../signal.mjs'; import { n, s } from '../controls.mjs'; @@ -1306,4 +1308,40 @@ describe('Pattern', () => { ); }); }); + describe('log', () => { + it('logs to console', () => { + const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); + const pattern = pure('a').log() + const haps = pattern.queryArc(0, 1); // query during first time arc + + // Force a trigger + haps.forEach(hap => { + hap.context?.onTrigger?.(hap); + }); + + expect(mockConsoleLog).toHaveBeenCalledWith( + '%c[hap] 0/1 → 1/1: a', + 'background-color: black;color:white;border-radius:15px', + ); + mockConsoleLog.mockRestore(); + }); + }); + describe('logValues', () => { + it('logs values to console', () => { + const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); + const pattern = pure('a').note("c#").log() + const haps = pattern.queryArc(0, 1); // query during first time arc + + // Force a trigger + haps.forEach(hap => { + hap.context?.onTrigger?.(hap); + }); + + expect(mockConsoleLog).toHaveBeenCalledWith( + '%c[hap] 0/1 → 1/1: value:a note:c#', + 'background-color: black;color:white;border-radius:15px', + ); + mockConsoleLog.mockRestore(); + }); + }); }); From 23e5358ae424bfb851d23688c9e547ce2061ae4d Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 5 Aug 2025 20:46:00 -0500 Subject: [PATCH 33/65] Formatting --- packages/core/test/pattern.test.mjs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 51e0acaef..b8e7de504 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -55,7 +55,7 @@ import { expand, } from '../index.mjs'; -import { log, logValues } from '../pattern.mjs' +import { log, logValues } from '../pattern.mjs'; import { steady } from '../signal.mjs'; @@ -1311,11 +1311,11 @@ describe('Pattern', () => { describe('log', () => { it('logs to console', () => { const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); - const pattern = pure('a').log() - const haps = pattern.queryArc(0, 1); // query during first time arc + const pattern = pure('a').log(); + const haps = pattern.queryArc(0, 1); // Force a trigger - haps.forEach(hap => { + haps.forEach((hap) => { hap.context?.onTrigger?.(hap); }); @@ -1329,11 +1329,11 @@ describe('Pattern', () => { describe('logValues', () => { it('logs values to console', () => { const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); - const pattern = pure('a').note("c#").log() - const haps = pattern.queryArc(0, 1); // query during first time arc + const pattern = pure('a').note('c#').log(); + const haps = pattern.queryArc(0, 1); // Force a trigger - haps.forEach(hap => { + haps.forEach((hap) => { hap.context?.onTrigger?.(hap); }); From de15d79edf39fea50159ec990c268c8aa56fcde3 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 5 Aug 2025 22:29:59 -0500 Subject: [PATCH 34/65] Add doscstrings and move stringifyValues into its own function for reuse --- packages/core/hap.mjs | 9 ++------- packages/core/pattern.mjs | 19 ++++++++++++++++++- packages/core/test/pattern.test.mjs | 4 ++-- packages/core/util.mjs | 10 ++++++++++ 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/core/hap.mjs b/packages/core/hap.mjs index a6e3c55ad..5f820d644 100644 --- a/packages/core/hap.mjs +++ b/packages/core/hap.mjs @@ -4,6 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ import Fraction from './fraction.mjs'; +import { stringifyValues } from './util.mjs'; export class Hap { /* @@ -148,13 +149,7 @@ export class Hap { } showWhole(compact = false) { - return `${this.whole == undefined ? '~' : this.whole.show()}: ${ - typeof this.value === 'object' - ? compact - ? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ') - : JSON.stringify(this.value) - : this.value - }`; + return `${this.whole == undefined ? '~' : this.whole.show()}: ${stringifyValues(this.value, compact)}`; } combineContext(b) { diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index d724bb853..f64761f4f 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -21,6 +21,7 @@ import { numeralArgs, parseNumeral, pairs, + stringifyValues, } from './util.mjs'; import drawLine from './drawLine.mjs'; import { logger } from './logger.mjs'; @@ -852,13 +853,29 @@ export class Pattern { ); } + /** + * Writes the content of the current event to the console, which is visible in the side menu + * or as the developer console. + * @name log + * @memberof Pattern + * @example + * s("bd sd").log() + */ log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) { return this.onTrigger((...args) => { logger(func(...args), undefined, getData(...args)); }, false); } - logValues(func = id) { + /** + * A simplified version of `log` which writes all "values" (various configurable parameters) + * within the event to the console, which is visible in the side menu or as the developer console. + * @name logValues + * @memberof Pattern + * @example + * s("bd sd").gain("0.25 0.5 1").n("2 1 0").logValues() + */ + logValues(func = (value) => `${stringifyValues(value, true)}`) { return this.log((hap) => func(hap.value)); } diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index b8e7de504..b82d39b9d 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1329,7 +1329,7 @@ describe('Pattern', () => { describe('logValues', () => { it('logs values to console', () => { const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); - const pattern = pure('a').note('c#').log(); + const pattern = pure('a').note('c#').logValues(); const haps = pattern.queryArc(0, 1); // Force a trigger @@ -1338,7 +1338,7 @@ describe('Pattern', () => { }); expect(mockConsoleLog).toHaveBeenCalledWith( - '%c[hap] 0/1 → 1/1: value:a note:c#', + '%cvalue:a note:c#', 'background-color: black;color:white;border-radius:15px', ); mockConsoleLog.mockRestore(); diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 756fac8e8..2e7c6e026 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -487,3 +487,13 @@ export function getCurrentKeyboardState() { // } // return lcm((x * y) / gcd(x, y), ...z); // }; + +// Takes values -- typically derived from events, i.e. `hap`s -- and renders them +// into a readable format +export function stringifyValues(value, compact = false) { + return typeof value === 'object' + ? compact + ? JSON.stringify(value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ') + : JSON.stringify(value) + : value; +} From f22f89347e9dc8da263a56d30bde6f41187b5506 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 5 Aug 2025 22:40:11 -0500 Subject: [PATCH 35/65] Final docstring cleanup & adding hap tag to logValues for consistency --- packages/core/pattern.mjs | 7 +++---- packages/core/test/pattern.test.mjs | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index f64761f4f..dbf1b17f7 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -854,8 +854,7 @@ export class Pattern { } /** - * Writes the content of the current event to the console, which is visible in the side menu - * or as the developer console. + * Writes the content of the current event to the console (visible in the side menu). * @name log * @memberof Pattern * @example @@ -869,13 +868,13 @@ export class Pattern { /** * A simplified version of `log` which writes all "values" (various configurable parameters) - * within the event to the console, which is visible in the side menu or as the developer console. + * within the event to the console (visible in the side menu). * @name logValues * @memberof Pattern * @example * s("bd sd").gain("0.25 0.5 1").n("2 1 0").logValues() */ - logValues(func = (value) => `${stringifyValues(value, true)}`) { + logValues(func = (value) => `[hap] ${stringifyValues(value, true)}`) { return this.log((hap) => func(hap.value)); } diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index b82d39b9d..1df5c8776 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1338,7 +1338,7 @@ describe('Pattern', () => { }); expect(mockConsoleLog).toHaveBeenCalledWith( - '%cvalue:a note:c#', + '%c[hap] value:a note:c#', 'background-color: black;color:white;border-radius:15px', ); mockConsoleLog.mockRestore(); From 3292f888108cd4633ac869ba625809ef07e6d761 Mon Sep 17 00:00:00 2001 From: robase <11038379+robase@users.noreply.github.com> Date: Sun, 27 Jul 2025 18:50:37 +0100 Subject: [PATCH 36/65] fix: repl autocomplete not rendering correctly --- packages/codemirror/autocomplete.mjs | 123 ++++++++++++++----------- website/src/repl/Repl.css | 131 +++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 53 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 203ab8556..d3091d874 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -1,68 +1,91 @@ import jsdoc from '../../doc.json'; -// import { javascriptLanguage } from '@codemirror/lang-javascript'; import { autocompletion } from '@codemirror/autocomplete'; import { h } from './html'; -function plaintext(str) { +const escapeHtml = (str) => { const div = document.createElement('div'); div.innerText = str; return div.innerHTML; -} +}; -const getDocLabel = (doc) => doc.name || doc.longname; -const getInnerText = (html) => { - var div = document.createElement('div'); +const stripHtml = (html) => { + const div = document.createElement('div'); div.innerHTML = html; return div.textContent || div.innerText || ''; }; -export function Autocomplete({ doc, label }) { - return h`
-

${label || getDocLabel(doc)}

-${doc.description} -
    - ${doc.params?.map( - ({ name, type, description }) => - `
  • ${name} : ${type.names?.join(' | ')} ${description ? ` - ${getInnerText(description)}` : ''}
  • `, - )} -
-
- ${doc.examples?.map((example) => `
${plaintext(example)}
`)} -
-
`[0]; - /* -
 {
-  console.log('ola!');
-  navigator.clipboard.writeText(example);
-  e.stopPropagation();
-}}
->
-{example}
-
-*/ -} +const getDocLabel = (doc) => doc.name || doc.longname; + +const buildParamsList = (params) => + params?.length + ? ` +
+

Parameters

+
    + ${params + .map( + ({ name, type, description }) => ` +
  • + ${name} + ${type.names?.join(' | ')} + ${description ? `
    ${stripHtml(description)}
    ` : ''} +
  • + `, + ) + .join('')} +
+
+ ` + : ''; + +const buildExamples = (examples) => + examples?.length + ? ` +
+

Examples

+ ${examples + .map( + (example) => ` +
${escapeHtml(example)}
+ `, + ) + .join('')} +
+ ` + : ''; + +export const Autocomplete = ({ doc, label }) => + h` +
+

${label || getDocLabel(doc)}

+ ${doc.description ? `

${doc.description}

` : ''} + ${buildParamsList(doc.params)} + ${buildExamples(doc.examples)} +
+`[0]; + +const isValidDoc = (doc) => { + const label = getDocLabel(doc); + return label && !label.startsWith('_') && !['package'].includes(doc.kind); +}; + +const hasExcludedTags = (doc) => + ['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)); const jsdocCompletions = jsdoc.docs - .filter( - (doc) => - getDocLabel(doc) && - !getDocLabel(doc).startsWith('_') && - !['package'].includes(doc.kind) && - !['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)), - ) - // https://codemirror.net/docs/ref/#autocomplete.Completion - .map((doc) /*: Completion */ => ({ + .filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc)) + // https://codemirror.net/docs/ref/#autocomplete.Completion + .map((doc) => ({ label: getDocLabel(doc), // detail: 'xxx', // An optional short piece of information to show (with a different style) after the label. info: () => Autocomplete({ doc }), type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type })); -export const strudelAutocomplete = (context /* : CompletionContext */) => { - let word = context.matchBefore(/\w*/); - if (word.from == word.to && !context.explicit) return null; +export const strudelAutocomplete = (context) => { + const word = context.matchBefore(/\w*/); + if (word.from === word.to && !context.explicit) return null; + return { from: word.from, options: jsdocCompletions, @@ -74,11 +97,5 @@ export const strudelAutocomplete = (context /* : CompletionContext */) => { }; }; -export function isAutoCompletionEnabled(on) { - return on - ? [ - autocompletion({ override: [strudelAutocomplete] }), - //javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }), - ] - : []; // autocompletion({ override: [] }) -} +export const isAutoCompletionEnabled = (enabled) => + enabled ? [autocompletion({ override: [strudelAutocomplete] })] : []; diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css index 3e13ff5a2..5f1d427e6 100644 --- a/website/src/repl/Repl.css +++ b/website/src/repl/Repl.css @@ -69,3 +69,134 @@ text-decoration: underline 0.18rem; text-underline-offset: 0.22rem; } + +/* Override default styles from the codemirror inline css for autocomplete info tooltip*/ +.cm-tooltip.cm-completionInfo { + padding: 0 !important; + background: #1e1e1e !important; + border: 1px solid #3a3a3a !important; + border-radius: 4px !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important; + max-width: 500px !important; + min-width: 300px !important; + max-height: 400px !important; + white-space: normal !important; +} + +/* Main tooltip container */ +.autocomplete-info-tooltip { + padding: 12px; + color: #d4d4d4; + font-family: 'SF Mono', 'Monaco', monospace; + font-size: 13px; + line-height: 1.4; + overflow-y: auto; + max-width: 600px; + max-height: 400px; +} + +/* Function name */ +.autocomplete-info-function-name { + font-size: 15px; + font-weight: 600; + color: #ffffff; + margin: 0 0 8px 0; +} + +/* Function description */ +.autocomplete-info-function-description { + margin: 0 0 12px 0; + color: #b4b4b4; + line-height: 1.5; +} + +/* Section titles */ +.autocomplete-info-section-title { + font-size: 12px; + font-weight: 600; + color: #ffffff; + margin: 16px 0 6px 0; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.autocomplete-info-section-title:first-child { + margin-top: 0; +} + +/* Parameters */ +.autocomplete-info-params-section { + margin-top: 12px; +} + +.autocomplete-info-params-list { + list-style: none; + margin: 0; + padding: 0; +} + +.autocomplete-info-param-item { + margin-bottom: 8px; + padding: 8px; + background: #2a2a2a; + border-radius: 3px; + border-left: 2px solid #555; +} + +.autocomplete-info-param-item:last-child { + margin-bottom: 0; +} + +.autocomplete-info-param-name { + font-weight: 600; + color: #ffffff; + margin-right: 8px; +} + +.autocomplete-info-param-type { + color: #888; + font-size: 12px; + background: #333; + padding: 1px 4px; + border-radius: 2px; +} + +.autocomplete-info-param-desc { + color: #b4b4b4; + font-size: 10px; + margin-top: 4px; + line-height: 1.4; +} + +/* Examples */ +.autocomplete-info-examples-section { + margin-top: 12px; +} + +.autocomplete-info-example-code { + background: #2a2a2a; + color: #d4d4d4; + padding: 8px; + border-radius: 3px; + font-family: 'SF Mono', 'Monaco', monospace; + font-size: 12px; + line-height: 1.5; + margin: 4px 0; + overflow-x: auto; + white-space: pre; + border: 1px solid #3a3a3a; +} + +/* Scrollbar */ +.autocomplete-info-tooltip::-webkit-scrollbar { + width: 4px; +} + +.autocomplete-info-tooltip::-webkit-scrollbar-track { + background: #2a2a2a; +} + +.autocomplete-info-tooltip::-webkit-scrollbar-thumb { + background: #555; + border-radius: 2px; +} From e7839a09a1c12fe3d803b747fe41289280a8ebac Mon Sep 17 00:00:00 2001 From: robase <11038379+robase@users.noreply.github.com> Date: Sun, 27 Jul 2025 22:02:06 +0100 Subject: [PATCH 37/65] fix: tooltip should respect themes --- website/src/repl/Repl.css | 57 ++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css index 5f1d427e6..3a5a6eb23 100644 --- a/website/src/repl/Repl.css +++ b/website/src/repl/Repl.css @@ -71,50 +71,54 @@ } /* Override default styles from the codemirror inline css for autocomplete info tooltip*/ +/* Override default styles from the c odemirror inline css for autocomplete info tooltip*/ .cm-tooltip.cm-completionInfo { - padding: 0 !important; - background: #1e1e1e !important; - border: 1px solid #3a3a3a !important; + padding: 12px !important; + padding-bottom: 12px !important; + border: 1px solid var(--foreground) !important; border-radius: 4px !important; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important; max-width: 500px !important; min-width: 300px !important; max-height: 400px !important; white-space: normal !important; + overflow: auto !important; + background-color: var(--lineHighlight) !important; } /* Main tooltip container */ .autocomplete-info-tooltip { - padding: 12px; - color: #d4d4d4; - font-family: 'SF Mono', 'Monaco', monospace; - font-size: 13px; + border-radius: 4px !important; + color: var(--foreground); + font-family: var(--font-family, 'SF Mono', 'Monaco', monospace); + font-size: var(--font-size, 13px); line-height: 1.4; - overflow-y: auto; max-width: 600px; max-height: 400px; + min-width: 400px; } /* Function name */ .autocomplete-info-function-name { font-size: 15px; font-weight: 600; - color: #ffffff; + color: var(--foreground); margin: 0 0 8px 0; } /* Function description */ .autocomplete-info-function-description { margin: 0 0 12px 0; - color: #b4b4b4; + color: var(--foreground); line-height: 1.5; + opacity: 0.8; } /* Section titles */ .autocomplete-info-section-title { font-size: 12px; font-weight: 600; - color: #ffffff; + color: var(--foreground); margin: 16px 0 6px 0; text-transform: uppercase; letter-spacing: 0.5px; @@ -138,9 +142,9 @@ .autocomplete-info-param-item { margin-bottom: 8px; padding: 8px; - background: #2a2a2a; + background-color: var(--lineBackground); border-radius: 3px; - border-left: 2px solid #555; + border-left: 2px solid var(--foreground, #555); } .autocomplete-info-param-item:last-child { @@ -149,23 +153,24 @@ .autocomplete-info-param-name { font-weight: 600; - color: #ffffff; + color: var(--variable, var(--foreground)); margin-right: 8px; } .autocomplete-info-param-type { - color: #888; + color: var(--comment); font-size: 12px; - background: #333; + background-color: var(--gutterForeground); padding: 1px 4px; border-radius: 2px; } .autocomplete-info-param-desc { - color: #b4b4b4; + color: var(--foreground); font-size: 10px; margin-top: 4px; line-height: 1.4; + opacity: 0.7; } /* Examples */ @@ -174,29 +179,33 @@ } .autocomplete-info-example-code { - background: #2a2a2a; - color: #d4d4d4; + background: var(--lineBackground); + color: var(--foreground); padding: 8px; border-radius: 3px; - font-family: 'SF Mono', 'Monaco', monospace; + font-family: var(--font-family, 'SF Mono', 'Monaco', monospace); font-size: 12px; line-height: 1.5; margin: 4px 0; overflow-x: auto; white-space: pre; - border: 1px solid #3a3a3a; + border: 1px solid var(--foreground, #3a3a3a); } -/* Scrollbar */ +/* Scrollbar - using theme colors */ .autocomplete-info-tooltip::-webkit-scrollbar { width: 4px; } .autocomplete-info-tooltip::-webkit-scrollbar-track { - background: #2a2a2a; + /* background: var(--lineBackground, var(--background)); */ } .autocomplete-info-tooltip::-webkit-scrollbar-thumb { - background: #555; + /* background: var(--border, #555); */ border-radius: 2px; } + +.autocomplete-info-tooltip::-webkit-scrollbar-thumb:hover { + /* background: var(--selection, var(--border, #777)); */ +} From 612efad3eb47ce376281a7fd3043338538ab842f Mon Sep 17 00:00:00 2001 From: robase <11038379+robase@users.noreply.github.com> Date: Sun, 27 Jul 2025 22:09:58 +0100 Subject: [PATCH 38/65] clean up comments --- website/src/repl/Repl.css | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css index 3a5a6eb23..62e7dcf24 100644 --- a/website/src/repl/Repl.css +++ b/website/src/repl/Repl.css @@ -71,7 +71,6 @@ } /* Override default styles from the codemirror inline css for autocomplete info tooltip*/ -/* Override default styles from the c odemirror inline css for autocomplete info tooltip*/ .cm-tooltip.cm-completionInfo { padding: 12px !important; padding-bottom: 12px !important; @@ -98,7 +97,6 @@ min-width: 400px; } -/* Function name */ .autocomplete-info-function-name { font-size: 15px; font-weight: 600; @@ -106,7 +104,6 @@ margin: 0 0 8px 0; } -/* Function description */ .autocomplete-info-function-description { margin: 0 0 12px 0; color: var(--foreground); @@ -114,7 +111,6 @@ opacity: 0.8; } -/* Section titles */ .autocomplete-info-section-title { font-size: 12px; font-weight: 600; @@ -128,7 +124,6 @@ margin-top: 0; } -/* Parameters */ .autocomplete-info-params-section { margin-top: 12px; } @@ -173,7 +168,6 @@ opacity: 0.7; } -/* Examples */ .autocomplete-info-examples-section { margin-top: 12px; } @@ -192,20 +186,17 @@ border: 1px solid var(--foreground, #3a3a3a); } -/* Scrollbar - using theme colors */ .autocomplete-info-tooltip::-webkit-scrollbar { width: 4px; } .autocomplete-info-tooltip::-webkit-scrollbar-track { - /* background: var(--lineBackground, var(--background)); */ } .autocomplete-info-tooltip::-webkit-scrollbar-thumb { - /* background: var(--border, #555); */ border-radius: 2px; } .autocomplete-info-tooltip::-webkit-scrollbar-thumb:hover { - /* background: var(--selection, var(--border, #777)); */ + } From 2e8a8b50df239d92fd7d14e0858a3e88e341ef94 Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 8 Aug 2025 22:27:05 -0500 Subject: [PATCH 39/65] Seemingly working version of supersaw FM and light cleanup --- eslint.config.mjs | 10 +++++++++ packages/superdough/synth.mjs | 7 ++---- packages/superdough/worklets.mjs | 37 ++++++++++++++++---------------- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 19d9bb390..e30b8e8a6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -83,4 +83,14 @@ export default [ ], }, }, + { + // Properties provided by AudioWorkletGlobalScope + files: ['packages/superdough/worklets.mjs'], + languageOptions: { + globals: { + currentTime: 'readonly', + sampleRate: 'readonly', + }, + }, + }, ]; diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 4e876caff..71138ae41 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -201,10 +201,7 @@ 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); - // https://codeberg.org/uzu/strudel/issues/1428 - // if you think about re-enabling this, please test with fm > 1 first - // it's like 10x gain, so it's really dangerous + const fm = applyFM(o.parameters.get('frequency'), value, begin); let envGain = gainNode(1); envGain = o.connect(envGain); @@ -216,7 +213,7 @@ export function registerSynthSounds() { destroyAudioWorkletNode(o); envGain.disconnect(); onended(); - // fm?.stop(); + fm?.stop(); vibratoOscillator?.stop(); }, begin, diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index e7928a48f..0ab71cbfa 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -8,18 +8,27 @@ import FFT from './fft.js'; const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const _mod = (n, m) => ((n % m) + m) % m; +// Restrict phase to the range [0, maxPhase) via wrapping +function wrapPhase(phase, maxPhase = 1) { + if (phase >= maxPhase) { + phase -= maxPhase; + } else if (phase < 0) { + phase += maxPhase; + } + return phase; +} const blockSize = 128; -// adjust waveshape to remove frequencies above nyquist to prevent aliasing +// Smooth waveshape near discontinuities to remove frequencies above nyquist and prevent aliasing // referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517 function polyBlep(phase, dt) { - // 0 <= phase < 1 + // Start of cycle if (phase < dt) { phase /= dt; // 2 * (phase - phase^2/2 - 0.5) return phase + phase - phase * phase - 1; } - // -1 < phase < 0 + // End of cycle else if (phase > 1 - dt) { phase = (phase - 1) / dt; // 2 * (phase^2/2 + phase + 0.5) @@ -115,7 +124,6 @@ class LFOProcessor extends AudioWorkletProcessor { process(inputs, outputs, parameters) { const begin = parameters['begin'][0]; - // eslint-disable-next-line no-undef if (currentTime >= parameters.end[0]) { return false; } @@ -143,7 +151,6 @@ class LFOProcessor extends AudioWorkletProcessor { if (this.phase == null) { this.phase = _mod(time * frequency + phaseoffset, 1); } - // eslint-disable-next-line no-undef const dt = frequency / sampleRate; for (let n = 0; n < blockSize; n++) { for (let i = 0; i < output.length; i++) { @@ -305,7 +312,6 @@ class LadderProcessor extends AudioWorkletProcessor { const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000); let cutoff = parameters.frequency[0]; - // eslint-disable-next-line no-undef cutoff = (cutoff * 2 * _PI) / sampleRate; cutoff = cutoff > 1 ? 1 : cutoff; @@ -438,18 +444,14 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { ]; } process(input, outputs, params) { - // eslint-disable-next-line no-undef if (currentTime <= params.begin[0]) { return true; } - // eslint-disable-next-line no-undef if (currentTime >= params.end[0]) { // this.port.postMessage({ type: 'onended' }); return false; } - let frequency = params.frequency[0]; - //apply detune in cents - frequency = frequency * Math.pow(2, params.detune[0] / 1200); + const frequency = applySemitoneDetuneToFrequency(params.frequency[0], params.detune[0] / 100); const output = outputs[0]; const voices = params.voices[0]; @@ -470,8 +472,9 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { gainL = gain2; gainR = gain1; } - // eslint-disable-next-line no-undef - const dt = freq / sampleRate; + // We must wrap this here because it is passed into sawblep below which + // has domain [0, 1] + const dt = wrapPhase(freq / sampleRate); for (let i = 0; i < output[0].length; i++) { this.phase[n] = this.phase[n] ?? Math.random(); @@ -480,11 +483,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { output[0][i] = output[0][i] + v * gainL; output[1][i] = output[1][i] + v * gainR; - this.phase[n] += dt; - - if (this.phase[n] > 1.0) { - this.phase[n] = this.phase[n] - 1; - } + this.phase[n] = wrapPhase(this.phase[n] + dt); } } return true; @@ -493,7 +492,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor); -// Phase Vocoder sourced from // sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file +// Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file const BUFFERED_BLOCK_SIZE = 2048; function genHannWindow(length) { From 218952b8b6f9a0e3ebf6cf2771d58e9f9ce6bf08 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 9 Aug 2025 16:25:11 -0500 Subject: [PATCH 40/65] Use the per-sample frequency and properly wrap polyBlep --- packages/superdough/worklets.mjs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 0ab71cbfa..2406d56dc 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -18,9 +18,10 @@ function wrapPhase(phase, maxPhase = 1) { return phase; } const blockSize = 128; -// Smooth waveshape near discontinuities to remove frequencies above nyquist and prevent aliasing +// Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing // referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517 function polyBlep(phase, dt) { + dt = Math.min(dt, 1 - dt); // Start of cycle if (phase < dt) { phase /= dt; @@ -451,7 +452,6 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { // this.port.postMessage({ type: 'onended' }); return false; } - const frequency = applySemitoneDetuneToFrequency(params.frequency[0], params.detune[0] / 100); const output = outputs[0]; const voices = params.voices[0]; @@ -462,9 +462,6 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { for (let n = 0; n < voices; n++) { const isOdd = (n & 1) == 1; - - //applies unison "spread" detune in semitones - const freq = applySemitoneDetuneToFrequency(frequency, getUnisonDetune(voices, freqspread, n)); let gainL = gain1; let gainR = gain2; // invert right and left gain @@ -472,11 +469,14 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { gainL = gain2; gainR = gain1; } - // We must wrap this here because it is passed into sawblep below which - // has domain [0, 1] - const dt = wrapPhase(freq / sampleRate); - for (let i = 0; i < output[0].length; i++) { + // Main detuning + let freq = applySemitoneDetuneToFrequency(params.frequency[i] ?? params.frequency[0], params.detune[0] / 100); + // Individual voice detuning + freq = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n)); + // We must wrap this here because it is passed into sawblep below which + // has domain [0, 1] + const dt = _mod(freq / sampleRate, 1); this.phase[n] = this.phase[n] ?? Math.random(); const v = waveshapes.sawblep(this.phase[n], dt); From 3dd667e82523564ef9cd8a70d985e705d10c4d32 Mon Sep 17 00:00:00 2001 From: Aria Date: Sun, 10 Aug 2025 17:11:59 -0500 Subject: [PATCH 41/65] Fix for a zero appearing (first) in FM's ADSR --- 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 63643ebbc..cca897913 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -264,7 +264,7 @@ export function applyFM(param, value, begin) { modulator = fmmod.node; stop = fmmod.stop; - if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) { + if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) { // no envelope by default modulator.connect(param); } else { From 2ab2cc5fc824c1abb6a8358e5e7a15680c2497db Mon Sep 17 00:00:00 2001 From: Aria Date: Fri, 15 Aug 2025 13:27:08 -0500 Subject: [PATCH 42/65] Add example tests --- test/__snapshots__/examples.test.mjs.snap | 34 +++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index eb6c238d5..51dff68a1 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -5285,6 +5285,40 @@ exports[`runs examples > example "lock" example index 0 1`] = ` ] `; +exports[`runs examples > example "log" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd ]", + "[ 1/2 → 1/1 | s:sd ]", + "[ 1/1 → 3/2 | s:bd ]", + "[ 3/2 → 2/1 | s:sd ]", + "[ 2/1 → 5/2 | s:bd ]", + "[ 5/2 → 3/1 | s:sd ]", + "[ 3/1 → 7/2 | s:bd ]", + "[ 7/2 → 4/1 | s:sd ]", +] +`; + +exports[`runs examples > example "logValues" example index 0 1`] = ` +[ + "[ (0/1 → 1/3) ⇝ 1/2 | s:bd gain:0.25 n:2 ]", + "[ 0/1 ⇜ (1/3 → 1/2) | s:bd gain:0.5 n:1 ]", + "[ (1/2 → 2/3) ⇝ 1/1 | s:sd gain:0.5 n:1 ]", + "[ 1/2 ⇜ (2/3 → 1/1) | s:sd gain:1 n:0 ]", + "[ (1/1 → 4/3) ⇝ 3/2 | s:bd gain:0.25 n:2 ]", + "[ 1/1 ⇜ (4/3 → 3/2) | s:bd gain:0.5 n:1 ]", + "[ (3/2 → 5/3) ⇝ 2/1 | s:sd gain:0.5 n:1 ]", + "[ 3/2 ⇜ (5/3 → 2/1) | s:sd gain:1 n:0 ]", + "[ (2/1 → 7/3) ⇝ 5/2 | s:bd gain:0.25 n:2 ]", + "[ 2/1 ⇜ (7/3 → 5/2) | s:bd gain:0.5 n:1 ]", + "[ (5/2 → 8/3) ⇝ 3/1 | s:sd gain:0.5 n:1 ]", + "[ 5/2 ⇜ (8/3 → 3/1) | s:sd gain:1 n:0 ]", + "[ (3/1 → 10/3) ⇝ 7/2 | s:bd gain:0.25 n:2 ]", + "[ 3/1 ⇜ (10/3 → 7/2) | s:bd gain:0.5 n:1 ]", + "[ (7/2 → 11/3) ⇝ 4/1 | s:sd gain:0.5 n:1 ]", + "[ 7/2 ⇜ (11/3 → 4/1) | s:sd gain:1 n:0 ]", +] +`; + exports[`runs examples > example "loop" example index 0 1`] = ` [ "[ 0/1 → 1/1 | s:casio loop: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 43/65] 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 44/65] 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 45/65] 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 ]", ] `; From b7176155589f6533346d0557172bd1ef72e859f3 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 17 Aug 2025 22:13:26 +0100 Subject: [PATCH 46/65] optimise euclidish, creating new `morph` function in the process --- packages/core/euclid.mjs | 22 ++--- packages/core/pattern.mjs | 67 ++++++++++++++++ packages/core/timespan.mjs | 2 +- test/__snapshots__/examples.test.mjs.snap | 98 ++++++++++++++++------- 4 files changed, 143 insertions(+), 46 deletions(-) diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index bf5a82b00..44ab07f11 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -10,7 +10,7 @@ https://rohandrape.net/?t=hmt 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 { timeCat, register, silence, stack, pure } from './pattern.mjs'; +import { timeCat, register, silence, stack, pure, _morph } from './pattern.mjs'; import { rotate, flatten, splitAt, zipWith } from './util.mjs'; import Fraction, { lcm } from './fraction.mjs'; @@ -212,22 +212,10 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s * @param {number} steps the number of steps to fill * @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse) * @example - * sound("hh").euclidish(7,12,tri.slow(8)) - * .pan(tri.slow(8)) + * sound("hh").euclidish(7,12,sine.slow(8)) + * .pan(sine.slow(8)) */ export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) { - const b = bjork(pulses, steps); - let trues = 0; - const offs = []; - for (const [pos, step] of b.entries()) { - if (step) { - offs.push([trues++, pos]); - } - } - const tweened = offs.map(([n, pos]) => - Fraction(pos) - .div(steps) - .add(Fraction(n).div(pulses).sub(Fraction(pos).div(steps)).mul(perc)), - ); - return pat.struct(stack(...tweened.map((pos) => pure(true)._fastGap(steps)._late(pos)))).setSteps(steps); + const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc); + return pat.struct(morphed).setSteps(steps); }); diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 4168645b8..ada13f65b 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -21,6 +21,7 @@ import { numeralArgs, parseNumeral, pairs, + zipWith, } from './util.mjs'; import drawLine from './drawLine.mjs'; import { logger } from './logger.mjs'; @@ -3400,3 +3401,69 @@ export const { beat } = register( ['beat'], __beat((x) => x.innerJoin()), ); + +export const _morph = (from, to, by) => { + by = Fraction(by); + const dur = Fraction(1).div(from.length); + const positions = (list) => { + const result = []; + for (const [pos, value] of list.entries()) { + if (value) { + result.push([Fraction(pos).div(list.length), value]); + } + } + return result; + }; + const arcs = zipWith( + ([posa, valuea], [posb, valueb]) => { + const b = by.mul(posb - posa).add(posa); + const e = b.add(dur); + return new TimeSpan(b, e); + }, + positions(from), + positions(to), + ); + function query(state) { + const cycle = state.span.begin.sam(); + const cycleArc = state.span.cycleArc(); + const result = []; + for (const whole of arcs) { + const part = whole.intersection(cycleArc); + if (part !== undefined) { + result.push( + new Hap( + whole.withTime((x) => x.add(cycle)), + part.withTime((x) => x.add(cycle)), + true, + ), + ); + } + } + return result; + } + return new Pattern(query).splitQueries(); +}; + +/** + * Takes two binary rhythms represented as lists of 1s and 0s, and a number + * between 0 and 1 that morphs between them. The two lists should contain the same + * number of true values. + * @example + * sound("hh").struct(morph([1,0,1,0,1,0,1,0], // straight rhythm + * [1,1,0,1,0,1,0], // wonky rhythm + * 0.25 // creates a slightly wonky rhythm + * ) + * ) + * @example + * sound("hh").struct(morph("1:0:1:0:1:0:1:0", // straight rhythm + * "1:1:0:1:0:1:0", // wonky rhythm + * sine.slow(8) // slowly morph between the rhythms + * ) + * ) + */ +export const morph = (frompat, topat, bypat) => { + frompat = reify(frompat); + topat = reify(topat); + bypat = reify(bypat); + return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by)))); +}; diff --git a/packages/core/timespan.mjs b/packages/core/timespan.mjs index 0dbc74fc8..446156bbf 100644 --- a/packages/core/timespan.mjs +++ b/packages/core/timespan.mjs @@ -72,7 +72,7 @@ export class TimeSpan { } intersection(other) { - // Intersection of two timespans, returns None if they don't intersect. + // Intersection of two timespans, returns undefined if they don't intersect. const intersect_begin = this.begin.max(other.begin); const intersect_end = this.end.min(other.end); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 83dd8c029..079e6ff3b 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3273,34 +3273,34 @@ exports[`runs examples > example "euclidRot" example index 0 1`] = ` exports[`runs examples > example "euclidish" example index 0 1`] = ` [ - "[ 0/1 → 1/12 | s:hh pan:0 ]", - "[ 1/6 → 1/4 | s:hh pan:0.041666666666666664 ]", - "[ 1/4 → 1/3 | s:hh pan:0.0625 ]", - "[ 5/12 → 1/2 | s:hh pan:0.10416666666666667 ]", - "[ 7/12 → 2/3 | s:hh pan:0.14583333333333334 ]", - "[ 2/3 → 3/4 | s:hh pan:0.16666666666666666 ]", - "[ 5/6 → 11/12 | s:hh pan:0.20833333333333334 ]", - "[ 1/1 → 13/12 | s:hh pan:0.25 ]", - "[ 65/56 → 209/168 | s:hh pan:0.29017857142857145 ]", - "[ 141/112 → 451/336 | s:hh pan:0.31473214285714285 ]", - "[ 159/112 → 505/336 | s:hh pan:0.3549107142857143 ]", - "[ 177/112 → 559/336 | s:hh pan:0.3950892857142857 ]", - "[ 47/28 → 37/21 | s:hh pan:0.41964285714285715 ]", - "[ 103/56 → 323/168 | s:hh pan:0.45982142857142855 ]", - "[ 2/1 → 25/12 | s:hh pan:0.5 ]", - "[ 181/84 → 47/21 | s:hh pan:0.5386904761904762 ]", - "[ 127/56 → 395/168 | s:hh pan:0.5669642857142857 ]", - "[ 407/168 → 421/168 | s:hh pan:0.6056547619047619 ]", - "[ 433/168 → 149/56 | s:hh pan:0.6443452380952381 ]", - "[ 113/42 → 233/84 | s:hh pan:0.6726190476190477 ]", - "[ 239/84 → 41/14 | s:hh pan:0.7113095238095238 ]", - "[ 3/1 → 37/12 | s:hh pan:0.75 ]", - "[ 529/168 → 181/56 | s:hh pan:0.7872023809523809 ]", - "[ 367/112 → 1129/336 | s:hh pan:0.8191964285714286 ]", - "[ 1151/336 → 393/112 | s:hh pan:0.8563988095238095 ]", - "[ 1201/336 → 1229/336 | s:hh pan:0.8936011904761905 ]", - "[ 311/84 → 53/14 | s:hh pan:0.9255952380952381 ]", - "[ 647/168 → 661/168 | s:hh pan:0.9627976190476191 ]", + "[ 0/1 → 1/12 | s:hh pan:0.5 ]", + "[ 13/84 → 5/21 | s:hh pan:0.5606253170575308 ]", + "[ 15/56 → 59/168 | s:hh pan:0.604413082836085 ]", + "[ 71/168 → 85/168 | s:hh pan:0.6629314122869361 ]", + "[ 97/168 → 37/56 | s:hh pan:0.7190455010067492 ]", + "[ 29/42 → 65/84 | s:hh pan:0.7580531369037533 ]", + "[ 71/84 → 13/14 | s:hh pan:0.8080762739548087 ]", + "[ 1/1 → 13/12 | s:hh pan:0.8535533905932737 ]", + "[ 451099417/393511398 → 322594689/262340932 | s:hh pan:0.891768001805729 ]", + "[ 335923379/262340932 → 536677685/393511398 | s:hh pan:0.9222657853371297 ]", + "[ 1122946175/787022796 → 99044284/65585233 | s:hh pan:0.9501869591788796 ]", + "[ 1238122213/787022796 → 651853723/393511398 | s:hh pan:0.9721673436944069 ]", + "[ 335923379/196755699 → 469759583/262340932 | s:hh pan:0.9868472639237561 ]", + "[ 729434777/393511398 → 1524454787/787022796 | s:hh pan:0.9967009321321423 ]", + "[ 2/1 → 25/12 | s:hh pan:1 ]", + "[ 15/7 → 187/84 | s:hh pan:0.9968561049466214 ]", + "[ 16/7 → 199/84 | s:hh pan:0.9874639560909118 ]", + "[ 17/7 → 211/84 | s:hh pan:0.9719416651541839 ]", + "[ 18/7 → 223/84 | s:hh pan:0.9504844339512095 ]", + "[ 19/7 → 235/84 | s:hh pan:0.9233620996141421 ]", + "[ 20/7 → 247/84 | s:hh pan:0.890915741234015 ]", + "[ 3/1 → 37/12 | s:hh pan:0.8535533905932737 ]", + "[ 1238122213/393511398 → 847276553/262340932 | s:hh pan:0.8106731928589048 ]", + "[ 860605243/262340932 → 1323700481/393511398 | s:hh pan:0.7677528833339 ]", + "[ 2696991767/787022796 → 230214750/65585233 | s:hh pan:0.7175585019834292 ]", + "[ 2812167805/787022796 → 1438876519/393511398 | s:hh pan:0.6644931595798675 ]", + "[ 729434777/196755699 → 994441447/262340932 | s:hh pan:0.6139286689554151 ]", + "[ 1516457573/393511398 → 3098500379/787022796 | s:hh pan:0.557342689325327 ]", ] `; @@ -5997,6 +5997,48 @@ exports[`runs examples > example "miditouch" example index 0 1`] = ` ] `; +exports[`runs examples > example "morph" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:hh ]", + "[ 25/112 → 39/112 | s:hh ]", + "[ 27/56 → 17/28 | s:hh ]", + "[ 83/112 → 97/112 | s:hh ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 137/112 → 151/112 | s:hh ]", + "[ 83/56 → 45/28 | s:hh ]", + "[ 195/112 → 209/112 | s:hh ]", + "[ 2/1 → 17/8 | s:hh ]", + "[ 249/112 → 263/112 | s:hh ]", + "[ 139/56 → 73/28 | s:hh ]", + "[ 307/112 → 321/112 | s:hh ]", + "[ 3/1 → 25/8 | s:hh ]", + "[ 361/112 → 375/112 | s:hh ]", + "[ 195/56 → 101/28 | s:hh ]", + "[ 419/112 → 433/112 | s:hh ]", +] +`; + +exports[`runs examples > example "morph" example index 1 1`] = ` +[ + "[ 0/1 → 1/8 | s:hh ]", + "[ 11/56 → 9/28 | s:hh ]", + "[ 13/28 → 33/56 | s:hh ]", + "[ 41/56 → 6/7 | s:hh ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 303934523/262340932 → 673454279/524681864 | s:hh ]", + "[ 188758485/131170466 → 820619173/524681864 | s:hh ]", + "[ 451099417/262340932 → 967784067/524681864 | s:hh ]", + "[ 2/1 → 17/8 | s:hh ]", + "[ 15/7 → 127/56 | s:hh ]", + "[ 17/7 → 143/56 | s:hh ]", + "[ 19/7 → 159/56 | s:hh ]", + "[ 3/1 → 25/8 | s:hh ]", + "[ 828616387/262340932 → 1722818007/524681864 | s:hh ]", + "[ 451099417/131170466 → 1869982901/524681864 | s:hh ]", + "[ 975781281/262340932 → 2017147795/524681864 | s:hh ]", +] +`; + exports[`runs examples > example "mousex" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C3 ]", From f17a4d045fce0c10ae8e1d40f73d4e6da950fbfb Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 18 Aug 2025 11:46:29 -0400 Subject: [PATCH 47/65] Publish - @strudel/codemirror@1.2.3 - @strudel/core@1.2.3 - @strudel/csound@1.2.4 - @strudel/draw@1.2.3 - @strudel/embed@1.1.1 - @strudel/gamepad@1.2.3 - @strudel/hydra@1.2.3 - @strudel/midi@1.2.4 - @strudel/mini@1.2.3 - mondolang@1.1.1 - @strudel/mondo@1.1.1 - @strudel/motion@1.2.3 - @strudel/mqtt@1.2.3 - @strudel/osc@1.2.3 - @strudel/reference@1.2.1 - @strudel/repl@1.2.4 - @strudel/serial@1.2.3 - @strudel/soundfonts@1.2.4 - superdough@1.2.4 - @strudel/tonal@1.2.3 - @strudel/transpiler@1.2.3 - @strudel/web@1.2.4 - @strudel/webaudio@1.2.4 - @strudel/xen@1.2.3 --- packages/codemirror/package.json | 2 +- packages/core/package.json | 2 +- packages/csound/package.json | 2 +- packages/draw/package.json | 2 +- packages/embed/package.json | 2 +- packages/gamepad/package.json | 2 +- packages/hydra/package.json | 2 +- packages/midi/package.json | 2 +- packages/mini/package.json | 2 +- packages/mondo/package.json | 2 +- packages/mondough/package.json | 2 +- packages/motion/package.json | 2 +- packages/mqtt/package.json | 2 +- packages/osc/package.json | 2 +- packages/reference/package.json | 2 +- packages/repl/package.json | 2 +- packages/serial/package.json | 2 +- packages/soundfonts/package.json | 2 +- packages/superdough/package.json | 2 +- packages/tonal/package.json | 2 +- packages/transpiler/package.json | 2 +- packages/web/package.json | 2 +- packages/webaudio/package.json | 2 +- packages/xen/package.json | 2 +- 24 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 4f8508c90..797a4312b 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "1.2.2", + "version": "1.2.3", "description": "Codemirror Extensions for Strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/core/package.json b/packages/core/package.json index f4170f2b5..33540c8bf 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/core", - "version": "1.2.2", + "version": "1.2.3", "description": "Port of Tidal Cycles to JavaScript", "main": "index.mjs", "type": "module", diff --git a/packages/csound/package.json b/packages/csound/package.json index 04a5ff246..19b1dd6fd 100644 --- a/packages/csound/package.json +++ b/packages/csound/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/csound", - "version": "1.2.3", + "version": "1.2.4", "description": "csound bindings for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/draw/package.json b/packages/draw/package.json index ee1b8dd00..f63367402 100644 --- a/packages/draw/package.json +++ b/packages/draw/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/draw", - "version": "1.2.2", + "version": "1.2.3", "description": "Helpers for drawing with Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/embed/package.json b/packages/embed/package.json index a0cc33de1..6a00ee904 100644 --- a/packages/embed/package.json +++ b/packages/embed/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/embed", - "version": "1.1.0", + "version": "1.1.1", "description": "Embeddable Web Component to load a Strudel REPL into an iframe", "main": "embed.js", "type": "module", diff --git a/packages/gamepad/package.json b/packages/gamepad/package.json index 3efb2e084..53fad18a6 100644 --- a/packages/gamepad/package.json +++ b/packages/gamepad/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/gamepad", - "version": "1.2.2", + "version": "1.2.3", "description": "Gamepad Inputs for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/hydra/package.json b/packages/hydra/package.json index b022de87d..1553264d4 100644 --- a/packages/hydra/package.json +++ b/packages/hydra/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/hydra", - "version": "1.2.2", + "version": "1.2.3", "description": "Hydra integration for strudel", "main": "hydra.mjs", "type": "module", diff --git a/packages/midi/package.json b/packages/midi/package.json index 4efd329d8..8dde35984 100644 --- a/packages/midi/package.json +++ b/packages/midi/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/midi", - "version": "1.2.3", + "version": "1.2.4", "description": "Midi API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mini/package.json b/packages/mini/package.json index 5d94301d4..e8fca4ac6 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mini", - "version": "1.2.2", + "version": "1.2.3", "description": "Mini notation for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mondo/package.json b/packages/mondo/package.json index 277bddb1c..a59bfc344 100644 --- a/packages/mondo/package.json +++ b/packages/mondo/package.json @@ -1,6 +1,6 @@ { "name": "mondolang", - "version": "1.1.0", + "version": "1.1.1", "description": "a language for functional composition that translates to js", "main": "mondo.mjs", "type": "module", diff --git a/packages/mondough/package.json b/packages/mondough/package.json index a034424c0..99a87f098 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mondo", - "version": "1.1.0", + "version": "1.1.1", "description": "mondo notation for strudel", "main": "mondough.mjs", "type": "module", diff --git a/packages/motion/package.json b/packages/motion/package.json index a7db05680..acacc154f 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/motion", - "version": "1.2.2", + "version": "1.2.3", "description": "DeviceMotion API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json index f522e3354..d5ac6dd71 100644 --- a/packages/mqtt/package.json +++ b/packages/mqtt/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mqtt", - "version": "1.2.2", + "version": "1.2.3", "description": "MQTT API for strudel", "main": "mqtt.mjs", "type": "module", diff --git a/packages/osc/package.json b/packages/osc/package.json index 7d19fbbfc..6e8b57813 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/osc", - "version": "1.2.2", + "version": "1.2.3", "description": "OSC messaging for strudel", "main": "osc.mjs", "type": "module", diff --git a/packages/reference/package.json b/packages/reference/package.json index 8dc966cc2..634057e4c 100644 --- a/packages/reference/package.json +++ b/packages/reference/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/reference", - "version": "1.2.0", + "version": "1.2.1", "description": "Headless reference of all strudel functions", "main": "index.mjs", "type": "module", diff --git a/packages/repl/package.json b/packages/repl/package.json index bfa404c75..aabd1b8c7 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/repl", - "version": "1.2.3", + "version": "1.2.4", "description": "Strudel REPL as a Web Component", "module": "index.mjs", "publishConfig": { diff --git a/packages/serial/package.json b/packages/serial/package.json index c04a69cd0..8abbe7692 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/serial", - "version": "1.2.2", + "version": "1.2.3", "description": "Webserial API for strudel", "main": "serial.mjs", "type": "module", diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json index 2c87a6e05..e5c1780ed 100644 --- a/packages/soundfonts/package.json +++ b/packages/soundfonts/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/soundfonts", - "version": "1.2.3", + "version": "1.2.4", "description": "Soundsfont support for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/superdough/package.json b/packages/superdough/package.json index 439b83718..809da0227 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -1,6 +1,6 @@ { "name": "superdough", - "version": "1.2.3", + "version": "1.2.4", "description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.", "main": "index.mjs", "type": "module", diff --git a/packages/tonal/package.json b/packages/tonal/package.json index 614e86f74..de02e4b1f 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/tonal", - "version": "1.2.2", + "version": "1.2.3", "description": "Tonal functions for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 2a5e39776..f9ebdfde1 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/transpiler", - "version": "1.2.2", + "version": "1.2.3", "description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.", "main": "index.mjs", "type": "module", diff --git a/packages/web/package.json b/packages/web/package.json index 0feddc82d..3f9cc50d1 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/web", - "version": "1.2.3", + "version": "1.2.4", "description": "Easy to setup, opiniated bundle of Strudel for the browser.", "module": "web.mjs", "publishConfig": { diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index 5cc0a5538..78988340a 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/webaudio", - "version": "1.2.3", + "version": "1.2.4", "description": "Web Audio helpers for Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/xen/package.json b/packages/xen/package.json index 88c2bb082..1cb751157 100644 --- a/packages/xen/package.json +++ b/packages/xen/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/xen", - "version": "1.2.2", + "version": "1.2.3", "description": "Xenharmonic API for strudel", "main": "index.mjs", "type": "module", From 7d24e4569279d567b10789102391a49bf858dffc Mon Sep 17 00:00:00 2001 From: fyynn Date: Mon, 18 Aug 2025 23:36:39 +0200 Subject: [PATCH 48/65] Added scrub() to Learn Docs Signed-off-by: fyynn --- website/src/pages/learn/samples.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index bbe650cf5..dbcb8acb2 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -361,6 +361,10 @@ Sampler effects are functions that can be used to change the behaviour of sample +### scrub + + + ### speed From f7f1bd63e868126090adff8861ed8ea9c1e189f0 Mon Sep 17 00:00:00 2001 From: fyynn Date: Mon, 18 Aug 2025 23:49:43 +0200 Subject: [PATCH 49/65] Adding the Duck/Sidechain effect to Learn Docs (learn/effects.mdx) Added the duck Effect to the section global effects. Signed-off-by: fyynn --- website/src/pages/learn/effects.mdx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx index 8f6a3b8aa..89f36fe96 100644 --- a/website/src/pages/learn/effects.mdx +++ b/website/src/pages/learn/effects.mdx @@ -339,4 +339,18 @@ global effects use the same chain for all events of the same orbit: +## Duck + +### duckorbit + + + +### duckattack + + + +### duckdepth + + + Next, we'll look at input / output via [MIDI, OSC and other methods](/learn/input-output). From d009b9592ec7b8975294182399d9a0891855e863 Mon Sep 17 00:00:00 2001 From: fyynn Date: Tue, 19 Aug 2025 00:09:41 +0200 Subject: [PATCH 50/65] Added Refrence to arp() arpWidth() and hush to Docs Signed-off-by: fyynn --- website/src/pages/learn/conditional-modifiers.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/pages/learn/conditional-modifiers.mdx b/website/src/pages/learn/conditional-modifiers.mdx index c2e22595b..b408883a9 100644 --- a/website/src/pages/learn/conditional-modifiers.mdx +++ b/website/src/pages/learn/conditional-modifiers.mdx @@ -34,11 +34,11 @@ import { JsDoc } from '../../docs/JsDoc'; ## arp - + ## arpWith 🧪 - + ## struct @@ -58,7 +58,7 @@ import { JsDoc } from '../../docs/JsDoc'; ## hush - + ## invert From 189daa3942aa8518f6da85c3f263015bb1859a88 Mon Sep 17 00:00:00 2001 From: fyynn Date: Tue, 19 Aug 2025 00:21:32 +0200 Subject: [PATCH 51/65] Added Refrence to arp() arpWidth() to Docs Signed-off-by: fyynn --- website/src/pages/learn/conditional-modifiers.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/pages/learn/conditional-modifiers.mdx b/website/src/pages/learn/conditional-modifiers.mdx index b408883a9..ea4b92f06 100644 --- a/website/src/pages/learn/conditional-modifiers.mdx +++ b/website/src/pages/learn/conditional-modifiers.mdx @@ -34,11 +34,11 @@ import { JsDoc } from '../../docs/JsDoc'; ## arp - + ## arpWith 🧪 - + ## struct From 36441e7b73a2b1d8dad141bde27da5807714961e Mon Sep 17 00:00:00 2001 From: fyynn Date: Tue, 19 Aug 2025 00:25:31 +0200 Subject: [PATCH 52/65] Fixing hush refrence to code documentation Signed-off-by: fyynn --- website/src/pages/learn/conditional-modifiers.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/learn/conditional-modifiers.mdx b/website/src/pages/learn/conditional-modifiers.mdx index ea4b92f06..d31ab0cf0 100644 --- a/website/src/pages/learn/conditional-modifiers.mdx +++ b/website/src/pages/learn/conditional-modifiers.mdx @@ -58,7 +58,7 @@ import { JsDoc } from '../../docs/JsDoc'; ## hush - + ## invert From d6ee10e05c99b08e5e8e2c0645c2183b0301b307 Mon Sep 17 00:00:00 2001 From: fyynn Date: Tue, 19 Aug 2025 00:44:46 +0200 Subject: [PATCH 53/65] fixed new scrub refrence --- website/src/pages/learn/samples.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index dbcb8acb2..475319748 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -363,7 +363,7 @@ Sampler effects are functions that can be used to change the behaviour of sample ### scrub - + ### speed From 215ab878092605c71867096b4dcab619dd9edc29 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 21 Aug 2025 09:41:21 +0200 Subject: [PATCH 54/65] fix: prettier --- packages/codemirror/autocomplete.mjs | 2 +- website/src/pages/learn/samples.mdx | 2 +- website/src/repl/Repl.css | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index d3091d874..c0845895e 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -74,7 +74,7 @@ const hasExcludedTags = (doc) => const jsdocCompletions = jsdoc.docs .filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc)) - // https://codemirror.net/docs/ref/#autocomplete.Completion + // https://codemirror.net/docs/ref/#autocomplete.Completion .map((doc) => ({ label: getDocLabel(doc), // detail: 'xxx', // An optional short piece of information to show (with a different style) after the label. diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index 475319748..0deb60704 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -363,7 +363,7 @@ Sampler effects are functions that can be used to change the behaviour of sample ### scrub - +{' '} ### speed diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css index 62e7dcf24..b8443081f 100644 --- a/website/src/repl/Repl.css +++ b/website/src/repl/Repl.css @@ -198,5 +198,4 @@ } .autocomplete-info-tooltip::-webkit-scrollbar-thumb:hover { - } From 8465517c76aef62b081190cb33419b755f72fa9b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 21 Aug 2025 10:00:41 +0200 Subject: [PATCH 55/65] remove hs2js postinstall --- website/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/package.json b/website/package.json index 499758147..87f4025e4 100644 --- a/website/package.json +++ b/website/package.json @@ -8,8 +8,7 @@ "start": "astro dev", "build": "astro build", "preview": "astro preview --port 3009 --host 0.0.0.0", - "astro": "astro", - "postinstall": "cp node_modules/hs2js/dist/tree-sitter.wasm public && cp node_modules/hs2js/dist/tree-sitter-haskell.wasm public" + "astro": "astro" }, "dependencies": { "@algolia/client-search": "^5.20.0", From 66aa3ac1da2d1151d5bf50222152ec21966376ae Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 21 Aug 2025 20:30:02 +0200 Subject: [PATCH 56/65] add --json flag to strudel sampler --- packages/sampler/README.md | 10 ++++++++++ packages/sampler/sample-server.mjs | 26 ++++++++++++++++++-------- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/sampler/README.md b/packages/sampler/README.md index c495c2add..c7b44610a 100644 --- a/packages/sampler/README.md +++ b/packages/sampler/README.md @@ -20,3 +20,13 @@ samples('http://localhost:5432') LOG=1 npx @strudel/sampler # adds logging PORT=5555 npx @strudel/sampler # changes port ``` + +## static json + +when running with `--json`, you will simply get the json logged back: + +```sh +npx @strudel/sampler --json > strudel.json +``` + +this is useful if you want to create a sample pack from the current folder. \ No newline at end of file diff --git a/packages/sampler/sample-server.mjs b/packages/sampler/sample-server.mjs index d1e56108f..08456add9 100644 --- a/packages/sampler/sample-server.mjs +++ b/packages/sampler/sample-server.mjs @@ -10,14 +10,6 @@ import os from 'os'; // eslint-disable-next-line const LOG = !!process.env.LOG || false; -console.log( - cowsay.say({ - text: 'welcome to @strudel/sampler', - e: 'oO', - T: 'U ', - }), -); - async function getFilesInDirectory(directory) { let files = []; const dirents = await readdir(directory, { withFileTypes: true }); @@ -60,8 +52,26 @@ async function getBanks(directory) { return { banks, files }; } +const args = process.argv.slice(2); + // eslint-disable-next-line const directory = process.cwd(); + +if (args.includes('--json')) { + const { banks, files } = await getBanks(directory); + const json = JSON.stringify(banks); + console.log(json); + process.exit(0); +} + +console.log( + cowsay.say({ + text: 'welcome to @strudel/sampler', + e: 'oO', + T: 'U ', + }), +); + const server = http.createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); const { banks, files } = await getBanks(directory); From b12707316ac2c0493d9330a9cec9a3a349b7a326 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 21 Aug 2025 20:30:32 +0200 Subject: [PATCH 57/65] bump @strudel/sampler to 0.2.1 --- packages/sampler/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sampler/package.json b/packages/sampler/package.json index b0f27f86a..765f2962d 100644 --- a/packages/sampler/package.json +++ b/packages/sampler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/sampler", - "version": "0.2.0", + "version": "0.2.1", "description": "", "keywords": [ "tidalcycles", From da283eb55a94d4ee9ba9bd68a12c850be6eba439 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 21 Aug 2025 20:36:17 +0200 Subject: [PATCH 58/65] doc: generate strudel.json --- website/src/pages/learn/samples.mdx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index 0deb60704..a87b8f7dd 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -178,6 +178,16 @@ the version number). It is also possible, of course, to just remove it from cache (deleting cache in browser Privacy settings, or from the dev console if you're technically minded, or by using a cache deleting extension). +## Generating strudel.json + +You can use [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler) to generate a strudel.json file for you, by running: + +```sh +npx --yes @strudel/sampler --json > strudel.json +``` + +See other uses of strudel/sampler further below, under "From Disk via @strudel/sampler". + ## Github Shortcut Because loading samples from github is common, there is a shortcut: From 42a903ecc080b1615f6b097e8f478aa21ae8837e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 21 Aug 2025 20:36:42 +0200 Subject: [PATCH 59/65] add --yes flag to readme, making sure first run works --- packages/sampler/README.md | 2 +- packages/sampler/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sampler/README.md b/packages/sampler/README.md index c7b44610a..1142176b7 100644 --- a/packages/sampler/README.md +++ b/packages/sampler/README.md @@ -26,7 +26,7 @@ PORT=5555 npx @strudel/sampler # changes port when running with `--json`, you will simply get the json logged back: ```sh -npx @strudel/sampler --json > strudel.json +npx --yes @strudel/sampler --json > strudel.json ``` this is useful if you want to create a sample pack from the current folder. \ No newline at end of file diff --git a/packages/sampler/package.json b/packages/sampler/package.json index 765f2962d..dd701bcfb 100644 --- a/packages/sampler/package.json +++ b/packages/sampler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/sampler", - "version": "0.2.1", + "version": "0.2.2", "description": "", "keywords": [ "tidalcycles", From b8c98736c5024fc213ed4e84edb92c7a2e5c3c0b Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 23 Aug 2025 17:29:15 +0100 Subject: [PATCH 60/65] fix benchmarks --- packages/core/bench/pattern.bench.mjs | 12 ++++++------ packages/mini/bench/mini.bench.mjs | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/core/bench/pattern.bench.mjs b/packages/core/bench/pattern.bench.mjs index 1b5be0b9c..56a849801 100644 --- a/packages/core/bench/pattern.bench.mjs +++ b/packages/core/bench/pattern.bench.mjs @@ -1,11 +1,11 @@ import { describe, bench } from 'vitest'; -import { calculateTactus, sequence, stack } from '../index.mjs'; +import { calculateSteps, sequence, stack } from '../index.mjs'; const pat64 = sequence(...Array(64).keys()); describe('steps', () => { - calculateTactus(true); + calculateSteps(true); bench( '+tactus', () => { @@ -14,7 +14,7 @@ describe('steps', () => { { time: 1000 }, ); - calculateTactus(false); + calculateSteps(false); bench( '-tactus', () => { @@ -25,7 +25,7 @@ describe('steps', () => { }); describe('stack', () => { - calculateTactus(true); + calculateSteps(true); bench( '+tactus', () => { @@ -34,7 +34,7 @@ describe('stack', () => { { time: 1000 }, ); - calculateTactus(false); + calculateSteps(false); bench( '-tactus', () => { @@ -43,4 +43,4 @@ describe('stack', () => { { time: 1000 }, ); }); -calculateTactus(true); +calculateSteps(true); diff --git a/packages/mini/bench/mini.bench.mjs b/packages/mini/bench/mini.bench.mjs index 782ac86ba..e7471bf22 100644 --- a/packages/mini/bench/mini.bench.mjs +++ b/packages/mini/bench/mini.bench.mjs @@ -1,10 +1,10 @@ import { describe, bench } from 'vitest'; -import { calculateTactus } from '../../core/index.mjs'; +import { calculateSteps } from '../../core/index.mjs'; import { mini } from '../index.mjs'; describe('mini', () => { - calculateTactus(true); + calculateSteps(true); bench( '+tactus', () => { @@ -13,7 +13,7 @@ describe('mini', () => { { time: 1000 }, ); - calculateTactus(false); + calculateSteps(false); bench( '-tactus', () => { @@ -21,5 +21,5 @@ describe('mini', () => { }, { time: 1000 }, ); - calculateTactus(true); + calculateSteps(true); }); From 64f7bc444288a98410da299b6aa7b1e62b9a3110 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 19 Aug 2025 09:12:29 -0500 Subject: [PATCH 61/65] Working version of duck on iOS safari --- packages/superdough/helpers.mjs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index c09acf1a5..1217b5163 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -211,7 +211,14 @@ export function getVibratoOscillator(param, value, t) { export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { const constantNode = new ConstantSourceNode(audioContext); + // Safari requires audio nodes to be connected in order for their onended events + // to fire, so we _mute it_ and then connect it to the destination + const zeroGain = gainNode(0); + zeroGain.connect(audioContext.destination); + constantNode.connect(zeroGain); constantNode.start(startTime); + + // Schedule the `onComplete` callback to occur at `stopTime` constantNode.stop(stopTime); constantNode.onended = () => { onComplete(); From 659071a4ee6b7a67f0e1a5c46a10c0f854bd3ffa Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 19 Aug 2025 16:18:48 -0500 Subject: [PATCH 62/65] Ensure zeroGain is cleaned up after use; move start/stop to after callback assigned for safety --- packages/superdough/helpers.mjs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 1217b5163..53922aa2c 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -211,18 +211,25 @@ export function getVibratoOscillator(param, value, t) { export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { const constantNode = new ConstantSourceNode(audioContext); - // Safari requires audio nodes to be connected in order for their onended events + // Certain browsers requires audio nodes to be connected in order for their onended events // to fire, so we _mute it_ and then connect it to the destination const zeroGain = gainNode(0); zeroGain.connect(audioContext.destination); constantNode.connect(zeroGain); - constantNode.start(startTime); // Schedule the `onComplete` callback to occur at `stopTime` - constantNode.stop(stopTime); constantNode.onended = () => { + // Ensure garbage collection + try { + zeroGain.disconnect(); + } catch {} + try { + constantNode.disconnect(); + } catch {} onComplete(); }; + constantNode.start(startTime); + constantNode.stop(stopTime); return constantNode; } const mod = (freq, range = 1, type = 'sine') => { From d64a0ef0eb8d79d90ff8fb51dce0ef47631f6d1b Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 19 Aug 2025 16:21:00 -0500 Subject: [PATCH 63/65] Lint requires no empty blocks: pass comment --- packages/superdough/helpers.mjs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 53922aa2c..81bec9399 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -222,10 +222,14 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { // Ensure garbage collection try { zeroGain.disconnect(); - } catch {} + } catch { + // pass + } try { constantNode.disconnect(); - } catch {} + } catch { + // pass + } onComplete(); }; constantNode.start(startTime); From 9eb3c9410de3734fabee94a007f910d8d45b43ff Mon Sep 17 00:00:00 2001 From: yaxu Date: Sat, 30 Aug 2025 12:46:00 +0200 Subject: [PATCH 64/65] Add verify link to mastodon a/c --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 4aa90fd38..21c5b8b6b 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ Live coding patterns on the web https://strudel.cc/ - -Development is moving to https://codeberg.org/uzu/strudel +Mastodon: social.toplap.org/@strudel - Try it here: - Docs: From dc6b766ab7420fbd8eb025a73d69e7f2f2644e9f Mon Sep 17 00:00:00 2001 From: yaxu Date: Sat, 30 Aug 2025 12:47:06 +0200 Subject: [PATCH 65/65] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 21c5b8b6b..baaac82b2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ Live coding patterns on the web https://strudel.cc/ -Mastodon: social.toplap.org/@strudel - Try it here: - Docs: @@ -46,3 +45,5 @@ There is a #strudel channel on the TidalCycles discord: The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project. + +We also have a mastodon account: social.toplap.org/@strudel