From 8529516c663ee8c34b8ef108a148c56c1b690fa6 Mon Sep 17 00:00:00 2001 From: Aria Date: Tue, 25 Nov 2025 13:05:27 -0600 Subject: [PATCH 1/5] Add ability to turn mini parsing off with midi-off decorator --- packages/transpiler/test/transpiler.test.mjs | 14 ++++++++ packages/transpiler/transpiler.mjs | 38 ++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/packages/transpiler/test/transpiler.test.mjs b/packages/transpiler/test/transpiler.test.mjs index 02970cf43..68bec9154 100644 --- a/packages/transpiler/test/transpiler.test.mjs +++ b/packages/transpiler/test/transpiler.test.mjs @@ -42,4 +42,18 @@ describe('transpiler', () => { [12, 14], ]); }); + it('allows disabling mini', () => { + const code = `/* mini-off */ + const randPrefix = Math.random() > 0.5 ? "b" : "s"; + const drumPat = \`\${randPrefix}d\`; + // mini-on + s(drumPat).lpf("5000 10000") // make sure mini still runs; + `; + const { output, miniLocations } = transpiler(code, { ...simple, emitMiniLocations: true }); + expect(output).not.toContain("m('b'"); + expect(output).not.toContain("m('s'"); + const cutoffIdx = code.indexOf('5000 10000'); + expect(miniLocations).toHaveLength(2); + expect(miniLocations[0][0]).toEqual(cutoffIdx); + }); }); diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index fea6bad4d..b394591c2 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -21,12 +21,15 @@ export function registerLanguage(type, config) { export function transpiler(input, options = {}) { const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options; + const comments = []; let ast = parse(input, { ecmaVersion: 2022, allowAwaitOutsideFunction: true, locations: true, + onComment: comments, }); + const miniDisableRanges = findMiniDisableRanges(comments, input.length); let miniLocations = []; const collectMiniLocations = (value, node) => { const minilang = languages.get('minilang'); @@ -66,6 +69,9 @@ export function transpiler(input, options = {}) { return this.replace(tidalWithLocation(raw, offset)); } if (isBackTickString(node, parent)) { + if (isMiniDisabled(node.start, miniDisableRanges)) { + return; + } const { quasis } = node; const { raw } = quasis[0].value; this.skip(); @@ -73,6 +79,9 @@ export function transpiler(input, options = {}) { return this.replace(miniWithLocation(raw, node)); } if (isStringWithDoubleQuotes(node)) { + if (isMiniDisabled(node.start, miniDisableRanges)) { + return; + } const { value } = node; this.skip(); emitMiniLocations && collectMiniLocations(value, node); @@ -327,3 +336,32 @@ function languageWithLocation(name, value, offset) { optional: false, }; } + +function findMiniDisableRanges(comments, codeEnd) { + const ranges = []; + const stack = []; // used to track on/off pairs + for (const comment of comments) { + const value = comment.value.trim(); + if (value.startsWith('mini-off')) { + stack.push(comment.start); + } else if (value.startsWith('mini-on')) { + const start = stack.pop(); + ranges.push([start, comment.end]); + } + } + while (stack.length) { + // If no closing mini-on is found, just turn it off until `codeEnd` + const start = stack.pop(); + ranges.push([start, codeEnd]); + } + return ranges; +} + +function isMiniDisabled(offset, miniDisableRanges) { + for (const [start, end] of miniDisableRanges) { + if (offset >= start && offset < end) { + return true; + } + } + return false; +} From a79491dd16fbe10460d5263c77273abe1b305113 Mon Sep 17 00:00:00 2001 From: Aria Date: Mon, 8 Dec 2025 16:32:54 -0600 Subject: [PATCH 2/5] Update loopbegin/end to not be offset --- packages/superdough/sampler.mjs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 2deeb4b74..8e46426d3 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -65,21 +65,22 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => { bufferSource.playbackRate.value = playbackRate; const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue; + const bufferDuration = bufferSource.buffer.duration; - // "The computation of the offset into the sound is performed using the sound buffer's natural sample rate, - // rather than the current playback rate, so even if the sound is playing at twice its normal speed, - // the midway point through a 10-second audio buffer is still 5." - const offset = begin * bufferSource.buffer.duration; + // The computation of the offset into the sound is performed using the sound buffer's natural duration, + // rather than the playback duration, so that even if the sound is playing at twice its normal speed, + // the midway point through a 10-second audio buffer is still 5. + const offset = begin * bufferDuration; const loop = hapValue.loop; if (loop) { bufferSource.loop = true; - bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset; - bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset; + bufferSource.loopStart = loopBegin * bufferDuration; + bufferSource.loopEnd = loopEnd * bufferDuration; } - const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value; - const sliceDuration = (end - begin) * bufferDuration; - return { bufferSource, offset, bufferDuration, sliceDuration }; + const playbackDuration = bufferDuration / bufferSource.playbackRate.value; + const sliceDuration = (end - begin) * playbackDuration; + return { bufferSource, offset, bufferDuration, playbackDuration, sliceDuration }; }; export const loadBuffer = (url, ac, s, n = 0) => { From b4b01944550e6f678aaa20b35237a39eb92ca02b Mon Sep 17 00:00:00 2001 From: Aria Date: Mon, 5 Jan 2026 23:17:40 -0600 Subject: [PATCH 3/5] Update docstrings and allow modulator ids to be patterns --- packages/core/controls.mjs | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index b18a8dff6..8fef91294 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2844,7 +2844,7 @@ registerSubControls('lfo', [ ['dcoffset', 'dc'], ['shape', 'sh'], ['skew', 'sk'], - ['curve'], + ['curve', 'cu'], ['sync', 's'], ['fxi'], ]); @@ -2872,7 +2872,7 @@ registerSubControls('bmod', [ ['fxi'], ]); -Pattern.prototype.modulate = function (type, config, id) { +Pattern.prototype.modulate = function (type, config, idPat) { config = { control: undefined, ...config }; const modulatorKeys = ['lfo', 'env', 'bmod']; if (!modulatorKeys.includes(type)) { @@ -2881,11 +2881,14 @@ Pattern.prototype.modulate = function (type, config, id) { } let output = this; let defaultValue = undefined; + // Copy value into a temporary `v` container and attach a single `id` (to be shared across + // each config entry). At the output we destructure and throw away the id + output = output.fmap((v) => (id) => ({ v, id })).appLeft(reify(idPat)); for (const [rawKey, value] of Object.entries(config)) { const key = getMainSubcontrolName(type, rawKey); const valuePat = reify(value); output = output - .fmap((v) => (c) => { + .fmap(({ v, id }) => (c) => { if (defaultValue === undefined) { // default control to the control set just before this in the chain // e.g. pat.gain(0.5).lfo({..}) will be a gain-LFO @@ -2900,17 +2903,17 @@ Pattern.prototype.modulate = function (type, config, id) { id ??= t.__ids.size; t[id] ??= { control: defaultValue }; t.__ids.add(id); // keeps track of insertion order - if (c === undefined) return v; + if (c === undefined) return { v, id }; if (key === 'control' || key === 'subControl') { t[id][key] = getControlName(c); } else { t[id][key] = c; } - return v; + return { v, id }; }) .appLeft(valuePat); } - return output; + return output.fmap(({ v }) => v); }; /** @@ -2925,15 +2928,15 @@ Pattern.prototype.modulate = function (type, config, id) { * * @name lfo * @param {Object} config LFO configuration. - * @param {string | Pattern} [config.control] Node to modulate. Aliases: t - * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p - * @param {number | Pattern} [config.rate] Modulation rate. Aliases: rate, r + * @param {string | Pattern} [config.control] Node to modulate. Aliases: c + * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc + * @param {number | Pattern} [config.rate] Modulation rate. Aliases: r * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da * @param {number | Pattern} [config.dcoffset] DC offset / bias for the waveform. Aliases: dc * @param {number | Pattern} [config.shape] Shape index. Aliases: sh * @param {number | Pattern} [config.skew] Skew amount. Aliases: sk - * @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: c + * @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: cu * @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s * @param {number | Pattern} [config.fxi] FX index to target * @param {string | Pattern} id ID to use for this modulator @@ -2978,8 +2981,8 @@ export const lfo = (config) => pure({}).lfo(config); * * @name env * @param {Object} config Envelope configuration. - * @param {string | Pattern} [config.control] Node to modulate. Aliases: t - * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p + * @param {string | Pattern} [config.control] Node to modulate. Aliases: c + * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da * @param {number | Pattern} [config.attack] Time to reach depth. Aliases: att, a @@ -3039,8 +3042,8 @@ export const env = (config) => pure({}).env(config); * @name bmod * @param {Object} config Bus modulation configuration. * @param {string | Pattern} [config.bus] Bus to get modulation signal from - * @param {string | Pattern} [config.control] Node to modulate. Aliases: t - * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p + * @param {string | Pattern} [config.control] Node to modulate. Aliases: c + * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da * @param {number | Pattern} [config.dc] DC offset prior to application From 959e8ac7bc43fece46261cf6c8258fbf040bdede Mon Sep 17 00:00:00 2001 From: yaxu Date: Tue, 6 Jan 2026 16:31:51 +0100 Subject: [PATCH 4/5] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 779769d57..59e2483b3 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ Live coding patterns on the web https://strudel.cc/ +https://codeberg.org/uzu/strudel/ *(Along with many other live coding projects, we have moved from Microsoft's Github platform to Codeberg for ethical reasons. Please don't fork the project back to github.)* - Try it here: - Docs: From 37dd8343989d1c75ef682205189f3f01bb25c7f9 Mon Sep 17 00:00:00 2001 From: yaxu Date: Tue, 6 Jan 2026 16:33:06 +0100 Subject: [PATCH 5/5] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 59e2483b3..b1208e7bb 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@ # strudel Live coding patterns on the web -https://strudel.cc/ -https://codeberg.org/uzu/strudel/ *(Along with many other live coding projects, we have moved from Microsoft's Github platform to Codeberg for ethical reasons. Please don't fork the project back to github.)* + - Try it here: - Docs: +- Source: https://codeberg.org/uzu/strudel/ + * Along with many other live coding projects, we have moved from Microsoft's Github platform to Codeberg for ethical reasons. **Please don't fork the project back to github**. - Technical Blog Post: - 1 Year of Strudel Blog Post: - 2 Years of Strudel Blog Post: + ## Running Locally After cloning the project, you can run the REPL locally: