From 4de0c11f8c0834868ae346125d35e77134bf4d7d Mon Sep 17 00:00:00 2001 From: Aria Date: Mon, 29 Sep 2025 14:15:50 -0700 Subject: [PATCH 01/18] Remove mipmaps, fix clamping of parameters --- packages/superdough/wavetable.mjs | 32 +++++--------------- packages/superdough/worklets.mjs | 49 +++++++++++++------------------ 2 files changed, 27 insertions(+), 54 deletions(-) diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 48b7e661c..505e6ac38 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -5,7 +5,6 @@ import { destroyAudioWorkletNode, getADSRValues, getFrequencyFromValue, - getLfo, getParamADSR, getPitchEnvelope, getVibratoOscillator, @@ -14,7 +13,6 @@ import { } from './helpers.mjs'; import { logger } from './logger.mjs'; -const WT_MAX_MIP_LEVELS = 6; export const Warpmode = Object.freeze({ NONE: 0, ASYM: 1, @@ -50,25 +48,7 @@ async function loadWavetableFrames(url, label, frameLen = 2048) { const start = i * frameLen; frames[i] = ch0.subarray(start, start + frameLen); } - - // build mipmaps - const mipmaps = [frames]; - let levelFrames = frames; - for (let level = 1; level < WT_MAX_MIP_LEVELS; level++) { - const prevLen = levelFrames[0].length; - if (prevLen <= 32) break; - const nextLen = prevLen >> 1; - const next = levelFrames.map((src) => { - const out = new Float32Array(nextLen); - for (let j = 0; j < nextLen; j++) { - out[j] = (src[2 * j] + src[2 * j + 1]) / 2; - } - return out; - }); - mipmaps.push(next); - levelFrames = next; - } - return { frames, mipmaps, frameLen, numFrames }; + return { frames, frameLen, numFrames }; } const loadCache = {}; @@ -222,7 +202,7 @@ export const tables = async (url, frameLen, json, options = {}) => { }; export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { - const { s, n = 0, duration } = value; + const { s, n = 0, duration, clip } = value; const ac = getAudioContext(); const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); let { warpmode } = value; @@ -232,7 +212,10 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { const frequency = getFrequencyFromValue(value); const { url, label } = getCommonSampleInfo(value, tables); const payload = await loadWavetableFrames(url, label, frameLen); - const holdEnd = t + duration; + let holdEnd = t + duration; + if (clip !== undefined) { + holdEnd = Math.min(t + clip * duration, holdEnd); + } const endWithRelease = holdEnd + release; const envEnd = endWithRelease + 0.01; const source = getWorklet( @@ -252,7 +235,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { }, { outputChannelCount: [2] }, ); - source.port.postMessage({ type: 'tables', payload }); + source.port.postMessage({ type: 'table', payload }); if (ac.currentTime > t) { logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight'); return; @@ -328,7 +311,6 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { const timeoutNode = webAudioTimeout( ac, () => { - source.disconnect(); destroyAudioWorkletNode(source); vibratoOscillator?.stop(); node.disconnect(); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 7f5cbcf23..814c97f41 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -137,7 +137,7 @@ class LFOProcessor extends AudioWorkletProcessor { } } - process(inputs, outputs, parameters) { + process(_inputs, outputs, parameters) { const begin = parameters['begin'][0]; if (currentTime >= parameters.end[0]) { return false; @@ -510,7 +510,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { }, ]; } - process(input, outputs, params) { + process(_input, outputs, params) { if (currentTime <= params.begin[0]) { return true; } @@ -1061,7 +1061,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { constructor(options) { super(options); - this.tables = null; + this.frames = null; this.frameLen = 0; this.numFrames = 0; this.phase = []; @@ -1069,23 +1069,14 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { this.port.onmessage = (e) => { const { type, payload } = e.data || {}; - if (type === 'tables') { - this.tables = payload.mipmaps; + if (type === 'table') { + this.frames = payload.frames; this.frameLen = payload.frameLen; - this.numFrames = this.tables[0].length; + this.numFrames = this.frames.length; } }; } - _chooseMip(dphi) { - const approxHarm = Math.min(64, 1 / Math.max(1e-6, dphi)); - let level = 0; - while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) { - level++; - } - return level; - } - _mirror(x) { return 1 - Math.abs(2 * x - 1); } @@ -1222,11 +1213,13 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } _sampleFrame(frame, phase) { - const pos = phase * frame.length; + const len = frame.length; + const pos = phase * len; const i = pos | 0; const frac = pos - i; - const a = frame[i % frame.length]; - const b = frame[(i + 1) % frame.length]; + const a = frame[i]; + const i1 = i + 1 < len ? i + 1 : 0; // fast wrap + const b = frame[i1]; return a + (b - a) * frac; } @@ -1240,23 +1233,23 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const outL = outputs[0][0]; const outR = outputs[0][1] || outputs[0][0]; - if (!this.tables) { + if (!this.frames) { outL.fill(0); if (outR !== outL) outR.set(outL); return true; } - + const invSR = 1 / sampleRate; for (let i = 0; i < outL.length; i++) { const detune = pv(parameters.detune, i); - const tablePos = pv(parameters.position, i); + const tablePos = clamp(pv(parameters.position, i), 0, 1); const idx = tablePos * (this.numFrames - 1); const fIdx = idx | 0; const frac = idx - fIdx; - const warpAmount = pv(parameters.warp, i); + const warpAmount = clamp(pv(parameters.warp, i), 0, 1); const warpMode = pv(parameters.warpMode, i); const voices = pv(parameters.voices, i); - const phaseRand = pv(parameters.phaserand, i); - const spread = voices > 1 ? pv(parameters.spread, i) : 0; + const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1); + const spread = voices > 1 ? clamp(pv(parameters.spread, i), 0, 1) : 0; const gain1 = Math.sqrt(0.5 - 0.5 * spread); const gain2 = Math.sqrt(0.5 + 0.5 * spread); let f = pv(parameters.frequency, i); @@ -1272,15 +1265,13 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { gainR = gain1; } const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune - const dPhase = fVoice / sampleRate; - const level = this._chooseMip(dPhase); - const table = this.tables[level]; + const dPhase = fVoice * invSR; // warp phase then sample this.phase[n] = this.phase[n] ?? Math.random() * phaseRand; const ph = this._warpPhase(this.phase[n], warpAmount, warpMode); - const s0 = this._sampleFrame(table[fIdx], ph); - const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph); + const s0 = this._sampleFrame(this.frames[fIdx], ph); + const s1 = this._sampleFrame(this.frames[Math.min(this.numFrames - 1, fIdx + 1)], ph); let s = s0 + (s1 - s0) * frac; if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) { s = -s; From bad498a1e512061c663bd42408968e9d5d348a0d Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 30 Sep 2025 16:21:28 -0700 Subject: [PATCH 02/18] Remove unused var --- packages/superdough/worklets.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 814c97f41..aa49ea32a 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1065,7 +1065,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { this.frameLen = 0; this.numFrames = 0; this.phase = []; - this.syncRatio = 1; this.port.onmessage = (e) => { const { type, payload } = e.data || {}; From cdc5e5f3f33d0fcf06d346120124d76c575dd22a Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 30 Sep 2025 19:45:30 -0400 Subject: [PATCH 03/18] working --- packages/superdough/superdoughoutput.mjs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs index 9ef7bcbf3..221f80a26 100644 --- a/packages/superdough/superdoughoutput.mjs +++ b/packages/superdough/superdoughoutput.mjs @@ -135,12 +135,14 @@ export class SuperdoughOutput { } reset() { + this.disconnect() + this.initializeAudio(); + } + disconnect() { this.channelMerger.disconnect(); this.destinationGain.disconnect(); this.destinationGain = null; this.channelMerger = null; - this.nodes = {}; - this.initializeAudio(); } connectToDestination = (input, channels = [0, 1]) => { //This upmix can be removed if correct channel counts are set throughout the app, @@ -172,6 +174,7 @@ export class SuperdoughAudioController { Array.from(this.nodes).forEach((node) => { node.disconnect(); }); + this.nodes = {} this.output.reset(); } From 9f8143e062239658426c483f7ee9433ed839e3cd Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 30 Sep 2025 19:46:53 -0400 Subject: [PATCH 04/18] format --- packages/superdough/superdoughoutput.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs index 221f80a26..afe91e71e 100644 --- a/packages/superdough/superdoughoutput.mjs +++ b/packages/superdough/superdoughoutput.mjs @@ -135,7 +135,7 @@ export class SuperdoughOutput { } reset() { - this.disconnect() + this.disconnect(); this.initializeAudio(); } disconnect() { @@ -174,7 +174,7 @@ export class SuperdoughAudioController { Array.from(this.nodes).forEach((node) => { node.disconnect(); }); - this.nodes = {} + this.nodes = {}; this.output.reset(); } From a2b8407fbb86477ae4e684481c1cb4915f64307b Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 30 Sep 2025 23:08:28 -0500 Subject: [PATCH 05/18] Add back mipmaps; add caching --- packages/superdough/wavetable.mjs | 33 +++++++++++++---------- packages/superdough/worklets.mjs | 45 +++++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 22 deletions(-) diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 505e6ac38..44367f786 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -38,21 +38,25 @@ export const Warpmode = Object.freeze({ FLIP: 21, }); -async function loadWavetableFrames(url, label, frameLen = 2048) { - const buf = await loadBuffer(url, label); - const ch0 = buf.getChannelData(0); - const total = ch0.length; - const numFrames = Math.max(1, Math.floor(total / frameLen)); - const frames = new Array(numFrames); - for (let i = 0; i < numFrames; i++) { - const start = i * frameLen; - frames[i] = ch0.subarray(start, start + frameLen); +const seenKeys = new Set(); +async function getPayload(url, label, frameLen = 2048) { + const key = `${url},${frameLen}`; + if (!seenKeys.has(key)) { + const buf = await loadBuffer(url, label); + const ch0 = buf.getChannelData(0); + const total = ch0.length; + const numFrames = Math.max(1, Math.floor(total / frameLen)); + const frames = new Array(numFrames); + for (let i = 0; i < numFrames; i++) { + const start = i * frameLen; + frames[i] = ch0.subarray(start, start + frameLen); + } + seenKeys.add(key); + return { frames, frameLen, numFrames, key }; } - return { frames, frameLen, numFrames }; + return { frameLen, key }; // worklet will use the cached version } -const loadCache = {}; - function humanFileSize(bytes, si) { var thresh = si ? 1000 : 1024; if (bytes < thresh) return bytes + ' B'; @@ -97,6 +101,7 @@ async function decodeAtNativeRate(arr) { return await tempAC.decodeAudioData(arr); } +const loadCache = {}; const loadBuffer = (url, label) => { url = url.replace('#', '%23'); if (!loadCache[url]) { @@ -211,7 +216,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { } const frequency = getFrequencyFromValue(value); const { url, label } = getCommonSampleInfo(value, tables); - const payload = await loadWavetableFrames(url, label, frameLen); + const payload = await getPayload(url, label, frameLen); let holdEnd = t + duration; if (clip !== undefined) { holdEnd = Math.min(t + clip * duration, holdEnd); @@ -305,7 +310,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t); const envGain = ac.createGain(); const node = source.connect(envGain); - getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); + getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear'); getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd); const handle = { node, source }; const timeoutNode = webAudioTimeout( diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index aa49ea32a..9b190a456 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1043,6 +1043,7 @@ function brownian(x, oct = 4) { return (sum / norm) * 2 - 1; } +const tablesCache = {}; class WavetableOscillatorProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [ @@ -1061,7 +1062,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { constructor(options) { super(options); - this.frames = null; this.frameLen = 0; this.numFrames = 0; this.phase = []; @@ -1069,9 +1069,28 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { this.port.onmessage = (e) => { const { type, payload } = e.data || {}; if (type === 'table') { - this.frames = payload.frames; + const key = payload.key; this.frameLen = payload.frameLen; - this.numFrames = this.frames.length; + if (!tablesCache[key]) { + const tables = [payload.frames]; + let table = tables[0]; + for (let level = 1; level < 1; level++) { + const nextLen = table.length >> 1; + const nextTable = table.map((frame) => { + const avg = new Float32Array(nextLen); + for (let i = 0; i < nextLen; i++) { + avg[i] = (frame[2 * i] + frame[2 * i + 1]) / 2; + } + return avg; + }); + tables.push(nextTable); + table = nextTable; + if (nextLen <= 32) break; + } + tablesCache[key] = tables; + } + this.tables = tablesCache[key]; + this.numFrames = this.tables[0].length; } }; } @@ -1222,6 +1241,15 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { return a + (b - a) * frac; } + _chooseMip(dphi) { + const approxHarm = clamp(dphi, 1e-6, 64); + let level = 0; + while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) { + level++; + } + return level; + } + process(_inputs, outputs, parameters) { if (currentTime >= parameters.end[0]) { return false; @@ -1231,8 +1259,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } const outL = outputs[0][0]; const outR = outputs[0][1] || outputs[0][0]; - - if (!this.frames) { + if (!this.tables) { outL.fill(0); if (outR !== outL) outR.set(outL); return true; @@ -1253,7 +1280,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const gain2 = Math.sqrt(0.5 + 0.5 * spread); let f = pv(parameters.frequency, i); f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune - const normalizer = 0.3 / Math.sqrt(voices); + const normalizer = 1 / Math.sqrt(voices); for (let n = 0; n < voices; n++) { const isOdd = (n & 1) == 1; let gainL = gain1; @@ -1265,12 +1292,14 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { } const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune const dPhase = fVoice * invSR; + const level = this._chooseMip(dPhase); + const table = this.tables[level]; // warp phase then sample this.phase[n] = this.phase[n] ?? Math.random() * phaseRand; const ph = this._warpPhase(this.phase[n], warpAmount, warpMode); - const s0 = this._sampleFrame(this.frames[fIdx], ph); - const s1 = this._sampleFrame(this.frames[Math.min(this.numFrames - 1, fIdx + 1)], ph); + const s0 = this._sampleFrame(table[fIdx], ph); + const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph); let s = s0 + (s1 - s0) * frac; if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) { s = -s; From 3a7ec18d46f5e051796d9b0e92155e40665ba981 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 30 Sep 2025 23:19:55 -0500 Subject: [PATCH 06/18] Move invsr to constructor --- packages/superdough/worklets.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 9b190a456..ccfaf5107 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -1065,6 +1065,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { this.frameLen = 0; this.numFrames = 0; this.phase = []; + this.invSR = 1 / sampleRate; this.port.onmessage = (e) => { const { type, payload } = e.data || {}; @@ -1264,7 +1265,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { if (outR !== outL) outR.set(outL); return true; } - const invSR = 1 / sampleRate; for (let i = 0; i < outL.length; i++) { const detune = pv(parameters.detune, i); const tablePos = clamp(pv(parameters.position, i), 0, 1); @@ -1291,7 +1291,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { gainR = gain1; } const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune - const dPhase = fVoice * invSR; + const dPhase = fVoice * this.invSR; const level = this._chooseMip(dPhase); const table = this.tables[level]; From 8cd497abb245361b4ad2cf469e46b8b13d14247c Mon Sep 17 00:00:00 2001 From: yaxu Date: Wed, 1 Oct 2025 12:54:58 +0200 Subject: [PATCH 07/18] Update website/src/pages/technical-manual/project-start.mdx --- .../src/pages/technical-manual/project-start.mdx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/website/src/pages/technical-manual/project-start.mdx b/website/src/pages/technical-manual/project-start.mdx index 4e6e37636..0866d3e43 100644 --- a/website/src/pages/technical-manual/project-start.mdx +++ b/website/src/pages/technical-manual/project-start.mdx @@ -7,6 +7,21 @@ layout: ../../layouts/MainLayout.astro This Guide shows you the different ways to get started with using Strudel in your own project. +## Respect the license + +First, please take a moment to understand Strudel's free/open source license, +[AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.en.html). + +Here is a lay summary, but check the license for legal definitions and responsibilities. + +* You can distribute modified versions if you keep track of the changes and the date you made them. +* You must license derivative work under the same license. +* Source code must be distributed along with web publication. + +Among other things, it means that when you share your work, the whole application must be shared under the same free/open source license, or one compatible with it. This is because we want Strudel to stay free/open source. In other words, you are not permitted to distribute integrations of Strudel with libraries or other code that does not have a compatible free/open source license. + +This also applies to clones informed by reading Strudel's source code, as legally speaking, that counts as a 'derivative work'. Again, please [read the licence](https://www.gnu.org/licenses/agpl-3.0.en.html) for details. + ## Embedding the Strudel REPL There are 3 quick ways to embed strudel in your website: From 0ecacae71eddeb7f975b4614a58d3a59c0b67264 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 1 Oct 2025 12:02:09 +0100 Subject: [PATCH 08/18] format --- website/src/pages/technical-manual/project-start.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/website/src/pages/technical-manual/project-start.mdx b/website/src/pages/technical-manual/project-start.mdx index 0866d3e43..4a269d1b9 100644 --- a/website/src/pages/technical-manual/project-start.mdx +++ b/website/src/pages/technical-manual/project-start.mdx @@ -9,16 +9,16 @@ This Guide shows you the different ways to get started with using Strudel in you ## Respect the license -First, please take a moment to understand Strudel's free/open source license, +First, please take a moment to understand Strudel's free/open source license, [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.en.html). Here is a lay summary, but check the license for legal definitions and responsibilities. -* You can distribute modified versions if you keep track of the changes and the date you made them. -* You must license derivative work under the same license. -* Source code must be distributed along with web publication. +- You can distribute modified versions if you keep track of the changes and the date you made them. +- You must license derivative work under the same license. +- Source code must be distributed along with web publication. -Among other things, it means that when you share your work, the whole application must be shared under the same free/open source license, or one compatible with it. This is because we want Strudel to stay free/open source. In other words, you are not permitted to distribute integrations of Strudel with libraries or other code that does not have a compatible free/open source license. +Among other things, it means that when you share your work, the whole application must be shared under the same free/open source license, or one compatible with it. This is because we want Strudel to stay free/open source. In other words, you are not permitted to distribute integrations of Strudel with libraries or other code that does not have a compatible free/open source license. This also applies to clones informed by reading Strudel's source code, as legally speaking, that counts as a 'derivative work'. Again, please [read the licence](https://www.gnu.org/licenses/agpl-3.0.en.html) for details. From b7d98dd3709b3837fba16a517aac565c7e732ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Va=CC=81clav=20Volhejn?= Date: Wed, 1 Oct 2025 21:53:28 +0200 Subject: [PATCH 09/18] Fix case sensitivity for synonym search --- website/src/repl/components/panel/Reference.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index 6007667fa..505cf50d2 100644 --- a/website/src/repl/components/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -44,10 +44,10 @@ export function Reference() { return true; } - const lowCaseSearch = search.toLowerCase(); + const lowerCaseSearch = search.toLowerCase(); return ( - entry.name.toLowerCase().includes(lowCaseSearch) || - (entry.synonyms?.some((s) => s.includes(lowCaseSearch)) ?? false) + entry.name.toLowerCase().includes(lowerCaseSearch) || + (entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false) ); }); }, [search]); From f369de13d7116ce24f952251e6e955e193a14126 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 2 Oct 2025 22:45:48 -0500 Subject: [PATCH 10/18] Hook up FM to wavetables --- packages/superdough/wavetable.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 44367f786..220b95896 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -308,6 +308,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { }, ); const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t); + const fm = applyFM(source.parameters.get('frequency'), value, t); const envGain = ac.createGain(); const node = source.connect(envGain); getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear'); @@ -318,6 +319,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { () => { destroyAudioWorkletNode(source); vibratoOscillator?.stop(); + fm?.stop(); node.disconnect(); wtPosModulators?.disconnect(); wtWarpModulators?.disconnect(); From d496919fe507caa6323ce5d33b553e2b2a15d974 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 2 Oct 2025 22:48:41 -0500 Subject: [PATCH 11/18] Import --- packages/superdough/wavetable.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 220b95896..da6abcf76 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -1,6 +1,7 @@ import { getAudioContext, registerSound } from './index.mjs'; import { getCommonSampleInfo } from './util.mjs'; import { + applyFM, applyParameterModulators, destroyAudioWorkletNode, getADSRValues, From 0d0822598351fcb7b0050a2f452f0fada4375fee Mon Sep 17 00:00:00 2001 From: Jieren Chen Date: Sun, 5 Oct 2025 13:53:00 -0400 Subject: [PATCH 12/18] change the trigger handler to match new hap --- packages/mqtt/mqtt.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mqtt/mqtt.mjs b/packages/mqtt/mqtt.mjs index aef01bd93..d74de342d 100644 --- a/packages/mqtt/mqtt.mjs +++ b/packages/mqtt/mqtt.mjs @@ -82,7 +82,7 @@ Pattern.prototype.mqtt = function ( cx.connect(props); } return this.withHap((hap) => { - const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => { + const onTrigger = (hap, currentTime, cps, targetTime) => { let msg_topic = topic; if (!cx || !cx.isConnected()) { return; From 694162a7b1d0c916ffa76461bcb5fb77bf046459 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 8 Oct 2025 14:13:33 +0100 Subject: [PATCH 13/18] fix purity of 'withState' so it returns a new pattern --- packages/core/pattern.mjs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index d0be3f599..72f6371b3 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -98,10 +98,7 @@ export class Pattern { // runs func on query state withState(func) { - return this.withHaps((haps, state) => { - func(state); - return haps; - }); + return new Pattern((state) => this.query(func(state))); } /** From fdde05e75114e8aedb38d89648d83d1c58b3b303 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 8 Oct 2025 14:51:21 +0100 Subject: [PATCH 14/18] add pattern id to query state controls --- packages/core/repl.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 53562f216..8dbfbec75 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -214,7 +214,10 @@ export function repl({ } let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); if (Object.keys(pPatterns).length) { - let patterns = Object.values(pPatterns); + let patterns = []; + for (const [key, value] of Object.entries(pPatterns)) { + patterns.push(value.withState(state => state.setControls({id: key}))); + } if (eachTransform) { // Explicit lambda so only element (not index and array) are passed patterns = patterns.map((x) => eachTransform(x)); @@ -228,6 +231,7 @@ export function repl({ pattern = allTransforms[i](pattern); } } + if (!isPattern(pattern)) { const message = `got "${typeof evaluated}" instead of pattern`; throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.')); From 086596c47ca564743510301a963815572d6b48b0 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 8 Oct 2025 14:52:00 +0100 Subject: [PATCH 15/18] fix setControls to do union with existing controls --- packages/core/state.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/state.mjs b/packages/core/state.mjs index 162dc7da9..928710814 100644 --- a/packages/core/state.mjs +++ b/packages/core/state.mjs @@ -19,9 +19,9 @@ export class State { return this.setSpan(func(this.span)); } - // Returns new State with different controls + // Returns new State with added controls. setControls(controls) { - return new State(this.span, controls); + return new State(this.span, {...this.controls, ...controls}); } } From ffbbc43c8973059a95cfd69d0594e5cac71905ef Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 8 Oct 2025 15:34:51 +0100 Subject: [PATCH 16/18] add cyclist version to the query metadata --- packages/core/cyclist.mjs | 2 +- packages/core/neocyclist.mjs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index ad0961032..59e410c53 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -57,7 +57,7 @@ export class Cyclist { } // query the pattern for events - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' }); haps.forEach((hap) => { if (hap.hasOnset()) { diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 261f08acb..3e412074d 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -38,8 +38,7 @@ export class NeoCyclist { if (this.started === false) { return; } - - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'neocyclist' }); haps.forEach((hap) => { if (hap.hasOnset()) { const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps); From 64f16c4f337c63692b5f89e8226e37a0db82c923 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Wed, 8 Oct 2025 15:35:34 +0100 Subject: [PATCH 17/18] placate format gods --- packages/core/repl.mjs | 2 +- packages/core/state.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 8dbfbec75..171697eb9 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -216,7 +216,7 @@ export function repl({ if (Object.keys(pPatterns).length) { let patterns = []; for (const [key, value] of Object.entries(pPatterns)) { - patterns.push(value.withState(state => state.setControls({id: key}))); + patterns.push(value.withState((state) => state.setControls({ id: key }))); } if (eachTransform) { // Explicit lambda so only element (not index and array) are passed diff --git a/packages/core/state.mjs b/packages/core/state.mjs index 928710814..8aa581954 100644 --- a/packages/core/state.mjs +++ b/packages/core/state.mjs @@ -21,7 +21,7 @@ export class State { // Returns new State with added controls. setControls(controls) { - return new State(this.span, {...this.controls, ...controls}); + return new State(this.span, { ...this.controls, ...controls }); } } From 72eaf96b4fb922fc4aa36ef92d90843f8395b62c Mon Sep 17 00:00:00 2001 From: Erik Fox Date: Sun, 12 Oct 2025 12:30:36 -0400 Subject: [PATCH 18/18] fix: repair REPL sample sources and URL concat (#1640) --- packages/repl/prebake.mjs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/repl/prebake.mjs b/packages/repl/prebake.mjs index 3f421f985..26d875c2b 100644 --- a/packages/repl/prebake.mjs +++ b/packages/repl/prebake.mjs @@ -20,10 +20,13 @@ export async function prebake() { // import('@strudel/osc'), ); // load samples - const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/'; + const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main'; // TODO: move this onto the strudel repo - const ts = 'https://raw.githubusercontent.com/todepond/samples/main/'; + const ts = 'https://raw.githubusercontent.com/todepond/samples/main'; + + const tc = 'https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main'; + await Promise.all([ modulesLoading, registerSynthSounds(), @@ -36,9 +39,9 @@ export async function prebake() { samples(`${ds}/tidal-drum-machines.json`), samples(`${ds}/piano.json`), samples(`${ds}/Dirt-Samples.json`), - samples(`${ds}/uzu-drumkit.json`), samples(`${ds}/vcsl.json`), samples(`${ds}/mridangam.json`), + samples(`${tc}/strudel.json`), ]); aliasBank(`${ts}/tidal-drum-machines-alias.json`);