Merge branch 'main' into main

This commit is contained in:
1d10t 2026-01-08 09:44:32 +01:00
commit a1d1bd0e11
5 changed files with 83 additions and 24 deletions

View file

@ -1,14 +1,17 @@
# strudel # strudel
Live coding patterns on the web Live coding patterns on the web
https://strudel.cc/
- Try it here: <https://strudel.cc> - Try it here: <https://strudel.cc>
- Docs: <https://strudel.cc/learn> - Docs: <https://strudel.cc/learn>
- 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: <https://loophole-letters.vercel.app/strudel> - Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year> - 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
- 2 Years of Strudel Blog Post: <https://strudel.cc/blog/#year-2> - 2 Years of Strudel Blog Post: <https://strudel.cc/blog/#year-2>
## Running Locally ## Running Locally
After cloning the project, you can run the REPL locally: After cloning the project, you can run the REPL locally:

View file

@ -2844,7 +2844,7 @@ registerSubControls('lfo', [
['dcoffset', 'dc'], ['dcoffset', 'dc'],
['shape', 'sh'], ['shape', 'sh'],
['skew', 'sk'], ['skew', 'sk'],
['curve'], ['curve', 'cu'],
['sync', 's'], ['sync', 's'],
['fxi'], ['fxi'],
]); ]);
@ -2872,7 +2872,7 @@ registerSubControls('bmod', [
['fxi'], ['fxi'],
]); ]);
Pattern.prototype.modulate = function (type, config, id) { Pattern.prototype.modulate = function (type, config, idPat) {
config = { control: undefined, ...config }; config = { control: undefined, ...config };
const modulatorKeys = ['lfo', 'env', 'bmod']; const modulatorKeys = ['lfo', 'env', 'bmod'];
if (!modulatorKeys.includes(type)) { if (!modulatorKeys.includes(type)) {
@ -2881,11 +2881,14 @@ Pattern.prototype.modulate = function (type, config, id) {
} }
let output = this; let output = this;
let defaultValue = undefined; 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)) { for (const [rawKey, value] of Object.entries(config)) {
const key = getMainSubcontrolName(type, rawKey); const key = getMainSubcontrolName(type, rawKey);
const valuePat = reify(value); const valuePat = reify(value);
output = output output = output
.fmap((v) => (c) => { .fmap(({ v, id }) => (c) => {
if (defaultValue === undefined) { if (defaultValue === undefined) {
// default control to the control set just before this in the chain // default control to the control set just before this in the chain
// e.g. pat.gain(0.5).lfo({..}) will be a gain-LFO // 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; id ??= t.__ids.size;
t[id] ??= { control: defaultValue }; t[id] ??= { control: defaultValue };
t.__ids.add(id); // keeps track of insertion order 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') { if (key === 'control' || key === 'subControl') {
t[id][key] = getControlName(c); t[id][key] = getControlName(c);
} else { } else {
t[id][key] = c; t[id][key] = c;
} }
return v; return { v, id };
}) })
.appLeft(valuePat); .appLeft(valuePat);
} }
return output; return output.fmap(({ v }) => v);
}; };
/** /**
@ -2925,15 +2928,15 @@ Pattern.prototype.modulate = function (type, config, id) {
* *
* @name lfo * @name lfo
* @param {Object} config LFO configuration. * @param {Object} config LFO configuration.
* @param {string | Pattern} [config.control] Node to modulate. Aliases: t * @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, p * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc
* @param {number | Pattern} [config.rate] Modulation rate. Aliases: rate, r * @param {number | Pattern} [config.rate] Modulation rate. Aliases: r
* @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr * @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.depthabs] Absolute modulation depth. Aliases: da
* @param {number | Pattern} [config.dcoffset] DC offset / bias for the waveform. Aliases: dc * @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.shape] Shape index. Aliases: sh
* @param {number | Pattern} [config.skew] Skew amount. Aliases: sk * @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.sync] Tempo-synced modulation rate. Aliases: s
* @param {number | Pattern} [config.fxi] FX index to target * @param {number | Pattern} [config.fxi] FX index to target
* @param {string | Pattern} id ID to use for this modulator * @param {string | Pattern} id ID to use for this modulator
@ -2978,8 +2981,8 @@ export const lfo = (config) => pure({}).lfo(config);
* *
* @name env * @name env
* @param {Object} config Envelope configuration. * @param {Object} config Envelope configuration.
* @param {string | Pattern} [config.control] Node to modulate. Aliases: t * @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, p * @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.depth] Relative modulation depth. Aliases: dep, dr
* @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da
* @param {number | Pattern} [config.attack] Time to reach depth. Aliases: att, a * @param {number | Pattern} [config.attack] Time to reach depth. Aliases: att, a
@ -3039,8 +3042,8 @@ export const env = (config) => pure({}).env(config);
* @name bmod * @name bmod
* @param {Object} config Bus modulation configuration. * @param {Object} config Bus modulation configuration.
* @param {string | Pattern} [config.bus] Bus to get modulation signal from * @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.control] Node to modulate. Aliases: c
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc, p * @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.depth] Relative modulation depth. Aliases: dep, dr
* @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da
* @param {number | Pattern} [config.dc] DC offset prior to application * @param {number | Pattern} [config.dc] DC offset prior to application

View file

@ -65,21 +65,22 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
bufferSource.playbackRate.value = playbackRate; bufferSource.playbackRate.value = playbackRate;
const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue; 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, // The computation of the offset into the sound is performed using the sound buffer's natural duration,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed, // 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." // the midway point through a 10-second audio buffer is still 5.
const offset = begin * bufferSource.buffer.duration; const offset = begin * bufferDuration;
const loop = hapValue.loop; const loop = hapValue.loop;
if (loop) { if (loop) {
bufferSource.loop = true; bufferSource.loop = true;
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset; bufferSource.loopStart = loopBegin * bufferDuration;
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset; bufferSource.loopEnd = loopEnd * bufferDuration;
} }
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value; const playbackDuration = bufferDuration / bufferSource.playbackRate.value;
const sliceDuration = (end - begin) * bufferDuration; const sliceDuration = (end - begin) * playbackDuration;
return { bufferSource, offset, bufferDuration, sliceDuration }; return { bufferSource, offset, bufferDuration, playbackDuration, sliceDuration };
}; };
export const loadBuffer = (url, ac, s, n = 0) => { export const loadBuffer = (url, ac, s, n = 0) => {

View file

@ -42,4 +42,18 @@ describe('transpiler', () => {
[12, 14], [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);
});
}); });

View file

@ -21,12 +21,15 @@ export function registerLanguage(type, config) {
export function transpiler(input, options = {}) { export function transpiler(input, options = {}) {
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options; const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
const comments = [];
let ast = parse(input, { let ast = parse(input, {
ecmaVersion: 2022, ecmaVersion: 2022,
allowAwaitOutsideFunction: true, allowAwaitOutsideFunction: true,
locations: true, locations: true,
onComment: comments,
}); });
const miniDisableRanges = findMiniDisableRanges(comments, input.length);
let miniLocations = []; let miniLocations = [];
const collectMiniLocations = (value, node) => { const collectMiniLocations = (value, node) => {
const minilang = languages.get('minilang'); const minilang = languages.get('minilang');
@ -66,6 +69,9 @@ export function transpiler(input, options = {}) {
return this.replace(tidalWithLocation(raw, offset)); return this.replace(tidalWithLocation(raw, offset));
} }
if (isBackTickString(node, parent)) { if (isBackTickString(node, parent)) {
if (isMiniDisabled(node.start, miniDisableRanges)) {
return;
}
const { quasis } = node; const { quasis } = node;
const { raw } = quasis[0].value; const { raw } = quasis[0].value;
this.skip(); this.skip();
@ -73,6 +79,9 @@ export function transpiler(input, options = {}) {
return this.replace(miniWithLocation(raw, node)); return this.replace(miniWithLocation(raw, node));
} }
if (isStringWithDoubleQuotes(node)) { if (isStringWithDoubleQuotes(node)) {
if (isMiniDisabled(node.start, miniDisableRanges)) {
return;
}
const { value } = node; const { value } = node;
this.skip(); this.skip();
emitMiniLocations && collectMiniLocations(value, node); emitMiniLocations && collectMiniLocations(value, node);
@ -327,3 +336,32 @@ function languageWithLocation(name, value, offset) {
optional: false, 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;
}