From 65dd79e374ce791bf67c7de217b45c10de20f7ff Mon Sep 17 00:00:00 2001 From: "Lu[ke] Wilson" Date: Sat, 29 Nov 2025 17:44:33 +0000 Subject: [PATCH 01/12] add lots of synonyms --- packages/core/controls.mjs | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 21a6d9624..0f9ba4d84 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -465,6 +465,7 @@ export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm'); * * @name fmenv * @param {number | Pattern} type lin | exp + * @synonyms fme * @example * note("c e g b g e") * .fm(4) @@ -474,12 +475,13 @@ export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm'); * ._scope() * */ -export const { fmenv } = registerControl('fmenv'); +export const { fmenv } = registerControl('fmenv', 'fme'); /** * Attack time for the FM envelope: time it takes to reach maximum modulation * * @name fmattack * @param {number | Pattern} time attack time + * @synonyms fmatt * @example * note("c e g b g e") * .fm(4) @@ -487,7 +489,7 @@ export const { fmenv } = registerControl('fmenv'); * ._scope() * */ -export const { fmattack } = registerControl('fmattack'); +export const { fmattack } = registerControl('fmattack', 'fmatt'); /** * Waveform of the fm modulator @@ -507,6 +509,7 @@ export const { fmwave } = registerControl('fmwave'); * * @name fmdecay * @param {number | Pattern} time decay time + * @synonyms fmdec * @example * note("c e g b g e") * .fm(4) @@ -515,12 +518,13 @@ export const { fmwave } = registerControl('fmwave'); * ._scope() * */ -export const { fmdecay } = registerControl('fmdecay'); +export const { fmdecay } = registerControl('fmdecay', 'fmdec'); /** * Sustain level for the FM envelope: how much modulation is applied after the decay phase * * @name fmsustain * @param {number | Pattern} level sustain level + * @synonyms fmsus * @example * note("c e g b g e") * .fm(4) @@ -529,10 +533,10 @@ export const { fmdecay } = registerControl('fmdecay'); * ._scope() * */ -export const { fmsustain } = registerControl('fmsustain'); +export const { fmsustain } = registerControl('fmsustain', 'fmsus'); // these are not really useful... skipping for now -export const { fmrelease } = registerControl('fmrelease'); -export const { fmvelocity } = registerControl('fmvelocity'); +export const { fmrelease } = registerControl('fmrelease', 'fmrel'); +export const { fmvelocity } = registerControl('fmvelocity', 'fmvel'); /** * Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`. @@ -862,7 +866,7 @@ export const { duckonset } = registerControl('duckonset', 'duckons'); * Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`. * * @name duckattack - * @synonyms duckatt + * @synonyms duckatt, datt * * @param {number | Pattern} time The attack time in seconds * @example @@ -874,20 +878,20 @@ export const { duckonset } = registerControl('duckonset', 'duckons'); * ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:5").duckattack("0.4:0.1") * */ -export const { duckattack } = registerControl('duckattack', 'duckatt'); +export const { duckattack } = registerControl('duckattack', 'duckatt', 'datt'); /** * Create byte beats with custom expressions * * @name byteBeatExpression - * @synonyms bbexpr + * @synonyms bbexpr, bb * * @param {number | Pattern} byteBeatExpression bitwise expression for creating bytebeat * @example * s("bytebeat").bbexpr('t*(t>>15^t>>66)') * */ -export const { byteBeatExpression, bbexpr } = registerControl('byteBeatExpression', 'bbexpr'); +export const { byteBeatExpression, bbexpr } = registerControl('byteBeatExpression', 'bbexpr', 'bb'); /** * Create byte beats with custom expressions @@ -931,24 +935,26 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']); * Controls the lfo rate for the pulsewidth of the pulse oscillator * * @name pwrate + * @synonyms pwr * @param {number | Pattern} rate * @example * n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>") * */ -export const { pwrate } = registerControl('pwrate'); +export const { pwrate } = registerControl('pwrate', 'pwr'); /** * Controls the lfo sweep for the pulsewidth of the pulse oscillator * * @name pwsweep + * @synonyms pws * @param {number | Pattern} sweep * @example * n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>") * */ -export const { pwsweep } = registerControl('pwsweep'); +export const { pwsweep } = registerControl('pwsweep', 'pws'); /** * Phaser audio effect that approximates popular guitar pedals. @@ -1615,12 +1621,12 @@ export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', * * @name delaysync * @param {number | Pattern} cycles delay length in cycles - * @synonyms delayt, dt + * @synonyms delays, ds * @example * s("bd bd").delay(.25).delaysync("<1 2 3 5>".div(8)) * */ -export const { delaysync } = registerControl('delaysync'); +export const { delaysync } = registerControl('delaysync', 'delays', 'ds'); /** * Specifies whether delaytime is calculated relative to cps. From fd1e3d248aa3b2d429456c73a1de17cd19253e5c Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Tue, 23 Dec 2025 01:47:06 -0800 Subject: [PATCH 02/12] block based eval --- packages/codemirror/block_utilities.mjs | 51 +++ packages/codemirror/codemirror.mjs | 85 ++++- packages/codemirror/flash.mjs | 8 +- packages/codemirror/highlight.mjs | 229 ++++++++++---- packages/codemirror/slider.mjs | 123 +++++++- packages/codemirror/widget.mjs | 240 +++++++++++--- packages/core/evaluate.mjs | 28 ++ packages/core/repl.mjs | 296 ++++++++++++++++-- packages/draw/draw.mjs | 10 + packages/transpiler/transpiler.mjs | 209 +++++++++++-- .../src/repl/components/panel/SettingsTab.jsx | 6 + website/src/repl/useReplContext.jsx | 19 +- website/src/settings.mjs | 2 + 13 files changed, 1106 insertions(+), 200 deletions(-) create mode 100644 packages/codemirror/block_utilities.mjs diff --git a/packages/codemirror/block_utilities.mjs b/packages/codemirror/block_utilities.mjs new file mode 100644 index 000000000..4b53755dc --- /dev/null +++ b/packages/codemirror/block_utilities.mjs @@ -0,0 +1,51 @@ +// Block-based evaluation utilities + +export function getBlockRegions(code) { + const chars = code.split(''); + let i = 0, + blanks = [], + blockStart = 0, + regions = []; + while (i < chars.length) { + const isBlank = chars[i] === '\n'; + if (isBlank) { + blanks.push(i); + } else if (chars[i].trim() !== '') { + if (blanks.length > 1) { + regions.push([blockStart, blanks[0]]); + blockStart = i; + } + blanks = []; + } + i++; + } + regions.push([blockStart, blanks.length ? blanks[0] : i]); + return regions; +} + +export function getBlockAt(code, cursor) { + const regions = getBlockRegions(code); + for (const [start, end] of regions) { + if (cursor >= start && cursor <= end) { + return [start, end]; + } + } + return null; +} + +export const evalBlock = (strudelMirror) => { + const { state } = strudelMirror.editor; + const code = state.doc.toString(); + const cursor = state.selection.main.head; + const range = getBlockAt(code, cursor); + if (range) { + const [a, b] = range; + const block = code.slice(a, b); + if (block) { + // Flash the block being evaluated + strudelMirror.flash(200, { from: a, to: b }); + strudelMirror.repl.evaluate(block, true, true, true, { range }); + } + } + return true; +}; diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index a612ab610..71986ba4a 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -13,7 +13,7 @@ import { } from '@codemirror/view'; import { persistentAtom } from '@nanostores/persistent'; import { logger, registerControl, repl } from '@strudel/core'; -import { cleanupDraw, Drawer } from '@strudel/draw'; +import { cleanupDraw, Drawer, cleanupDrawContext } from '@strudel/draw'; import { isAutoCompletionEnabled } from './autocomplete.mjs'; import { basicSetup } from './basicSetup.mjs'; @@ -21,9 +21,13 @@ import { flash, isFlashEnabled } from './flash.mjs'; import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs'; import { keybindings } from './keybindings.mjs'; import { sliderPlugin, updateSliderWidgets } from './slider.mjs'; +import { widgetPlugin, updateWidgets } from './widget.mjs'; import { activateTheme, initTheme, theme } from './themes.mjs'; import { isTooltipEnabled } from './tooltip.mjs'; -import { updateWidgets, widgetPlugin } from './widget.mjs'; +import { getActiveWidgets } from './widget.mjs'; +import { getSliderWidgets } from './slider.mjs'; + +import { evalBlock } from './block_utilities.mjs'; export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands'; @@ -63,6 +67,7 @@ export const defaultSettings = { isLineWrappingEnabled: false, isTabIndentationEnabled: false, isMultiCursorEnabled: false, + isBlockBasedEvalEnabled: false, theme: 'strudelTheme', fontFamily: 'monospace', fontSize: 18, @@ -74,7 +79,7 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS }); // https://codemirror.net/docs/guide/ -export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) { +export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo, strudelMirror }) { const settings = codemirrorSettings.get(); const initialSettings = Object.keys(compartments).map((key) => compartments[key].of(extensions[key](parseBooleans(settings[key]))), @@ -104,11 +109,26 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo keymap.of([ { key: 'Ctrl-Enter', - run: () => onEvaluate?.(), + run: () => { + // issue with referencing settings, this works more reliably + if (strudelMirror?.isBlockBasedEvalEnabled) { + evalBlock(strudelMirror); + return true; + } else { + return onEvaluate?.(); + } + }, }, { key: 'Alt-Enter', - run: () => onEvaluate?.(), + run: () => { + if (strudelMirror?.isBlockBasedEvalEnabled) { + evalBlock(strudelMirror); + return true; + } else { + return onEvaluate?.(); + } + }, }, { key: 'Ctrl-.', @@ -162,6 +182,7 @@ export class StrudelMirror { this.onDraw = onDraw || this.draw; this.id = id || s4(); this.solo = solo; + this.isBlockBasedEvalEnabled = false; // Will be updated via updateSettings() this.drawer = new Drawer((haps, time, _, painters) => { const currentFrame = haps.filter((hap) => hap.isActive(time)); @@ -169,6 +190,7 @@ export class StrudelMirror { this.onDraw(haps, time, painters); }, drawTime); + this.prebaked = prebake(); autodraw && this.drawFirstFrame(); this.repl = repl({ @@ -192,20 +214,28 @@ export class StrudelMirror { cleanupDraw(true, id); } }, - beforeEval: async () => { - cleanupDraw(true, id); + beforeEval: async ({ blockBased } = {}) => { + // Only clean up all drawings for full evaluation + // Block-based eval should preserve animations (like .scope()) from other blocks + if (!blockBased) { + cleanupDraw(true, id); + } await this.prebaked; await replOptions?.beforeEval?.(); }, afterEval: (options) => { // remember for when highlighting is toggled on - this.miniLocations = options.meta?.miniLocations; + this.miniLocations = options.meta?.miniLocations || []; this.widgets = options.meta?.widgets; + const sliders = this.widgets.filter((w) => w.type === 'slider'); - updateSliderWidgets(this.editor, sliders); const widgets = this.widgets.filter((w) => w.type !== 'slider'); - updateWidgets(this.editor, widgets); - updateMiniLocations(this.editor, this.miniLocations); + // range-aware update for block-based evaluation + const range = options.range && options.range.length >= 2 ? options.range : null; + + updateSliderWidgets(this.editor, sliders, range); + updateWidgets(this.editor, widgets, range); + updateMiniLocations(this.editor, this.miniLocations, range); replOptions?.afterEval?.(options); // if no painters are set (.onPaint was not called), then we only need // the present moment (for highlighting) @@ -213,8 +243,14 @@ export class StrudelMirror { this.drawer.setDrawTime(drawTime); // invalidate drawer after we've set the appropriate drawTime this.drawer.invalidate(this.repl.scheduler); + + // Clean up draw context if a non-inline widget was removed + if (options.widgetRemoved) { + cleanupDrawContext(id); + } }, }); + this.cleanupDrawContext = () => cleanupDrawContext(id); this.editor = initEditor({ root, initialCode, @@ -227,7 +263,9 @@ export class StrudelMirror { onEvaluate: () => this.evaluate(), onStop: () => this.stop(), mondo: replOptions.mondo, + strudelMirror: this, }); + const cmEditor = this.root.querySelector('.cm-editor'); if (cmEditor) { this.root.style.display = 'block'; @@ -297,8 +335,9 @@ export class StrudelMirror { this.flash(); await this.repl.evaluate(this.code, autostart); } + async stop() { - this.repl.scheduler.stop(); + this.repl.stop(); } // Listen for global stop requests (e.g., from Vim :q) @@ -317,8 +356,8 @@ export class StrudelMirror { this.evaluate(); } } - flash(ms) { - flash(this.editor, ms); + flash(ms, range) { + flash(this.editor, ms, range); } highlight(haps, time) { highlightMiniLocations(this.editor, time, haps); @@ -350,6 +389,10 @@ export class StrudelMirror { setLineWrappingEnabled(enabled) { this.reconfigureExtension('isLineWrappingEnabled', enabled); } + + setBlockBasedEvalEnabled(enabled) { + this.reconfigureExtension('isBlockBasedEvalEnabled', enabled); + } setBracketMatchingEnabled(enabled) { this.reconfigureExtension('isBracketMatchingEnabled', enabled); } @@ -371,6 +414,10 @@ export class StrudelMirror { for (let key in extensions) { this.reconfigureExtension(key, settings[key]); } + // Update block-based eval setting on the instance + if (settings.isBlockBasedEvalEnabled !== undefined) { + this.isBlockBasedEvalEnabled = parseBooleans(settings.isBlockBasedEvalEnabled); + } const updated = { ...codemirrorSettings.get(), ...settings }; codemirrorSettings.set(updated); } @@ -392,6 +439,16 @@ export class StrudelMirror { }; this.editor.dispatch({ changes }); } + // used for debugging but could serve other purposes + getActiveWidgets() { + return getActiveWidgets(this.editor); + } + getSliderWidgets() { + return getSliderWidgets(this.editor); + } + getMiniLocations() { + return this.miniLocations; + } clear() { this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl); this.onEvaluateRequest && document.removeEventListener('repl-evaluate', this.onEvaluateRequest); diff --git a/packages/codemirror/flash.mjs b/packages/codemirror/flash.mjs index 243500bff..7b2473506 100644 --- a/packages/codemirror/flash.mjs +++ b/packages/codemirror/flash.mjs @@ -14,7 +14,8 @@ export const flashField = StateField.define({ const mark = Decoration.mark({ attributes: { style: `background-color: rgba(255,255,255, .4); filter: invert(10%)` }, }); - flash = Decoration.set([mark.range(0, tr.newDoc.length)]); + const range = e.value.range || { from: 0, to: tr.newDoc.length }; + flash = Decoration.set([mark.range(range.from, range.to)]); } else { flash = Decoration.set([]); } @@ -29,8 +30,9 @@ export const flashField = StateField.define({ provide: (f) => EditorView.decorations.from(f), }); -export const flash = (view, ms = 200) => { - view.dispatch({ effects: setFlash.of(true) }); +export const flash = (view, ms = 200, range) => { + const flashData = range ? { range } : true; + view.dispatch({ effects: setFlash.of(flashData) }); setTimeout(() => { view.dispatch({ effects: setFlash.of(false) }); }, ms); diff --git a/packages/codemirror/highlight.mjs b/packages/codemirror/highlight.mjs index f9f977c6b..5cc6d3a11 100644 --- a/packages/codemirror/highlight.mjs +++ b/packages/codemirror/highlight.mjs @@ -3,8 +3,9 @@ import { Decoration, EditorView } from '@codemirror/view'; export const setMiniLocations = StateEffect.define(); export const showMiniLocations = StateEffect.define(); -export const updateMiniLocations = (view, locations) => { - view.dispatch({ effects: setMiniLocations.of(locations) }); +export const displayMiniLocations = StateEffect.define(); +export const updateMiniLocations = (view, locations, range = null) => { + view.dispatch({ effects: setMiniLocations.of({ locations, range }) }); }; export const highlightMiniLocations = (view, atTime, haps) => { view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) }); @@ -21,23 +22,56 @@ const miniLocations = StateField.define({ for (let e of tr.effects) { if (e.is(setMiniLocations)) { - // this is called on eval, with the mini locations obtained from the transpiler - // codemirror will automatically remap the marks when the document is edited - // create a mark for each mini location, adding the range to the spec to find it later - const marks = e.value - .filter(([from]) => from < tr.newDoc.length) - .map(([from, to]) => [from, Math.min(to, tr.newDoc.length)]) - .map( - (range) => - Decoration.mark({ - id: range.join(':'), - // this green is only to verify that the decoration moves when the document is edited - // it will be removed later, so the mark is not visible by default - attributes: { style: `background-color: #00CA2880` }, - }).range(...range), // -> Decoration - ); + //block-based eval case + if (e.value.range) { + const stateMiniLocations = getMiniLocationsFromDecorations(locations); + const existingById = new Map(stateMiniLocations.map(({ id, from, to }) => [id, [from, to]])); - locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet + const normalized = e.value.locations + .filter(([from]) => from < tr.newDoc.length) + .map(([from, to]) => [from, Math.min(to, tr.newDoc.length)]); + + const newIds = new Set(normalized.map((r) => r.join(':'))); + + const marks = normalized.map((range) => { + const id = range.join(':'); + const useRange = existingById.get(id) || range; + return Decoration.mark({ + id, + // this green is only to verify that the decoration moves when the document is edited + // it will be removed later, so the mark is not visible by default + attributes: { style: `background-color: #00CA2880` }, + }).range(...useRange); // -> Decoration + }); + + const previousMarks = stateMiniLocations + .filter(({ id }) => !newIds.has(id)) + .map(({ from, to, id }) => + Decoration.mark({ + id, + attributes: { style: `background-color: #00CA2880` }, + }).range(from, to), + ); + + locations = Decoration.set(previousMarks.concat(marks), true); // -> DecorationSet === RangeSet + } else { + // this is called on eval, with the mini locations obtained from the transpiler + // codemirror will automatically remap the marks when the document is edited + // create a mark for each mini location, adding the range to the spec to find it later + const marks = e.value.locations + .filter(([from]) => from < tr.newDoc.length) + .map(([from, to]) => [from, Math.min(to, tr.newDoc.length)]) + .map( + (range) => + Decoration.mark({ + id: range.join(':'), + // this green is only to verify that the decoration moves when the document is edited + // it will be removed later, so the mark is not visible by default + attributes: { style: `background-color: #00CA2880` }, + }).range(...range), // -> Decoration + ); + locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet + } } } @@ -75,12 +109,83 @@ const visibleMiniLocations = StateField.define({ }, }); -// // Derive the set of decorations from the miniLocations and visibleLocations -const miniLocationHighlights = EditorView.decorations.compute([miniLocations, visibleMiniLocations], (state) => { - const iterator = state.field(miniLocations).iter(); - const { haps } = state.field(visibleMiniLocations); - const builder = new RangeSetBuilder(); +const displayMiniLocationsState = StateField.define({ + create() { + return true; // default to showing miniLocations + }, + update(display, tr) { + for (let e of tr.effects) { + if (e.is(displayMiniLocations)) { + display = e.value; + } + } + return display; + }, +}); +// // Derive the set of decorations from the miniLocations and visibleLocations +const miniLocationHighlights = EditorView.decorations.compute( + [miniLocations, visibleMiniLocations, displayMiniLocationsState], + (state) => { + // Check if miniLocations display is disabled + const shouldDisplay = state.field(displayMiniLocationsState); + if (!shouldDisplay) { + return Decoration.none; // Return empty decorations if display is disabled + } + + const iterator = state.field(miniLocations).iter(); + const { haps } = state.field(visibleMiniLocations); + const builder = new RangeSetBuilder(); + + while (iterator.value) { + const { + from, + to, + value: { + spec: { id }, + }, + } = iterator; + + if (haps.has(id)) { + const hap = haps.get(id); + const color = hap.value?.color ?? 'var(--foreground)'; + const style = hap.value?.markcss || `outline: solid 2px ${color}`; + // Get explicit channels for color values + /* + const swatch = document.createElement('div'); + swatch.style.color = color; + document.body.appendChild(swatch); + let channels = getComputedStyle(swatch) + .color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/) + .slice(1) + .map((c) => parseFloat(c || 1)); + document.body.removeChild(swatch); + + // Get percentage of event + const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration; + channels[3] *= percent; + */ + + builder.add( + from, + to, + Decoration.mark({ + // attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` }, + attributes: { style }, + }), + ); + } + + iterator.next(); + } + + return builder.finish(); + }, +); + +const getMiniLocationsFromDecorations = (decorations) => { + const iterator = decorations.iter(); + const miniLocationsArray = []; while (iterator.value) { const { from, @@ -89,50 +194,48 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi spec: { id }, }, } = iterator; - - if (haps.has(id)) { - const hap = haps.get(id); - const color = hap.value?.color ?? 'var(--foreground)'; - const style = hap.value?.markcss || `outline: solid 2px ${color}`; - // Get explicit channels for color values - /* - const swatch = document.createElement('div'); - swatch.style.color = color; - document.body.appendChild(swatch); - let channels = getComputedStyle(swatch) - .color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/) - .slice(1) - .map((c) => parseFloat(c || 1)); - document.body.removeChild(swatch); - - // Get percentage of event - const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration; - channels[3] *= percent; - */ - - builder.add( - from, - to, - Decoration.mark({ - // attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` }, - attributes: { style }, - }), - ); - } - + miniLocationsArray.push({ + from, + to, + id, + }); iterator.next(); } + return miniLocationsArray; +}; - return builder.finish(); -}); +export const getMiniLocations = (state) => { + const decorations = state.field(miniLocations); + return getMiniLocationsFromDecorations(decorations); +}; -export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights]; +export const getActiveMiniLocations = (state) => { + const miniLocations = getMiniLocations(state); + const { haps } = state.field(visibleMiniLocations); + + const activeMiniLocations = miniLocations.filter((location) => haps.has(location.id)); + return activeMiniLocations; +}; + +export const highlightExtension = [ + miniLocations, + visibleMiniLocations, + displayMiniLocationsState, + miniLocationHighlights, +]; export const isPatternHighlightingEnabled = (on, config) => { - on && - config && - setTimeout(() => { - updateMiniLocations(config.editor, config.miniLocations); - }, 100); - return on ? Prec.highest(highlightExtension) : []; + // NOTE: + // Modified this function to always return the highlightExtension, and instead just toggle whether or not the miniLocations are displayed. + // This is because block based evaluation only updates regions of miniLocations, and those updates need to be kept track of constantly. + // The setTimeout was also removed because it conflicted with the range-specific updates required by + // block based evaluation. + // Not sure if this is the best approach, but for block based eval I can't think of a better way to do it. + + if (config) { + // Toggle the display state for miniLocations + config.editor.dispatch({ effects: displayMiniLocations.of(on) }); + } + + return Prec.highest(highlightExtension); }; diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index 72f951254..647779ba8 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -2,11 +2,11 @@ import { ref, pure } from '@strudel/core'; import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view'; import { StateEffect } from '@codemirror/state'; +// Global state storage for all widget types export let sliderValues = {}; -const getSliderID = (from) => `slider_${from}`; export class SliderWidget extends WidgetType { - constructor(value, min, max, from, to, step, view) { + constructor(value, min, max, from, to, step, view, id) { super(); this.value = value; this.min = min; @@ -16,10 +16,21 @@ export class SliderWidget extends WidgetType { this.to = to; this.step = step; this.view = view; + this.id = id || `${from}:${to}`; // Range-based ID for stability } - eq() { - return false; + eq(other) { + if (!(other instanceof SliderWidget)) { + return false; + } + return ( + this.id === other.id && + this.from === other.from && + this.to === other.to && + this.value === other.value && + this.min === other.min && + this.max === other.max + ); } toDOM() { @@ -38,6 +49,7 @@ export class SliderWidget extends WidgetType { slider.from = this.from; slider.originalFrom = this.originalFrom; slider.to = this.to; + slider.id = this.id; // Store range-based ID in DOM element slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)'; this.slider = slider; slider.addEventListener('input', (e) => { @@ -49,7 +61,7 @@ export class SliderWidget extends WidgetType { slider.originalValue = insert; slider.value = insert; this.view.dispatch({ changes: change }); - const id = getSliderID(slider.originalFrom); // matches id generated in transpiler + const id = slider.id; // Use range-based ID window.postMessage({ type: 'cm-slider', value: Number(next), id }); }); return wrap; @@ -62,19 +74,60 @@ export class SliderWidget extends WidgetType { export const setSliderWidgets = StateEffect.define(); -export const updateSliderWidgets = (view, widgets) => { - view.dispatch({ effects: setSliderWidgets.of(widgets) }); +export const setSliderWidgetsInRange = StateEffect.define(); + +export const updateSliderWidgets = (view, widgets, range = null) => { + if (range) { + // range argument passed for block-based evaluation + view.dispatch({ effects: setSliderWidgetsInRange.of({ widgets, range }) }); + } else { + view.dispatch({ effects: setSliderWidgets.of(widgets) }); + } }; function getSliders(widgetConfigs, view) { - return widgetConfigs - .filter((w) => w.type === 'slider') - .map(({ from, to, value, min, max, step }) => { - return Decoration.widget({ - widget: new SliderWidget(value, min, max, from, to, step, view), - side: 0, - }).range(from /* , to */); - }); + return ( + widgetConfigs + .filter((w) => w.type === 'slider') + // Deduplicate sliders that might appear multiple times (e.g., during paste operations) + .filter((slider, index, self) => index === self.findIndex((s) => s.from === slider.from && s.to === slider.to)) + .sort((a, b) => a.from - b.from) + .map(({ from, to, value, min, max, step, id }) => { + return Decoration.widget({ + widget: new SliderWidget(value, min, max, from, to, step, view, id), + side: 0, + }).range(from /* , to */); + }) + ); +} + +export function getSliderWidgets(view) { + if (!view || !view.state) { + return []; + } + + const sliderPluginInstance = view.plugin(sliderPlugin); + if (!sliderPluginInstance || !sliderPluginInstance.decorations) { + return []; + } + + const sliderWidgets = []; + + sliderPluginInstance.decorations.between(0, view.state.doc.length, (from, to, decoration) => { + if (decoration.widget instanceof SliderWidget) { + sliderWidgets.push({ + type: 'slider', + from: decoration.widget.from, + to: decoration.widget.to, + value: decoration.widget.value, + min: decoration.widget.min, + max: decoration.widget.max, + step: decoration.widget.step, + }); + } + }); + + return sliderWidgets; } export const sliderPlugin = ViewPlugin.fromClass( @@ -101,7 +154,42 @@ export const sliderPlugin = ViewPlugin.fromClass( } } for (let e of tr.effects) { - if (e.is(setSliderWidgets)) { + if (e.is(setSliderWidgetsInRange)) { + // Block-aware slider update logic + const { widgets, range } = e.value; + const [rangeStart, rangeEnd] = range; + + // Get existing slider widgets that should be preserved + const existingSliders = []; + this.decorations.between(0, update.view.state.doc.length, (from, to, decoration) => { + if (decoration.widget instanceof SliderWidget) { + // Preserve sliders outside the evaluation range + // Use strict > for rangeEnd because when code is deleted, slider positions + // map to the deletion boundary (rangeEnd), and those should be removed, not preserved + if (from < rangeStart || from > rangeEnd) { + existingSliders.push({ + from, + to, + value: decoration.widget.value, + min: decoration.widget.min, + max: decoration.widget.max, + step: decoration.widget.step, + id: decoration.widget.id || `${from}:${to}`, + type: 'slider', + }); + } + } + }); + + // Merge preserved sliders with new widgets + const mergedWidgets = [...existingSliders, ...widgets] + .filter( + (slider, index, self) => index === self.findIndex((s) => s.type === 'slider' && s.id === slider.id), + ) + .sort((a, b) => a.from - b.from); + + this.decorations = Decoration.set(getSliders(mergedWidgets, update.view)); + } else if (e.is(setSliderWidgets)) { this.decorations = Decoration.set(getSliders(e.value, update.view)); } } @@ -131,6 +219,7 @@ export let sliderWithID = (id, value, min, max) => { sliderValues[id] = value; // sync state at eval time (code -> state) return ref(() => sliderValues[id]); // use state at query time }; + // update state when sliders are moved if (typeof window !== 'undefined') { window.addEventListener('message', (e) => { @@ -139,7 +228,7 @@ if (typeof window !== 'undefined') { // update state when slider is moved sliderValues[e.data.id] = e.data.value; } else { - console.warn(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`); + console.error(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`); } } }); diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs index 42d3b1512..6c40d6733 100644 --- a/packages/codemirror/widget.mjs +++ b/packages/codemirror/widget.mjs @@ -1,55 +1,111 @@ import { StateEffect, StateField } from '@codemirror/state'; -import { Decoration, EditorView, WidgetType } from '@codemirror/view'; +import { Decoration, EditorView, WidgetType, ViewPlugin } from '@codemirror/view'; import { getWidgetID, registerWidgetType } from '@strudel/transpiler'; import { Pattern } from '@strudel/core'; -export const addWidget = StateEffect.define({ - map: ({ from, to }, change) => { - return { from: change.mapPos(from), to: change.mapPos(to) }; - }, -}); +export const setWidgets = StateEffect.define(); -export const updateWidgets = (view, widgets) => { - view.dispatch({ effects: addWidget.of(widgets) }); +export const setWidgetsInRange = StateEffect.define(); + +export const updateWidgets = (view, widgets, range = null) => { + if (range) { + // range argument passed for block-based evaluation + view.dispatch({ effects: setWidgetsInRange.of({ widgets, range }) }); + } else { + view.dispatch({ effects: setWidgets.of(widgets) }); + } }; -function getWidgets(widgetConfigs) { - return ( - widgetConfigs - // codemirror throws an error if we don't sort - .sort((a, b) => a.to - b.to) - .map((widgetConfig) => { +function getWidgets(widgetConfigs, view) { + const filtered = widgetConfigs + // Filter to widget configs only (exclude sliders) + .filter((w) => w && w.type && w.type !== 'slider') + // Deduplicate widgets by ID, matching slider behavior for stable widget identity + .filter((widget, index, self) => index === self.findIndex((w) => w.type === widget.type && w.id === widget.id)); + + // Filter out widgets whose range is encompassed by another widget + // const nonEncompassed = filterEncompassedWidgets(filtered); + + return filtered + .sort((a, b) => (a.to || 0) - (b.to || 0)) + .map((widgetConfig) => { + try { return Decoration.widget({ - widget: new BlockWidget(widgetConfig), + widget: new BlockWidget(widgetConfig, view), side: 0, - block: true, - }).range(widgetConfig.to); - }) - ); + }).range(widgetConfig.to || widgetConfig.from || 0); + } catch (error) { + console.error('error creating widget', error); + return null; + } + }) + .filter(Boolean); // Remove any null results from failed creations } -const widgetField = StateField.define( - /* */ { - create() { - return Decoration.none; - }, - update(widgets, tr) { - widgets = widgets.map(tr.changes); - for (let e of tr.effects) { - if (e.is(addWidget)) { - try { - widgets = widgets.update({ - filter: () => false, - add: getWidgets(e.value), - }); - } catch (error) { - console.log('err', error); +export const widgetPlugin = ViewPlugin.fromClass( + class { + decorations; //: DecorationSet + + constructor(view /* : EditorView */) { + this.decorations = Decoration.set([]); + } + + update(update /* : ViewUpdate */) { + update.transactions.forEach((tr) => { + if (tr.docChanged) { + this.decorations = this.decorations.map(tr.changes); + const iterator = this.decorations.iter(); + // Apply changes to iterator.from and iterator.to if docChanged + while (iterator.value) { + // when the widgets are moved, we need to tell the dom node the current position + // this is important because the widget functions have to work with the dom node + if (iterator.value?.widget instanceof BlockWidget) { + iterator.value.widget.from = iterator.from; + iterator.value.widget.to = iterator.to; + } + iterator.next(); } } - } - return widgets; - }, - provide: (f) => EditorView.decorations.from(f), + for (let e of tr.effects) { + if (e.is(setWidgetsInRange)) { + // Block-aware widget update logic + const { widgets, range } = e.value; + const [rangeStart, rangeEnd] = range; + + // Get existing widget widgets that should be preserved + const existingWidgets = []; + this.decorations.between(0, update.view.state.doc.length, (from, to, decoration) => { + if (decoration.widget instanceof BlockWidget) { + // Preserve widgets outside the evaluation range + // Use strict > for rangeEnd because when code is deleted, widget positions + // map to the deletion boundary (rangeEnd), and those should be removed, not preserved + if (from < rangeStart || from > rangeEnd) { + existingWidgets.push({ + from: decoration.widget.from, + to: decoration.widget.to, + type: decoration.widget.type, + index: decoration.widget.index, + id: decoration.widget.id, + }); + } + } + }); + + // Merge preserved widgets with new widgets, deduplicating by ID + const mergedWidgets = [...existingWidgets, ...widgets].filter( + (widget, index, self) => index === self.findIndex((w) => w.type === widget.type && w.id === widget.id), + ); + + this.decorations = Decoration.set(getWidgets(mergedWidgets, update.view)); + } else if (e.is(setWidgets)) { + this.decorations = Decoration.set(getWidgets(e.value, update.view)); + } + } + }); + } + }, + { + decorations: (v) => v.decorations, }, ); @@ -60,24 +116,116 @@ export function setWidget(id, el) { } export class BlockWidget extends WidgetType { - constructor(widgetConfig) { + constructor(widgetConfig, view) { super(); + + // Graceful handling of invalid configs like sliders + if (!widgetConfig || typeof widgetConfig !== 'object') { + widgetConfig = { type: 'unknown', from: 0, to: 0 }; + } + + this.from = widgetConfig.from || 0; + this.originalFrom = widgetConfig.from || 0; + this.to = widgetConfig.to || this.from; + this.originalTo = widgetConfig.to || this.from; + this.type = widgetConfig.type || 'unknown'; + this.index = widgetConfig.index || 0; + this.view = view; + + // Use range-based ID for stability, similar to sliders + this.id = widgetConfig.id || getWidgetID?.(widgetConfig); this.widgetConfig = widgetConfig; } - eq() { - return true; + + eq(other) { + if (!(other instanceof BlockWidget)) { + return false; + } + return ( + this.id === other.id && + this.from === other.from && + this.to === other.to && + this.type === other.type && + this.index === other.index + ); } + toDOM() { - const id = getWidgetID(this.widgetConfig); - const el = widgetElements[id]; - return el; + let wrap = document.createElement('span'); + wrap.setAttribute('aria-hidden', 'true'); + wrap.className = 'cm-widget-container'; + + let el = widgetElements[this.id]; + if (el) { + // Ensure the element has the correct ID + el.id = this.id; + wrap.appendChild(el); + } else { + // Create a placeholder element if the widget element doesn't exist + // This prevents CodeMirror errors when widget is missing + const placeholder = document.createElement('span'); + placeholder.setAttribute('aria-hidden', 'true'); + placeholder.className = 'cm-widget-placeholder'; + placeholder.style.cssText = 'display: none;'; // Hide placeholder + placeholder.id = this.id; + wrap.appendChild(placeholder); + } + + return wrap; } + ignoreEvent(e) { return true; } } -export const widgetPlugin = [widgetField]; +export function getActiveWidgets(view) { + if (!view || !view.state) { + return []; + } + + const widgetPluginInstance = view.plugin(widgetPlugin); + if (!widgetPluginInstance || !widgetPluginInstance.decorations) { + return []; + } + + const widgets = []; + + widgetPluginInstance.decorations.between(0, view.state.doc.length, (from, to, decoration) => { + if (decoration.widget instanceof BlockWidget) { + widgets.push({ + type: decoration.widget.type, + from: decoration.widget.from, + to: decoration.widget.to, + index: decoration.widget.index, + id: decoration.widget.id, + }); + } + }); + + return widgets; +} + +export function getAllWidgetIds(view) { + if (!view || !view.state) { + return []; + } + + const widgetPluginInstance = view.plugin(widgetPlugin); + if (!widgetPluginInstance || !widgetPluginInstance.decorations) { + return []; + } + + const widgetIds = []; + + widgetPluginInstance.decorations.between(0, view.state.doc.length, (from, to, decoration) => { + if (decoration.widget instanceof BlockWidget) { + widgetIds.push(decoration.widget.id); + } + }); + + return widgetIds; +} // widget implementer API to create a new widget type export function registerWidget(type, fn) { diff --git a/packages/core/evaluate.mjs b/packages/core/evaluate.mjs index 0599d8662..bb7b46648 100644 --- a/packages/core/evaluate.mjs +++ b/packages/core/evaluate.mjs @@ -5,6 +5,34 @@ This program is free software: you can redistribute it and/or modify it under th */ export const strudelScope = {}; +// Make strudelScope available globally so transpiled code can access it +globalThis.strudelScope = strudelScope; + +// Track user-defined keys (from block-based eval) so we can clear them without removing strudel functions +export const userDefinedKeys = new Set(); +globalThis.userDefinedKeys = userDefinedKeys; + +/** + * Clears all user-defined variables and functions from the scope. + * This removes variables created during block-based evaluation. + * @name clearScope + * @example + * // After defining variables in blocks: + * // let myVar = 5 + * // function myFunc() { return 10; } + * clearScope() // removes myVar and myFunc from scope + */ +export const clearScope = () => { + for (const key of userDefinedKeys) { + delete strudelScope[key]; + delete globalThis[key]; + } + userDefinedKeys.clear(); + // Return silence if available (for use in pattern expressions), otherwise undefined + return globalThis.silence; +}; +// Make clearScope available globally +globalThis.clearScope = clearScope; export const evalScope = async (...args) => { const results = await Promise.allSettled(args); diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 470be47bc..4e0af7ab7 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -32,6 +32,7 @@ export function repl({ pattern: undefined, miniLocations: [], widgets: [], + sliders: [], pending: false, started: false, }; @@ -70,11 +71,172 @@ export function repl({ let allTransform; let eachTransform; + // Block-based evaluation state + let codeBlocks = {}; + let lastActiveVisualizerLabel = null; + // Track which patterns belong to which blocks: { blockRange: [patternKeys] } + let blockPatterns = new Map(); + + // Helper function to collect properties from all code blocks (handles both labeled and anonymous blocks) + function collectFromBlocks(property) { + return Object.entries(codeBlocks).flatMap(([key, block]) => { + if (key === '$') { + // Anonymous blocks are stored as an array of block objects + return Array.isArray(block) ? block.flatMap((b) => b[property] || []) : []; + } + // Labeled blocks are stored as single block objects + return block[property] || []; + }); + } + + // Helper function to process a single labeled block + function processLabeledBlock(labels, i, code, options, meta) { + const label = labels[i]; + const nextLabel = labels[i + 1] || { index: code.length, end: code.length }; + + const labelCode = code.slice(label.index, nextLabel.index); + const labelRange = [label.index + options.range[0], label.end + options.range[0]]; + + // Calculate the full block range (from label start to next label start) + const blockStart = label.index + options.range[0]; + const blockEnd = nextLabel.index + options.range[0]; + + const blockWidgets = (meta?.widgets || []).filter((widget) => { + const widgetPos = widget.from ?? widget.index ?? 0; + return widgetPos >= blockStart && widgetPos < blockEnd; + }); + + const blockSliders = (meta?.sliders || []).filter((slider) => { + const sliderPos = slider.from ?? slider.index ?? 0; + return sliderPos >= blockStart && sliderPos < blockEnd; + }); + + const blockMiniLocations = (meta?.miniLocations || []).filter((loc) => { + // const locStart = loc.start ?? loc.from ?? 0; + // mini locations can be either [start, end] arrays or objects with start/from + const locStart = Array.isArray(loc) ? loc[0] : (loc.start ?? loc.from ?? 0); + return locStart >= blockStart && locStart < blockEnd; + }); + + handleSingleLabelBlock( + label, + labelCode, + { ...options, range: labelRange }, + { widgets: blockWidgets, sliders: blockSliders, miniLocations: blockMiniLocations }, + ); + } + + // // Helper function to filter meta (widgets, sliders, miniLocations) by block range + // function splitCodeByRange(meta, blockStart, blockEnd) { + // } + + // Helper function to extract labels from code with their positions + function extractLabelsFromCode(code) { + const labels = []; + // Regex to find label patterns like "d1:" or "myLabel:" + const labelRegex = /^(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:)/gm; + let match; + while ((match = labelRegex.exec(code)) !== null) { + labels.push({ + name: match[2], + index: match.index, + end: match.index + match[1].length, + fullMatch: match[1], + }); + } + + // Also check for 'all()' and treat it as a special label + // just for management purposes + const allRegex = /all\s*\(\s*([^)]+)\s*\)/; + const allMatch = allRegex.exec(code); + if (allMatch) { + // Check if the argument contains a widget call + const argCode = allMatch[1]; + const activeVisualizer = detectActiveVisualizer(argCode); + + labels.push({ + name: 'all', + index: allMatch.index, + end: allMatch.index + allMatch[0].length, + fullMatch: allMatch[0], + activeVisualizer: activeVisualizer, + }); + } + + return labels; + } + + // Helper function to detect non-inline widget calls in code + function detectActiveVisualizer(code) { + // List of non-inline widgets that need cleanup + // These are Pattern.prototype methods that create persistent visualizations + // I don't like this approach, feels hacky, but since these methods don't create ids that + // can be read through the transpiler, I'm not sure how best to detect them. + // Would probably be better to register these methods through some function that would + // tag them better + const nonInlineWidgets = ['punchcard', 'spiral', 'scope', 'pitchwheel', 'spectrum', 'pianoroll', 'wordfall',]; + + for (const widget of nonInlineWidgets) { + const widgetRegex = new RegExp(`\\.${widget}\\s*\\(`); + if (widgetRegex.test(code)) { + return widget; + } + } + return null; + } + + // Helper function to handle single label code block storage + function handleSingleLabelBlock(label, code, options, meta) { + + // Detect if this block contains a non-inline widget + // For 'all' label, widget info is already on the label object from extractLabelsFromCode + + // As mentioned in detectActiveVisualizer, this is a bad approach to + // managing non-inline widgets (or widgets that don't have ids) + // and a proper solution would give them widgets in the transpiler, or at least + // track where they are so we don't have to futz around with regexes + const activeVisualizer = label.activeVisualizer !== undefined + ? label.activeVisualizer + : detectActiveVisualizer(code); + + if (activeVisualizer !== null) { + lastActiveVisualizerLabel = label.name; + } + + // Store the entire code block under the label name + codeBlocks[label.name] = { + code: code, + range: options.range, + labels: [label.name], + miniLocations: meta?.miniLocations || [], + widgets: meta?.widgets || [], + sliders: meta?.sliders || [], + activeVisualizer: activeVisualizer, // Store the widget type if present, null otherwise + }; + + + // Clean up any blocks with conflicting ranges + for (const [existingKey, existingBlock] of Object.entries(codeBlocks)) { + if (existingKey !== label.name && existingBlock.range && options.range) { + const [existingStart, existingEnd] = existingBlock.range; + const [newStart, newEnd] = options.range; + + // If ranges overlap (not just touch), remove the stale block + if (!(newEnd <= existingStart || newStart >= existingEnd)) { + delete codeBlocks[existingKey]; + } + } + } + } + const hush = function () { pPatterns = {}; anonymousIndex = 0; allTransform = undefined; eachTransform = undefined; + codeBlocks = {}; + blockPatterns.clear(); + lastActiveVisualizerLabel = null; // Reset 'all' visualizer tracking return silence; }; @@ -93,7 +255,18 @@ export function repl({ }; setTime(() => scheduler.now()); // TODO: refactor? - const stop = () => scheduler.stop(); + const stop = () => { + codeBlocks = {}; + blockPatterns.clear(); + pPatterns = {}; + lastActiveVisualizerLabel = null; // Reset 'all' visualizer tracking + updateState({ + miniLocations: [], + widgets: [], + sliders: [], + }); + scheduler.stop(); + }; const start = () => scheduler.start(); const pause = () => scheduler.pause(); const toggle = () => scheduler.toggle(); @@ -201,7 +374,7 @@ export function repl({ }); }; - const evaluate = async (code, autostart = true, shouldHush = true) => { + const evaluate = async (code, autostart = true, shouldHush = true, blockBased = false, options = {}) => { if (!code) { throw new Error('no code to evaluate'); } @@ -209,15 +382,92 @@ export function repl({ updateState({ code, pending: true }); await injectPatternMethods(); setTime(() => scheduler.now()); // TODO: refactor? - await beforeEval?.({ code }); + await beforeEval?.({ code, blockBased }); allTransforms = []; // reset all transforms - shouldHush && hush(); + + const transpilerOptionsWithBlock = { + ...transpilerOptions, + blockBased, + range: options.range || [], + }; + + if (!blockBased) { + codeBlocks = {}; + shouldHush && hush(); + } if (mondo) { code = `mondolang\`${code}\``; } - let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); - if (Object.keys(pPatterns).length) { + + let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptionsWithBlock); + + // Track activeVisualizer cleanup: check if any block's visualizer was removed + let widgetRemoved = false; + + if (blockBased) { + const labels = extractLabelsFromCode(code); + + // Store code blocks in dictionary using labels as keys + if (labels.length > 0) { + for (let i = 0; i < labels.length; i++) { + // processLabeledBlock(labels, i, code, options, meta); + // processing transpiler output instead of code is simply to avoid + // extra regex in detecting whether or not an inline widget has been commented out + processLabeledBlock(labels, i, meta.output, options, meta); + } + } else if (pattern !== silence) { + // variable/function declarations that return silence are allowed, + // but anonymous pattern blocks pose an issue for block-based evaluation + // if an anonymous pattern is evaluated multiple times it will just stack and get louder and louder + + // it's very common for users to write code prefixed with '$' + // but to modify and override existing patterns, the patterns must be labeled, + // otherwise we'll have no idea of which pattern is being overridden + + // (we probably need to update the docs on this) + + // we could easily enable it, but it would confuse a lot of people + throw new Error( + 'anonymous labels disabled for block based evaluation (see https://strudel.cc/blog/#label-notation)', + ); + } + + // For block-based evaluation, collect from all blocks + // The highlight/widget/slider systems will handle selective updates based on the range + meta.miniLocations = collectFromBlocks('miniLocations'); + meta.widgets = collectFromBlocks('widgets'); + meta.sliders = collectFromBlocks('sliders'); + + // Track activeVisualizer cleanup: check if any block's visualizer was removed + const blocksToUpdate = labels.map((label) => label.name); + + for (const [key, block] of Object.entries(codeBlocks)) { + if (blocksToUpdate.includes(key)) { + // This block was just updated + if (block.activeVisualizer !== null) { + // Block now has a visualizer, update tracking + lastActiveVisualizerLabel = key; + } else if (lastActiveVisualizerLabel === key) { + // This block lost its visualizer, trigger cleanup + widgetRemoved = true; + lastActiveVisualizerLabel = null; + } + } + } + } + + const allPatterns = Object.values(pPatterns); + + if (blockBased) { + pPatterns = Object.fromEntries( + Object.entries(pPatterns).filter(([key]) => { + return Object.keys(codeBlocks).includes(key); + }), + ); + } + + if (allPatterns.length) { let patterns = []; let soloActive = false; for (const [key, value] of Object.entries(pPatterns)) { @@ -250,18 +500,22 @@ export function repl({ if (!isPattern(pattern)) { pattern = silence; } + logger(`[eval] code updated`); pattern = await setPattern(pattern, autostart); updateState({ miniLocations: meta?.miniLocations || [], widgets: meta?.widgets || [], + sliders: meta?.sliders || [], activeCode: code, pattern, evalError: undefined, schedulerError: undefined, pending: false, }); - afterEval?.({ code, pattern, meta }); + + + afterEval?.({ code, pattern, meta, range: options.range, widgetRemoved }); return pattern; } catch (err) { logger(`[eval] error: ${err.message}`, 'error'); @@ -276,18 +530,18 @@ export function repl({ export const getTrigger = ({ getTime, defaultOutput }) => - async (hap, deadline, duration, cps, t) => { - // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) - // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 - try { - if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t); + async (hap, deadline, duration, cps, t) => { + // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) + // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 + try { + if (!hap.context.onTrigger || !hap.context.dominantTrigger) { + await defaultOutput(hap, deadline, duration, cps, t); + } + if (hap.context.onTrigger) { + // call signature of output / onTrigger is different... + await hap.context.onTrigger(hap, getTime(), cps, t); + } + } catch (err) { + errorLogger(err, 'getTrigger'); } - if (hap.context.onTrigger) { - // call signature of output / onTrigger is different... - await hap.context.onTrigger(hap, getTime(), cps, t); - } - } catch (err) { - errorLogger(err, 'getTrigger'); - } - }; + }; diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index 95f3aed50..514215f10 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -78,6 +78,16 @@ export const cleanupDraw = (clearScreen = true, id) => { stopAllAnimations(id); }; +export const cleanupDrawContext = (replID) => { + const ctx = getDrawContext(); + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + + // clear the big canvas context, ignore inline widgets + Object.keys(animationFrames).forEach((id) => + (!replID || id.startsWith(replID)) && !id.startsWith('_') && stopAnimationFrame(id) + ); +}; + Pattern.prototype.onPaint = function (painter) { return this.withState((state) => { if (!state.controls.painters) { diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index fea6bad4d..467e6c53f 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -19,7 +19,14 @@ export function registerLanguage(type, config) { } export function transpiler(input, options = {}) { - const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options; + const { + wrapAsync = false, + addReturn = true, + emitMiniLocations = true, + emitWidgets = true, + blockBased = false, + range = [] + } = options; let ast = parse(input, { ecmaVersion: 2022, @@ -28,6 +35,13 @@ export function transpiler(input, options = {}) { }); let miniLocations = []; + + // Position offset for block-based evaluation + let nodeOffset = range && range.length > 0 ? range[0] : 0; + + // Track declarations to add to strudelScope for block-based eval + let scopeDeclarations = []; + const collectMiniLocations = (value, node) => { const minilang = languages.get('minilang'); if (minilang) { @@ -40,9 +54,27 @@ export function transpiler(input, options = {}) { } }; let widgets = []; + let sliders = []; walk(ast, { enter(node, parent /* , prop, index */) { + // Apply position offset for block-based evaluation + if (blockBased && node.start !== undefined) { + node.start = node.start + nodeOffset; + node.end = node.end + nodeOffset; + } + // Collect variable and function declarations for strudelScope (block-based eval) + if (blockBased && parent?.type === 'Program') { + if (node.type === 'VariableDeclaration') { + for (const declarator of node.declarations) { + if (declarator.id?.name) { + scopeDeclarations.push(declarator.id.name); + } + } + } else if (node.type === 'FunctionDeclaration' && node.id?.name) { + scopeDeclarations.push(node.id.name); + } + } if (isLanguageLiteral(node)) { const { name } = node.tag; const language = languages.get(name); @@ -79,22 +111,29 @@ export function transpiler(input, options = {}) { return this.replace(miniWithLocation(value, node)); } if (isSliderFunction(node)) { - emitWidgets && - widgets.push({ - from: node.arguments[0].start, - to: node.arguments[0].end, - value: node.arguments[0].raw, // don't use value! - min: node.arguments[1]?.value ?? 0, - max: node.arguments[2]?.value ?? 1, - step: node.arguments[3]?.value, - type: 'slider', - }); - return this.replace(sliderWithLocation(node)); + const from = node.arguments[0].start + nodeOffset; + const to = node.arguments[0].end + nodeOffset; + const id = `${from}:${to}`; // Range-based ID for stability + + const sliderConfig = { + from, + to, + id, + value: node.arguments[0].raw, // don't use value! + min: node.arguments[1]?.value ?? 0, + max: node.arguments[2]?.value ?? 1, + step: node.arguments[3]?.value, + type: 'slider', + }; + emitWidgets && widgets.push(sliderConfig); + sliders.push(sliderConfig); + return this.replace(sliderWithLocation(node, nodeOffset)); } if (isWidgetMethod(node)) { const type = node.callee.property.name; const index = widgets.filter((w) => w.type === type).length; const widgetConfig = { + from: node.start, to: node.end, index, type, @@ -115,17 +154,33 @@ export function transpiler(input, options = {}) { let { body } = ast; + const silenceExpression = { + type: 'ExpressionStatement', + expression: { + type: 'Identifier', + name: 'silence', + }, + }; + if (!body.length) { console.warn('empty body -> fallback to silence'); - body.push({ - type: 'ExpressionStatement', - expression: { - type: 'Identifier', - name: 'silence', - }, - }); + body.push(silenceExpression); } else if (!body?.[body.length - 1]?.expression) { - throw new Error('unexpected ast format without body expression'); + // Last statement is not an expression (e.g., VariableDeclaration, FunctionDeclaration) + if (blockBased) { + // For block-based eval, add silence as the return value when block ends with declaration + body.push(silenceExpression); + } else { + throw new Error('unexpected ast format without body expression'); + } + } + + // For block-based eval, add scope assignments before the return statement + // This allows variables/functions defined in one block to be used in other blocks + if (blockBased && scopeDeclarations.length > 0) { + const scopeAssignments = scopeDeclarations.flatMap((name) => createScopeAssignment(name)); + // Insert scope assignments before the last statement (which will become the return) + body.splice(body.length - 1, 0, ...scopeAssignments); } // add return to last statement @@ -143,7 +198,7 @@ export function transpiler(input, options = {}) { if (!emitMiniLocations) { return { output }; } - return { output, miniLocations, widgets }; + return { output, miniLocations, widgets, sliders }; } function isStringWithDoubleQuotes(node, locations, code) { @@ -190,8 +245,14 @@ function isWidgetMethod(node) { return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name); } -function sliderWithLocation(node) { - const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id +function sliderWithLocation(node, nodeOffset = 0) { + // Apply nodeOffset for block-based evaluation to generate correct range + const from = node.arguments[0].start + nodeOffset; + const to = node.arguments[0].end + nodeOffset; + + // Use range-based ID for stability during block evaluation + const id = `${from}:${to}`; + // add loc as identifier to first argument // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) node.arguments.unshift({ @@ -206,14 +267,26 @@ function sliderWithLocation(node) { export function getWidgetID(widgetConfig) { // the widget id is used as id for the dom element + as key for eventual resources // for example, for each scope widget, a new analyser + buffer (large) is created - // that means, if we use the index index of line position as id, less garbage is generated - // return `widget_${widgetConfig.to}`; // more gargabe - //return `widget_${widgetConfig.index}_${widgetConfig.to}`; // also more garbage - return `${widgetConfig.id || ''}_widget_${widgetConfig.type}_${widgetConfig.index}`; // less garbage + // Update: use range-based ID generation for better stability during block evaluation + // When we have both from and to, use them together for stability + // Otherwise fall back to position-based ID for backward compatibility + let uniqueIdentifier; + if (widgetConfig.from !== undefined && widgetConfig.to !== undefined) { + // Use range for more stable identification + uniqueIdentifier = `${widgetConfig.from}-${widgetConfig.to}`; + } else { + // Fallback to single position (for backward compatibility) + uniqueIdentifier = widgetConfig.to || widgetConfig.from || 0; + } + const baseId = `${widgetConfig.id || ''}_widget_${widgetConfig.type}`; + return `${baseId}_${widgetConfig.index}_${uniqueIdentifier}`; } function widgetWithLocation(node, widgetConfig) { const id = getWidgetID(widgetConfig); + // Store the unique ID back into the config so it's available for widget management + // This is critical for block-based evaluation to match existing widgets with new ones + widgetConfig.id = id; // add loc as identifier to first argument // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) node.arguments.unshift({ @@ -327,3 +400,85 @@ function languageWithLocation(name, value, offset) { optional: false, }; } + +// Creates AST nodes for: userDefinedKeys.add('name'); strudelScope.name = name; globalThis.name = name; +// Used in block-based evaluation to persist variables/functions across blocks +// We add to both strudelScope (for internal lookups) and globalThis (for direct access) +// We also track the key in userDefinedKeys so clearScope() can remove it later +function createScopeAssignment(name) { + return [ + // userDefinedKeys.add('name'); + { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'userDefinedKeys', + }, + property: { + type: 'Identifier', + name: 'add', + }, + computed: false, + }, + arguments: [ + { + type: 'Literal', + value: name, + }, + ], + }, + }, + // strudelScope.name = name; + { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'strudelScope', + }, + property: { + type: 'Identifier', + name: name, + }, + computed: false, + }, + right: { + type: 'Identifier', + name: name, + }, + }, + }, + // globalThis.name = name; + { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'globalThis', + }, + property: { + type: 'Identifier', + name: name, + }, + computed: false, + }, + right: { + type: 'Identifier', + name: name, + }, + }, + }, + ]; +} diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index f46fa661c..4e81aa00e 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -116,6 +116,7 @@ export function SettingsTab({ started }) { isMultiCursorEnabled, patternAutoStart, includePrebakeScriptInShare, + isBlockBasedEvalEnabled, } = useSettings(); const shouldAlwaysSync = isUdels(); const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; @@ -288,6 +289,11 @@ export function SettingsTab({ started }) { onChange={(cbEvent) => settingsMap.setKey('isMultiCursorEnabled', cbEvent.target.checked)} value={isMultiCursorEnabled} /> + settingsMap.setKey('isBlockBasedEvalEnabled', cbEvent.target.checked)} + value={isBlockBasedEvalEnabled} + /> settingsMap.setKey('isFlashEnabled', cbEvent.target.checked)} diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 0fbd89e38..0d44eedfe 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -106,17 +106,19 @@ export function useReplContext() { //post to iframe parent (like Udels) if it exists... window.parent?.postMessage(code); - setLatestCode(code); - window.location.hash = '#' + code2hash(code); - setDocumentTitle(code); + // Get the full buffer content from the editor instead of just the evaluated block + const fullBufferCode = editorRef.current?.code || code; + setLatestCode(fullBufferCode); + window.location.hash = '#' + code2hash(fullBufferCode); + setDocumentTitle(fullBufferCode); const viewingPatternData = getViewingPatternData(); - setVersionDefaultsFrom(code); - const data = { ...viewingPatternData, code }; + setVersionDefaultsFrom(fullBufferCode); + const data = { ...viewingPatternData, code: fullBufferCode }; let id = data.id; const isExamplePattern = viewingPatternData.collection !== userPattern.collection; if (isExamplePattern) { - const codeHasChanged = code !== viewingPatternData.code; + const codeHasChanged = fullBufferCode !== viewingPatternData.code; if (codeHasChanged) { // fork example const newPattern = userPattern.duplicate(data); @@ -168,9 +170,8 @@ export function useReplContext() { useEffect(() => { let editorSettings = {}; Object.keys(defaultSettings).forEach((key) => { - if (Object.prototype.hasOwnProperty.call(_settings, key)) { - editorSettings[key] = _settings[key]; - } + // Don't use hasOwnProperty - nanostore uses proxies so values may not be own properties + editorSettings[key] = _settings[key]; }); editorRef.current?.updateSettings(editorSettings); }, [_settings]); diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 8ec7d455a..9094cba9a 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -32,6 +32,7 @@ export const defaultSettings = { isPatternHighlightingEnabled: true, isTabIndentationEnabled: false, isMultiCursorEnabled: false, + isBlockBasedEvalEnabled: false, theme: 'strudelTheme', fontFamily: 'monospace', fontSize: 18, @@ -92,6 +93,7 @@ export function useSettings() { isSyncEnabled: isUdels() ? true : parseBoolean(state.isSyncEnabled), isTabIndentationEnabled: parseBoolean(state.isTabIndentationEnabled), isMultiCursorEnabled: parseBoolean(state.isMultiCursorEnabled), + isBlockBasedEvalEnabled: parseBoolean(state.isBlockBasedEvalEnabled), fontSize: Number(state.fontSize), panelPosition: state.activeFooter !== '' && !isUdels() ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is! isPanelPinned: parseBoolean(state.isPanelPinned), From ba7721f53c47644368fc7d17801cd60fa817c190 Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Tue, 23 Dec 2025 02:25:18 -0800 Subject: [PATCH 03/12] code formatting --- packages/codemirror/codemirror.mjs | 1 - packages/core/repl.mjs | 44 ++++++++++++++---------------- packages/draw/draw.mjs | 4 +-- packages/transpiler/transpiler.mjs | 2 +- website/astro.config.mjs | 9 ++++-- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 71986ba4a..d0115e13c 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -190,7 +190,6 @@ export class StrudelMirror { this.onDraw(haps, time, painters); }, drawTime); - this.prebaked = prebake(); autodraw && this.drawFirstFrame(); this.repl = repl({ diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 4e0af7ab7..4c730515e 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -172,9 +172,9 @@ export function repl({ // These are Pattern.prototype methods that create persistent visualizations // I don't like this approach, feels hacky, but since these methods don't create ids that // can be read through the transpiler, I'm not sure how best to detect them. - // Would probably be better to register these methods through some function that would + // Would probably be better to register these methods through some function that would // tag them better - const nonInlineWidgets = ['punchcard', 'spiral', 'scope', 'pitchwheel', 'spectrum', 'pianoroll', 'wordfall',]; + const nonInlineWidgets = ['punchcard', 'spiral', 'scope', 'pitchwheel', 'spectrum', 'pianoroll', 'wordfall']; for (const widget of nonInlineWidgets) { const widgetRegex = new RegExp(`\\.${widget}\\s*\\(`); @@ -187,17 +187,15 @@ export function repl({ // Helper function to handle single label code block storage function handleSingleLabelBlock(label, code, options, meta) { - // Detect if this block contains a non-inline widget // For 'all' label, widget info is already on the label object from extractLabelsFromCode - // As mentioned in detectActiveVisualizer, this is a bad approach to + // As mentioned in detectActiveVisualizer, this is a bad approach to // managing non-inline widgets (or widgets that don't have ids) // and a proper solution would give them widgets in the transpiler, or at least // track where they are so we don't have to futz around with regexes - const activeVisualizer = label.activeVisualizer !== undefined - ? label.activeVisualizer - : detectActiveVisualizer(code); + const activeVisualizer = + label.activeVisualizer !== undefined ? label.activeVisualizer : detectActiveVisualizer(code); if (activeVisualizer !== null) { lastActiveVisualizerLabel = label.name; @@ -214,7 +212,6 @@ export function repl({ activeVisualizer: activeVisualizer, // Store the widget type if present, null otherwise }; - // Clean up any blocks with conflicting ranges for (const [existingKey, existingBlock] of Object.entries(codeBlocks)) { if (existingKey !== label.name && existingBlock.range && options.range) { @@ -412,7 +409,7 @@ export function repl({ if (labels.length > 0) { for (let i = 0; i < labels.length; i++) { // processLabeledBlock(labels, i, code, options, meta); - // processing transpiler output instead of code is simply to avoid + // processing transpiler output instead of code is simply to avoid // extra regex in detecting whether or not an inline widget has been commented out processLabeledBlock(labels, i, meta.output, options, meta); } @@ -514,7 +511,6 @@ export function repl({ pending: false, }); - afterEval?.({ code, pattern, meta, range: options.range, widgetRemoved }); return pattern; } catch (err) { @@ -530,18 +526,18 @@ export function repl({ export const getTrigger = ({ getTime, defaultOutput }) => - async (hap, deadline, duration, cps, t) => { - // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) - // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 - try { - if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t); - } - if (hap.context.onTrigger) { - // call signature of output / onTrigger is different... - await hap.context.onTrigger(hap, getTime(), cps, t); - } - } catch (err) { - errorLogger(err, 'getTrigger'); + async (hap, deadline, duration, cps, t) => { + // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) + // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 + try { + if (!hap.context.onTrigger || !hap.context.dominantTrigger) { + await defaultOutput(hap, deadline, duration, cps, t); } - }; + if (hap.context.onTrigger) { + // call signature of output / onTrigger is different... + await hap.context.onTrigger(hap, getTime(), cps, t); + } + } catch (err) { + errorLogger(err, 'getTrigger'); + } + }; diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index 514215f10..f915e9116 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -83,8 +83,8 @@ export const cleanupDrawContext = (replID) => { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); // clear the big canvas context, ignore inline widgets - Object.keys(animationFrames).forEach((id) => - (!replID || id.startsWith(replID)) && !id.startsWith('_') && stopAnimationFrame(id) + Object.keys(animationFrames).forEach( + (id) => (!replID || id.startsWith(replID)) && !id.startsWith('_') && stopAnimationFrame(id), ); }; diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 467e6c53f..24dd3f1f9 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -25,7 +25,7 @@ export function transpiler(input, options = {}) { emitMiniLocations = true, emitWidgets = true, blockBased = false, - range = [] + range = [], } = options; let ast = parse(input, { diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 151c48fc1..b5921d552 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -10,8 +10,13 @@ import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; import tailwind from '@astrojs/tailwind'; import AstroPWA from '@vite-pwa/astro'; -const site = `https://strudel.cc/`; // root url without a path -const base = '/'; // base path of the strudel site +import process from 'node:process'; + +const site = process.env.SITE_URL || `https://strudel.cc/`; // root url without a path +const base = process.env.BASE_PATH || ''; // base path of the strudel site + +// const site = `https://strudel.cc/`; // root url without a path +// const base = '/'; // base path of the strudel site const baseNoTrailing = base.endsWith('/') ? base.slice(0, -1) : base; // this rehype plugin fixes relative links From 1a7f464998e3dd797ff821c89a5754133f4c93b1 Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Tue, 30 Dec 2025 18:34:26 -0600 Subject: [PATCH 04/12] requested revisions 1 (and fix failing test) --- packages/codemirror/block_utilities.mjs | 2 +- packages/core/repl.mjs | 103 +++++------------------- packages/transpiler/transpiler.mjs | 71 +++++++++++++++- test/examples.test.mjs | 1 + 4 files changed, 90 insertions(+), 87 deletions(-) diff --git a/packages/codemirror/block_utilities.mjs b/packages/codemirror/block_utilities.mjs index 4b53755dc..0d13259a5 100644 --- a/packages/codemirror/block_utilities.mjs +++ b/packages/codemirror/block_utilities.mjs @@ -44,7 +44,7 @@ export const evalBlock = (strudelMirror) => { if (block) { // Flash the block being evaluated strudelMirror.flash(200, { from: a, to: b }); - strudelMirror.repl.evaluate(block, true, true, true, { range }); + strudelMirror.repl.evaluate(block, true, true, { range }); } } return true; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 4c730515e..d1bb1b1ad 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -126,76 +126,11 @@ export function repl({ ); } - // // Helper function to filter meta (widgets, sliders, miniLocations) by block range - // function splitCodeByRange(meta, blockStart, blockEnd) { - // } - - // Helper function to extract labels from code with their positions - function extractLabelsFromCode(code) { - const labels = []; - // Regex to find label patterns like "d1:" or "myLabel:" - const labelRegex = /^(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:)/gm; - let match; - while ((match = labelRegex.exec(code)) !== null) { - labels.push({ - name: match[2], - index: match.index, - end: match.index + match[1].length, - fullMatch: match[1], - }); - } - - // Also check for 'all()' and treat it as a special label - // just for management purposes - const allRegex = /all\s*\(\s*([^)]+)\s*\)/; - const allMatch = allRegex.exec(code); - if (allMatch) { - // Check if the argument contains a widget call - const argCode = allMatch[1]; - const activeVisualizer = detectActiveVisualizer(argCode); - - labels.push({ - name: 'all', - index: allMatch.index, - end: allMatch.index + allMatch[0].length, - fullMatch: allMatch[0], - activeVisualizer: activeVisualizer, - }); - } - - return labels; - } - - // Helper function to detect non-inline widget calls in code - function detectActiveVisualizer(code) { - // List of non-inline widgets that need cleanup - // These are Pattern.prototype methods that create persistent visualizations - // I don't like this approach, feels hacky, but since these methods don't create ids that - // can be read through the transpiler, I'm not sure how best to detect them. - // Would probably be better to register these methods through some function that would - // tag them better - const nonInlineWidgets = ['punchcard', 'spiral', 'scope', 'pitchwheel', 'spectrum', 'pianoroll', 'wordfall']; - - for (const widget of nonInlineWidgets) { - const widgetRegex = new RegExp(`\\.${widget}\\s*\\(`); - if (widgetRegex.test(code)) { - return widget; - } - } - return null; - } - // Helper function to handle single label code block storage function handleSingleLabelBlock(label, code, options, meta) { // Detect if this block contains a non-inline widget - // For 'all' label, widget info is already on the label object from extractLabelsFromCode - - // As mentioned in detectActiveVisualizer, this is a bad approach to - // managing non-inline widgets (or widgets that don't have ids) - // and a proper solution would give them widgets in the transpiler, or at least - // track where they are so we don't have to futz around with regexes - const activeVisualizer = - label.activeVisualizer !== undefined ? label.activeVisualizer : detectActiveVisualizer(code); + // The activeVisualizer is now provided by the transpiler for all labels + const activeVisualizer = label.activeVisualizer || null; if (activeVisualizer !== null) { lastActiveVisualizerLabel = label.name; @@ -371,7 +306,7 @@ export function repl({ }); }; - const evaluate = async (code, autostart = true, shouldHush = true, blockBased = false, options = {}) => { + const evaluate = async (code, autostart = true, blockBased = false, options = {}) => { if (!code) { throw new Error('no code to evaluate'); } @@ -390,7 +325,7 @@ export function repl({ if (!blockBased) { codeBlocks = {}; - shouldHush && hush(); + hush(); } if (mondo) { @@ -403,7 +338,7 @@ export function repl({ let widgetRemoved = false; if (blockBased) { - const labels = extractLabelsFromCode(code); + const labels = meta.labels || []; // Store code blocks in dictionary using labels as keys if (labels.length > 0) { @@ -526,18 +461,18 @@ export function repl({ export const getTrigger = ({ getTime, defaultOutput }) => - async (hap, deadline, duration, cps, t) => { - // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) - // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 - try { - if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t); + async (hap, deadline, duration, cps, t) => { + // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) + // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 + try { + if (!hap.context.onTrigger || !hap.context.dominantTrigger) { + await defaultOutput(hap, deadline, duration, cps, t); + } + if (hap.context.onTrigger) { + // call signature of output / onTrigger is different... + await hap.context.onTrigger(hap, getTime(), cps, t); + } + } catch (err) { + errorLogger(err, 'getTrigger'); } - if (hap.context.onTrigger) { - // call signature of output / onTrigger is different... - await hap.context.onTrigger(hap, getTime(), cps, t); - } - } catch (err) { - errorLogger(err, 'getTrigger'); - } - }; + }; diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 24dd3f1f9..e7d4acfa1 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -55,6 +55,7 @@ export function transpiler(input, options = {}) { }; let widgets = []; let sliders = []; + let labels = []; walk(ast, { enter(node, parent /* , prop, index */) { @@ -146,10 +147,32 @@ export function transpiler(input, options = {}) { return this.replace(withAwait(node)); } if (isLabelStatement(node)) { + // Collect label info for block-based evaluation + // Store positions WITHOUT offset so repl can slice the transpiler output correctly + if (blockBased) { + labels.push({ + name: node.label.name, + index: node.start - nodeOffset, + end: node.label.end - nodeOffset, + fullMatch: input.slice(node.start - nodeOffset, node.label.end - nodeOffset), + activeVisualizer: findVisualizerInSubtree(node.body), + }); + } return this.replace(labelToP(node)); } + // Detect all() calls as special labels for block management + // Store positions WITHOUT offset so repl can slice the transpiler output correctly + if (blockBased && isAllCall(node)) { + labels.push({ + name: 'all', + index: node.start - nodeOffset, + end: node.end - nodeOffset, + fullMatch: input.slice(node.start - nodeOffset, node.end - nodeOffset), + activeVisualizer: node.arguments[0] ? findVisualizerInSubtree(node.arguments[0]) : null, + }); + } }, - leave(node, parent, prop, index) {}, + leave(node, parent, prop, index) { }, }); let { body } = ast; @@ -198,7 +221,7 @@ export function transpiler(input, options = {}) { if (!emitMiniLocations) { return { output }; } - return { output, miniLocations, widgets, sliders }; + return { output, miniLocations, widgets, sliders, labels }; } function isStringWithDoubleQuotes(node, locations, code) { @@ -301,6 +324,10 @@ function isBareSamplesCall(node, parent) { return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression'; } +function isAllCall(node) { + return node.type === 'CallExpression' && node.callee.name === 'all'; +} + function withAwait(node) { return { type: 'AwaitExpression', @@ -401,6 +428,46 @@ function languageWithLocation(name, value, offset) { }; } +// List of non-inline widgets that need cleanup +// These are Pattern.prototype methods that create persistent visualizations +// (should be repalced by a function call producing an actual list of registered widgets) +const nonInlineWidgets = ['punchcard', 'spiral', 'scope', 'pitchwheel', 'spectrum', 'pianoroll', 'wordfall']; + +function isVisualizerCall(node) { + if ( + node.type === 'CallExpression' && + node.callee.type === 'MemberExpression' && + nonInlineWidgets.includes(node.callee.property?.name) + ) { + return node.callee.property.name; + } + return null; +} + +function findVisualizerInSubtree(node) { + if (!node || typeof node !== 'object') return null; + + // Check if this node is a visualizer call + const viz = isVisualizerCall(node); + if (viz) return viz; + + // Recursively search children + for (const key of Object.keys(node)) { + if (key === 'parent') continue; // Skip parent references to avoid cycles + const child = node[key]; + if (Array.isArray(child)) { + for (const item of child) { + const found = findVisualizerInSubtree(item); + if (found) return found; + } + } else if (child && typeof child === 'object' && child.type) { + const found = findVisualizerInSubtree(child); + if (found) return found; + } + } + return null; +} + // Creates AST nodes for: userDefinedKeys.add('name'); strudelScope.name = name; globalThis.name = name; // Used in block-based evaluation to persist variables/functions across blocks // We add to both strudelScope (for internal lookups) and globalThis (for direct access) diff --git a/test/examples.test.mjs b/test/examples.test.mjs index 896a02f8a..18f12bb42 100644 --- a/test/examples.test.mjs +++ b/test/examples.test.mjs @@ -20,6 +20,7 @@ const skippedExamples = [ 'accelerationX', 'defaultmidimap', 'midimaps', + 'clearScope' ]; describe('runs examples', () => { From 160ef84b47be62bb8181cd63858c2d83ff451cb4 Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Tue, 30 Dec 2025 20:14:32 -0600 Subject: [PATCH 05/12] move evaluate/evaluateBlock to different functions --- packages/codemirror/block_utilities.mjs | 2 +- packages/core/repl.mjs | 255 ++++++++++++++---------- packages/transpiler/transpiler.mjs | 2 +- test/examples.test.mjs | 2 +- 4 files changed, 152 insertions(+), 109 deletions(-) diff --git a/packages/codemirror/block_utilities.mjs b/packages/codemirror/block_utilities.mjs index 0d13259a5..fd3dbedcd 100644 --- a/packages/codemirror/block_utilities.mjs +++ b/packages/codemirror/block_utilities.mjs @@ -44,7 +44,7 @@ export const evalBlock = (strudelMirror) => { if (block) { // Flash the block being evaluated strudelMirror.flash(200, { from: a, to: b }); - strudelMirror.repl.evaluate(block, true, true, { range }); + strudelMirror.repl.evaluateBlock(block, true, { range }); } } return true; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index d1bb1b1ad..e3efdd3e5 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -187,6 +187,48 @@ export function repl({ }; setTime(() => scheduler.now()); // TODO: refactor? + // Helper function to apply pattern transformations (solo, each, all) + // this should be abstracted more + function applyPatternTransforms(pattern) { + const allPatterns = Object.values(pPatterns); + + if (allPatterns.length) { + let patterns = []; + let soloActive = false; + for (const [key, value] of Object.entries(pPatterns)) { + // handle soloed patterns ex: S$: s("bd!4") + const isSolod = key.length > 1 && key.startsWith('S'); + if (isSolod && soloActive === false) { + // first time we see a soloed pattern, clear existing patterns + patterns = []; + soloActive = true; + } + if (!soloActive || (soloActive && isSolod)) { + const valWithState = value.withState((state) => state.setControls({ id: key })); + patterns.push(valWithState); + } + } + if (eachTransform) { + // Explicit lambda so only element (not index and array) are passed + patterns = patterns.map((x) => eachTransform(x)); + } + pattern = stack(...patterns); + } else if (eachTransform) { + pattern = eachTransform(pattern); + } + if (allTransforms.length) { + for (const transform of allTransforms) { + pattern = transform(pattern); + } + } + + if (!isPattern(pattern)) { + pattern = silence; + } + + return pattern; + } + const stop = () => { codeBlocks = {}; blockPatterns.clear(); @@ -306,7 +348,7 @@ export function repl({ }); }; - const evaluate = async (code, autostart = true, blockBased = false, options = {}) => { + const evaluate = async (code, autostart = true) => { if (!code) { throw new Error('no code to evaluate'); } @@ -314,20 +356,60 @@ export function repl({ updateState({ code, pending: true }); await injectPatternMethods(); setTime(() => scheduler.now()); // TODO: refactor? - await beforeEval?.({ code, blockBased }); + await beforeEval?.({ code, blockBased: false }); + allTransforms = []; // reset all transforms + + codeBlocks = {}; + hush(); + + if (mondo) { + code = `mondolang\`${code}\``; + } + + let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); + + pattern = applyPatternTransforms(pattern); + + logger(`[eval] code updated`); + pattern = await setPattern(pattern, autostart); + updateState({ + miniLocations: meta?.miniLocations || [], + widgets: meta?.widgets || [], + sliders: meta?.sliders || [], + activeCode: code, + pattern, + evalError: undefined, + schedulerError: undefined, + pending: false, + }); + + afterEval?.({ code, pattern, meta, range: undefined, widgetRemoved: false }); + return pattern; + } catch (err) { + logger(`[eval] error: ${err.message}`, 'error'); + console.error(err); + updateState({ evalError: err, pending: false }); + onEvalError?.(err); + } + }; + + const evaluateBlock = async (code, autostart = true, options = {}) => { + if (!code) { + throw new Error('no code to evaluate'); + } + try { + updateState({ code, pending: true }); + await injectPatternMethods(); + setTime(() => scheduler.now()); // TODO: refactor? + await beforeEval?.({ code, blockBased: true }); allTransforms = []; // reset all transforms const transpilerOptionsWithBlock = { ...transpilerOptions, - blockBased, + blockBased: true, range: options.range || [], }; - if (!blockBased) { - codeBlocks = {}; - hush(); - } - if (mondo) { code = `mondolang\`${code}\``; } @@ -337,101 +419,61 @@ export function repl({ // Track activeVisualizer cleanup: check if any block's visualizer was removed let widgetRemoved = false; - if (blockBased) { - const labels = meta.labels || []; + const labels = meta.labels || []; - // Store code blocks in dictionary using labels as keys - if (labels.length > 0) { - for (let i = 0; i < labels.length; i++) { - // processLabeledBlock(labels, i, code, options, meta); - // processing transpiler output instead of code is simply to avoid - // extra regex in detecting whether or not an inline widget has been commented out - processLabeledBlock(labels, i, meta.output, options, meta); - } - } else if (pattern !== silence) { - // variable/function declarations that return silence are allowed, - // but anonymous pattern blocks pose an issue for block-based evaluation - // if an anonymous pattern is evaluated multiple times it will just stack and get louder and louder - - // it's very common for users to write code prefixed with '$' - // but to modify and override existing patterns, the patterns must be labeled, - // otherwise we'll have no idea of which pattern is being overridden - - // (we probably need to update the docs on this) - - // we could easily enable it, but it would confuse a lot of people - throw new Error( - 'anonymous labels disabled for block based evaluation (see https://strudel.cc/blog/#label-notation)', - ); + // Store code blocks in dictionary using labels as keys + if (labels.length > 0) { + for (let i = 0; i < labels.length; i++) { + // processing transpiler output instead of code is simply to avoid + // extra regex in detecting whether or not an inline widget has been commented out + processLabeledBlock(labels, i, meta.output, options, meta); } + } else if (pattern !== silence) { + // variable/function declarations that return silence are allowed, + // but anonymous pattern blocks pose an issue for block-based evaluation + // if an anonymous pattern is evaluated multiple times it will just stack and get louder and louder - // For block-based evaluation, collect from all blocks - // The highlight/widget/slider systems will handle selective updates based on the range - meta.miniLocations = collectFromBlocks('miniLocations'); - meta.widgets = collectFromBlocks('widgets'); - meta.sliders = collectFromBlocks('sliders'); + // it's very common for users to write code prefixed with '$' + // but to modify and override existing patterns, the patterns must be labeled, + // otherwise we'll have no idea of which pattern is being overridden - // Track activeVisualizer cleanup: check if any block's visualizer was removed - const blocksToUpdate = labels.map((label) => label.name); + // (we probably need to update the docs on this) - for (const [key, block] of Object.entries(codeBlocks)) { - if (blocksToUpdate.includes(key)) { - // This block was just updated - if (block.activeVisualizer !== null) { - // Block now has a visualizer, update tracking - lastActiveVisualizerLabel = key; - } else if (lastActiveVisualizerLabel === key) { - // This block lost its visualizer, trigger cleanup - widgetRemoved = true; - lastActiveVisualizerLabel = null; - } - } - } - } - - const allPatterns = Object.values(pPatterns); - - if (blockBased) { - pPatterns = Object.fromEntries( - Object.entries(pPatterns).filter(([key]) => { - return Object.keys(codeBlocks).includes(key); - }), + // we could easily enable it, but it would confuse a lot of people + throw new Error( + 'anonymous labels disabled for block based evaluation (see https://strudel.cc/blog/#label-notation)', ); } - if (allPatterns.length) { - let patterns = []; - let soloActive = false; - for (const [key, value] of Object.entries(pPatterns)) { - // handle soloed patterns ex: S$: s("bd!4") - const isSolod = key.length > 1 && key.startsWith('S'); - if (isSolod && soloActive === false) { - // first time we see a soloed pattern, clear existing patterns - patterns = []; - soloActive = true; + meta.miniLocations = collectFromBlocks('miniLocations'); + meta.widgets = collectFromBlocks('widgets'); + meta.sliders = collectFromBlocks('sliders'); + + // Track activeVisualizer cleanup: check if any block's visualizer was removed + const blocksToUpdate = labels.map((label) => label.name); + + // this is the hackiest bit + for (const [key, block] of Object.entries(codeBlocks)) { + if (blocksToUpdate.includes(key)) { + // This block was just updated + if (block.activeVisualizer !== null) { + // Block now has a visualizer, update tracking + lastActiveVisualizerLabel = key; + } else if (lastActiveVisualizerLabel === key) { + // This block lost its visualizer, trigger cleanup + widgetRemoved = true; + lastActiveVisualizerLabel = null; } - if (!soloActive || (soloActive && isSolod)) { - const valWithState = value.withState((state) => state.setControls({ id: key })); - patterns.push(valWithState); - } - } - if (eachTransform) { - // Explicit lambda so only element (not index and array) are passed - patterns = patterns.map((x) => eachTransform(x)); - } - pattern = stack(...patterns); - } else if (eachTransform) { - pattern = eachTransform(pattern); - } - if (allTransforms.length) { - for (const transform of allTransforms) { - pattern = transform(pattern); } } - if (!isPattern(pattern)) { - pattern = silence; - } + pPatterns = Object.fromEntries( + Object.entries(pPatterns).filter(([key]) => { + return Object.keys(codeBlocks).includes(key); + }), + ); + + pattern = applyPatternTransforms(pattern); logger(`[eval] code updated`); pattern = await setPattern(pattern, autostart); @@ -455,24 +497,25 @@ export function repl({ onEvalError?.(err); } }; + const setCode = (code) => updateState({ code }); - return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state }; + return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state }; } export const getTrigger = ({ getTime, defaultOutput }) => - async (hap, deadline, duration, cps, t) => { - // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) - // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 - try { - if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t); - } - if (hap.context.onTrigger) { - // call signature of output / onTrigger is different... - await hap.context.onTrigger(hap, getTime(), cps, t); - } - } catch (err) { - errorLogger(err, 'getTrigger'); + async (hap, deadline, duration, cps, t) => { + // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) + // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 + try { + if (!hap.context.onTrigger || !hap.context.dominantTrigger) { + await defaultOutput(hap, deadline, duration, cps, t); } - }; + if (hap.context.onTrigger) { + // call signature of output / onTrigger is different... + await hap.context.onTrigger(hap, getTime(), cps, t); + } + } catch (err) { + errorLogger(err, 'getTrigger'); + } + }; diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index e7d4acfa1..db08c60b5 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -172,7 +172,7 @@ export function transpiler(input, options = {}) { }); } }, - leave(node, parent, prop, index) { }, + leave(node, parent, prop, index) {}, }); let { body } = ast; diff --git a/test/examples.test.mjs b/test/examples.test.mjs index 18f12bb42..a3dec47aa 100644 --- a/test/examples.test.mjs +++ b/test/examples.test.mjs @@ -20,7 +20,7 @@ const skippedExamples = [ 'accelerationX', 'defaultmidimap', 'midimaps', - 'clearScope' + 'clearScope', ]; describe('runs examples', () => { From 2e7ec9ea272b368e4c9b4fe98990d0e662c0f57a Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Thu, 15 Jan 2026 19:27:54 -0800 Subject: [PATCH 06/12] variable declarations with active patterns now have highlighting --- packages/core/repl.mjs | 56 +++++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index e3efdd3e5..09c76d607 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -126,7 +126,23 @@ export function repl({ ); } - // Helper function to handle single label code block storage + // helper + function cleanupConflictingRanges(codeBlocks, currentKey, newRange) { + for (const [existingKey, existingBlock] of Object.entries(codeBlocks)) { + if (existingKey === currentKey) continue; + if (!existingBlock.range) continue; + + const [existingStart, existingEnd] = existingBlock.range; + const [newStart, newEnd] = newRange; + + // If ranges overlap (not just touch), remove the stale block + if (!(newEnd <= existingStart || newStart >= existingEnd)) { + delete codeBlocks[existingKey]; + } + } + } + + // helper function handleSingleLabelBlock(label, code, options, meta) { // Detect if this block contains a non-inline widget // The activeVisualizer is now provided by the transpiler for all labels @@ -147,18 +163,30 @@ export function repl({ activeVisualizer: activeVisualizer, // Store the widget type if present, null otherwise }; - // Clean up any blocks with conflicting ranges - for (const [existingKey, existingBlock] of Object.entries(codeBlocks)) { - if (existingKey !== label.name && existingBlock.range && options.range) { - const [existingStart, existingEnd] = existingBlock.range; - const [newStart, newEnd] = options.range; + // Clean up any blocks with conflicting ranges (including declaration blocks) + cleanupConflictingRanges(codeBlocks, label.name, options.range); + } - // If ranges overlap (not just touch), remove the stale block - if (!(newEnd <= existingStart || newStart >= existingEnd)) { - delete codeBlocks[existingKey]; - } - } - } + // helper + // These blocks return silence but may contain mini notation strings that need highlighting + function handleDeclarationBlock(code, options, meta) { + const range = options.range || []; + if (range.length < 2) return; + + const blockKey = `_decl:${range[0]}:${range[1]}`; + + codeBlocks[blockKey] = { + code: code, + range: range, + labels: [], + miniLocations: meta?.miniLocations || [], + widgets: meta?.widgets || [], + sliders: meta?.sliders || [], + activeVisualizer: null, + }; + + // Clean up any overlapping declaration blocks + cleanupConflictingRanges(codeBlocks, blockKey, range); } const hush = function () { @@ -443,6 +471,10 @@ export function repl({ throw new Error( 'anonymous labels disabled for block based evaluation (see https://strudel.cc/blog/#label-notation)', ); + } else { + // Declaration block (variable/function that returns silence) + // Store it so its miniLocations are preserved for highlighting patterns stored in variables + handleDeclarationBlock(code, options, meta); } meta.miniLocations = collectFromBlocks('miniLocations'); From 87e723649ead1a899842c30af71d58dd356c30a5 Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Sun, 18 Jan 2026 23:43:11 -0800 Subject: [PATCH 07/12] fixed highlighting issue --- packages/codemirror/highlight.mjs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/codemirror/highlight.mjs b/packages/codemirror/highlight.mjs index 5cc6d3a11..b37390ef7 100644 --- a/packages/codemirror/highlight.mjs +++ b/packages/codemirror/highlight.mjs @@ -25,7 +25,6 @@ const miniLocations = StateField.define({ //block-based eval case if (e.value.range) { const stateMiniLocations = getMiniLocationsFromDecorations(locations); - const existingById = new Map(stateMiniLocations.map(({ id, from, to }) => [id, [from, to]])); const normalized = e.value.locations .filter(([from]) => from < tr.newDoc.length) @@ -35,13 +34,12 @@ const miniLocations = StateField.define({ const marks = normalized.map((range) => { const id = range.join(':'); - const useRange = existingById.get(id) || range; return Decoration.mark({ id, // this green is only to verify that the decoration moves when the document is edited // it will be removed later, so the mark is not visible by default attributes: { style: `background-color: #00CA2880` }, - }).range(...useRange); // -> Decoration + }).range(...range); // -> Decoration }); const previousMarks = stateMiniLocations From c96b1c2faf8c31bbe38f31714e2c6432fc9a81e6 Mon Sep 17 00:00:00 2001 From: Dsm0 Date: Mon, 19 Jan 2026 01:15:55 -0800 Subject: [PATCH 08/12] fixed anonymous label casing --- packages/core/repl.mjs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 8d023c72b..3ff43e2f0 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -473,15 +473,12 @@ export function repl({ const labels = meta.labels || []; + // Check for anonymous labels (labels starting with '$') + const hasAnonymousLabel = labels.some((label) => label.name.startsWith('$')); + // Store code blocks in dictionary using labels as keys - if (labels.length > 0) { - for (let i = 0; i < labels.length; i++) { - // processing transpiler output instead of code is simply to avoid - // extra regex in detecting whether or not an inline widget has been commented out - processLabeledBlock(labels, i, meta.output, options, meta); - } - } else if (pattern !== silence) { - // variable/function declarations that return silence are allowed, + if (hasAnonymousLabel) { + // variable/function declarations that don't return patterns are allowed, // but anonymous pattern blocks pose an issue for block-based evaluation // if an anonymous pattern is evaluated multiple times it will just stack and get louder and louder @@ -490,11 +487,17 @@ export function repl({ // otherwise we'll have no idea of which pattern is being overridden // (we probably need to update the docs on this) - // we could easily enable it, but it would confuse a lot of people + throw new Error( 'anonymous labels disabled for block based evaluation (see https://strudel.cc/blog/#label-notation)', ); + } else if (labels.length > 0) { + for (let i = 0; i < labels.length; i++) { + // processing transpiler output instead of code is simply to avoid + // extra regex in detecting whether or not an inline widget has been commented out + processLabeledBlock(labels, i, meta.output, options, meta); + } } else { // Declaration block (variable/function that returns silence) // Store it so its miniLocations are preserved for highlighting patterns stored in variables From 55ffb59b9c72c2838ff5c9a93c7ea364dbfbb652 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 23 Jan 2026 23:40:51 +0100 Subject: [PATCH 09/12] tagging a lot of superdough functions --- packages/core/controls.mjs | 436 +++++++++--------- packages/core/pattern.mjs | 114 +++-- packages/core/pick.mjs | 32 +- packages/midi/midi.mjs | 8 +- packages/superdough/superdough.mjs | 4 +- packages/superdough/wavetable.mjs | 2 +- packages/tonal/tonal.mjs | 6 +- packages/tonal/voicings.mjs | 8 +- .../src/repl/components/panel/Reference.jsx | 13 +- 9 files changed, 314 insertions(+), 309 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index a5a1c0a12..af8807a11 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -99,7 +99,7 @@ export function registerMultiControl(names, maxControls, ...aliases) { * separated by ':'. * * @name s - * @tags samples + * @tags superdough, samples * @param {string | Pattern} sound The sound / pattern of sounds to pick * @synonyms sound * @example @@ -114,7 +114,7 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound'); * Position in the wavetable of the wavetable oscillator * * @name wt - * @tags fx + * @tags wavetable, superdough * @param {number | Pattern} position Position in the wavetable from 0 to 1 * @synonyms wavetablePosition * @example @@ -126,7 +126,7 @@ export const { wt, wavetablePosition } = registerControl('wt', 'wavetablePositio * Amount of envelope applied wavetable oscillator's position envelope * * @name wtenv - * @tags fx + * @tags wavetable, envelope, superdough * @param {number | Pattern} amount between 0 and 1 */ export const { wtenv } = registerControl('wtenv'); @@ -134,7 +134,7 @@ export const { wtenv } = registerControl('wtenv'); * Attack time of the wavetable oscillator's position envelope * * @name wtattack - * @tags fx + * @tags wavetable, envelope, superdough * @synonyms wtatt * @param {number | Pattern} time attack time in seconds */ @@ -144,7 +144,7 @@ export const { wtattack, wtatt } = registerControl('wtattack', 'wtatt'); * Decay time of the wavetable oscillator's position envelope * * @name wtdecay - * @tags fx + * @tags wavetable, envelope, superdough * @synonyms wtdec * @param {number | Pattern} time decay time in seconds */ @@ -154,7 +154,7 @@ export const { wtdecay, wtdec } = registerControl('wtdecay', 'wtdec'); * Sustain time of the wavetable oscillator's position envelope * * @name wtsustain - * @tags fx + * @tags wavetable, envelope, superdough * @synonyms wtsus * @param {number | Pattern} gain sustain level (0 to 1) */ @@ -164,7 +164,7 @@ export const { wtsustain, wtsus } = registerControl('wtsustain', 'wtsus'); * Release time of the wavetable oscillator's position envelope * * @name wtrelease - * @tags fx + * @tags wavetable, envelope, superdough * @synonyms wtrel * @param {number | Pattern} time release time in seconds */ @@ -174,7 +174,7 @@ export const { wtrelease, wtrel } = registerControl('wtrelease', 'wtrel'); * Rate of the LFO for the wavetable oscillator's position * * @name wtrate - * @tags fx + * @tags wavetable, lfo, superdough * @param {number | Pattern} rate rate in hertz */ export const { wtrate } = registerControl('wtrate'); @@ -182,7 +182,7 @@ export const { wtrate } = registerControl('wtrate'); * cycle synced rate of the LFO for the wavetable oscillator's position * * @name wtsync - * @tags fx + * @tags wavetable, lfo, superdough * @param {number | Pattern} rate rate in cycles */ export const { wtsync } = registerControl('wtsync'); @@ -191,7 +191,7 @@ export const { wtsync } = registerControl('wtsync'); * Depth of the LFO for the wavetable oscillator's position * * @name wtdepth - * @tags fx + * @tags wavetable, lfo, superdough * @param {number | Pattern} depth depth of modulation */ export const { wtdepth } = registerControl('wtdepth'); @@ -200,7 +200,7 @@ export const { wtdepth } = registerControl('wtdepth'); * Shape of the LFO for the wavetable oscillator's position * * @name wtshape - * @tags fx + * @tags wavetable, lfo, superdough * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) */ export const { wtshape } = registerControl('wtshape'); @@ -209,7 +209,7 @@ export const { wtshape } = registerControl('wtshape'); * DC offset of the LFO for the wavetable oscillator's position * * @name wtdc - * @tags fx + * @tags wavetable, lfo, superdough * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar */ export const { wtdc } = registerControl('wtdc'); @@ -218,7 +218,7 @@ export const { wtdc } = registerControl('wtdc'); * Skew of the LFO for the wavetable oscillator's position * * @name wtskew - * @tags fx + * @tags wavetable, lfo, superdough * @param {number | Pattern} skew How much to bend the LFO shape */ export const { wtskew } = registerControl('wtskew'); @@ -227,7 +227,7 @@ export const { wtskew } = registerControl('wtskew'); * Amount of warp (alteration of the waveform) to apply to the wavetable oscillator * * @name warp - * @tags fx + * @tags wavetable, superdough * @param {number | Pattern} amount Warp of the wavetable from 0 to 1 * @synonyms wavetableWarp * @example @@ -240,7 +240,7 @@ export const { warp, wavetableWarp } = registerControl('warp', 'wavetableWarp'); * Attack time of the wavetable oscillator's warp envelope * * @name warpattack - * @tags fx + * @tags wavetable, envelope, superdough * @synonyms warpatt * @param {number | Pattern} time attack time in seconds */ @@ -250,7 +250,7 @@ export const { warpattack, warpatt } = registerControl('warpattack', 'warpatt'); * Decay time of the wavetable oscillator's warp envelope * * @name warpdecay - * @tags fx + * @tags wavetable, envelope, superdough * @synonyms warpdec * @param {number | Pattern} time decay time in seconds */ @@ -260,7 +260,7 @@ export const { warpdecay, warpdec } = registerControl('warpdecay', 'warpdec'); * Sustain time of the wavetable oscillator's warp envelope * * @name warpsustain - * @tags fx + * @tags wavetable, envelope, superdough * @synonyms warpsus * @param {number | Pattern} gain sustain level (0 to 1) */ @@ -270,7 +270,7 @@ export const { warpsustain, warpsus } = registerControl('warpsustain', 'warpsus' * Release time of the wavetable oscillator's warp envelope * * @name warprelease - * @tags fx + * @tags wavetable, envelope, superdough * @synonyms warprel * @param {number | Pattern} time release time in seconds */ @@ -280,7 +280,7 @@ export const { warprelease, warprel } = registerControl('warprelease', 'warprel' * Rate of the LFO for the wavetable oscillator's warp * * @name warprate - * @tags fx + * @tags wavetable, lfo, superdough * @param {number | Pattern} rate rate in hertz */ export const { warprate } = registerControl('warprate'); @@ -289,7 +289,7 @@ export const { warprate } = registerControl('warprate'); * Depth of the LFO for the wavetable oscillator's warp * * @name warpdepth - * @tags fx + * @tags wavetable, lfo, superdough * @param {number | Pattern} depth depth of modulation */ export const { warpdepth } = registerControl('warpdepth'); @@ -298,7 +298,7 @@ export const { warpdepth } = registerControl('warpdepth'); * Shape of the LFO for the wavetable oscillator's warp * * @name warpshape - * @tags fx + * @tags wavetable, lfo, superdough * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) */ export const { warpshape } = registerControl('warpshape'); @@ -307,7 +307,7 @@ export const { warpshape } = registerControl('warpshape'); * DC offset of the LFO for the wavetable oscillator's warp * * @name warpdc - * @tags fx + * @tags wavetable, lfo, superdough * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar */ export const { warpdc } = registerControl('warpdc'); @@ -316,7 +316,7 @@ export const { warpdc } = registerControl('warpdc'); * Skew of the LFO for the wavetable oscillator's warp * * @name warpskew - * @tags fx + * @tags wavetable, lfo, superdough * @param {number | Pattern} skew How much to bend the LFO shape */ export const { warpskew } = registerControl('warpskew'); @@ -328,7 +328,7 @@ export const { warpskew } = registerControl('warpskew'); * spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip * * @name warpmode - * @tags fx + * @tags wavetable, superdough * @param {number | string | Pattern} mode Warp mode * @synonyms wavetableWarpMode * @example @@ -342,7 +342,7 @@ export const { warpmode, wavetableWarpMode } = registerControl('warpmode', 'wave * Amount of randomness of the initial phase of the wavetable oscillator. * * @name wtphaserand - * @tags fx + * @tags wavetable, superdough * @param {number | Pattern} amount Randomness of the initial phase. Between 0 (not random) and 1 (fully random) * @synonyms wavetablePhaseRand * @example @@ -355,7 +355,7 @@ export const { wtphaserand, wavetablePhaseRand } = registerControl('wtphaserand' * Amount of envelope applied wavetable oscillator's position envelope * * @name warpenv - * @tags fx + * @tags wavetable, envelope, superdough * @param {number | Pattern} amount between 0 and 1 */ export const { warpenv } = registerControl('warpenv'); @@ -364,7 +364,7 @@ export const { warpenv } = registerControl('warpenv'); * cycle synced rate of the LFO for the wavetable warp position * * @name warpsync - * @tags fx + * @tags wavetable, lfo, superdough * @param {number | Pattern} rate rate in cycles */ export const { warpsync } = registerControl('warpsync'); @@ -381,12 +381,13 @@ export const { warpsync } = registerControl('warpsync'); */ export const { source, src } = registerControl('source', 'src'); /** - * Selects the given index from the sample map. - * Numbers too high will wrap around. - * `n` can also be used to play midi numbers, but it is recommended to use `note` instead. + * Selects the given index: + * - for samples, it picks the sample by index, with wrap around + * - for scales, it picks the scale degree + * - for voicings, it picks the voice index * * @name n - * @tags samples + * @tags superdough, samples, tonal * @param {number | Pattern} value sample index starting from 0 * @example * s("bd sd [~ bd] sd,hh*6").n("<0 1>") @@ -405,7 +406,7 @@ export const { n } = registerControl('n'); * You can also use midi numbers instead of note names, where 69 is mapped to A4 440Hz in 12EDO. * * @name note - * @tags music_theory + * @tags tonal * @example * note("c a f e") * @example @@ -433,7 +434,7 @@ export const { accelerate } = registerControl('accelerate'); * Sets the velocity from 0 to 1. Is multiplied together with gain. * * @name velocity - * @tags fx, superdough, supradough + * @tags amplitude, superdough, supradough * @synonyms vel * @example * s("hh*8") @@ -445,7 +446,7 @@ export const { velocity, vel } = registerControl('velocity', 'vel'); * Controls the gain by an exponential amount. * * @name gain - * @tags fx, superdough, supradough + * @tags amplitude, superdough, supradough * @param {number | Pattern} amount gain. * @example * s("hh*8").gain(".4!2 1 .4!2 1 .4 1").fast(2) @@ -456,7 +457,7 @@ export const { gain } = registerControl('gain'); * Gain applied after all effects have been processed. * * @name postgain - * @tags fx, superdough, supradough + * @tags amplitude, superdough, supradough * @example * s("bd sd [~ bd] sd,hh*8") * .compressor("-20:20:10:.002:.02").postgain(1.5) @@ -467,7 +468,7 @@ export const { postgain } = registerControl('postgain'); * Like `gain`, but linear. * * @name amp - * @tags fx, superdirt + * @tags amplitude, superdirt * @param {number | Pattern} amount gain. * @superdirtOnly * @example @@ -475,18 +476,6 @@ export const { postgain } = registerControl('postgain'); * */ export const { amp } = registerControl('amp'); -/** - * Amplitude envelope attack time: Specifies how long it takes for the sound to reach its peak value, relative to the onset. - * - * @name attack - * @tags fx, superdough, supradough - * @param {number | Pattern} attack time in seconds. - * @synonyms att - * @example - * note("c3 e3 f3 g3").attack("<0 .1 .5>") - * - */ -export const { attack, att } = registerControl('attack', 'att'); /** * Sets the Frequency Modulation Harmonicity Ratio. @@ -498,7 +487,7 @@ export const { attack, att } = registerControl('attack', 'att'); * any of the 8 individual FMs (e.g. `fmh2`) * * @name fmh - * @tags fx, superdough, supradough + * @tags fm, superdough, supradough * @param {number | Pattern} harmonicity * @example * note("c e g b g e") @@ -519,7 +508,7 @@ export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerM * `fm3` * * @name fmi - * @tags fx, superdough, supradough + * @tags fm, superdough, supradough * @param {number | Pattern} brightness modulation index * @synonyms fm * @example @@ -543,7 +532,7 @@ export const { fmi, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2 * any of the 8 individual FMs (e.g. `fmenv4`) * * @name fmenv - * @tags fx, superdough, supradough + * @tags fm, envelope, superdough, supradough * @param {number | Pattern} type lin | exp * @example * note("c e g b g e") @@ -566,7 +555,7 @@ export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fm * any of the 8 individual FMs (e.g. `fmatt5`) * * @name fmattack - * @tags fx, superdough, supradough + * @tags fm, envelope, superdough, supradough * @synonyms fmatt * @param {number | Pattern} time attack time * @example @@ -604,7 +593,7 @@ export const { * any of the 8 individual FMs (e.g. `fmwave6`) * * @name fmwave - * @tags fx, superdough, supradough + * @tags fm, superdough, supradough * @param {number | Pattern} wave waveform * @example * n("0 1 2 3".fast(4)).scale("d:minor").s("sine").fmwave("").fm(4).fmh(2.01) @@ -624,7 +613,7 @@ export const { fmwave, fmwave1, fmwave2, fmwave3, fmwave4, fmwave5, fmwave6, fmw * any of the 8 individual FMs (e.g. `fmdec6`) * * @name fmdecay - * @tags fx, superdough, supradough + * @tags fm, envelope, superdough, supradough * @synonyms fmdec * @param {number | Pattern} time decay time * @example @@ -663,7 +652,7 @@ export const { * any of the 8 individual FMs (e.g. `fmsus7`) * * @name fmsustain - * @tags fx, superdough, supradough + * @tags fm, envelope, superdough, supradough * @synonyms fmsus * @param {number | Pattern} level sustain level * @example @@ -702,7 +691,7 @@ export const { * any of the 8 individual FMs (e.g. `fmrel8`) * * @name fmrelease - * @tags fx, superdough, supradough + * @tags fm, envelope, superdough, supradough * @synonyms fmrel * @param {number | Pattern} time release time * @@ -755,7 +744,7 @@ export const { bank } = registerControl('bank'); * mix control for the chorus effect * * @name chorus - * @tags fx + * @tags pitch * @param {string | Pattern} chorus mix amount between 0 and 1 * @example * note("d d a# a").s("sawtooth").chorus(.5) @@ -768,12 +757,25 @@ export const { analyze } = registerControl('analyze'); // fftSize of analyser export const { fft } = registerControl('fft'); +/** + * Amplitude envelope attack time: Specifies how long it takes for the sound to reach its peak value, relative to the onset. + * + * @name attack + * @tags amplitude, envelope, superdough, supradough + * @param {number | Pattern} attack time in seconds. + * @synonyms att + * @example + * note("c3 e3 f3 g3").attack("<0 .1 .5>") + * + */ +export const { attack, att } = registerControl('attack', 'att'); + /** * Amplitude envelope decay time: the time it takes after the attack time to reach the sustain level. * Note that the decay is only audible if the sustain value is lower than 1. * * @name decay - * @tags fx, superdough, supradough + * @tags amplitude, envelope, superdough, supradough * @param {number | Pattern} time decay time in seconds * @synonyms dec * @example @@ -785,7 +787,7 @@ export const { decay, dec } = registerControl('decay', 'dec'); * Amplitude envelope sustain level: The level which is reached after attack / decay, being sustained until the offset. * * @name sustain - * @tags fx, superdough, supradough + * @tags amplitude, envelope, superdough, supradough * @param {number | Pattern} gain sustain level between 0 and 1 * @synonyms sus * @example @@ -797,7 +799,7 @@ export const { sustain, sus } = registerControl('sustain', 'sus'); * Amplitude envelope release time: The time it takes after the offset to go from sustain level to zero. * * @name release - * @tags fx, superdough, supradough + * @tags amplitude, envelope, superdough, supradough * @param {number | Pattern} time release time in seconds * @synonyms rel * @example @@ -812,7 +814,7 @@ export const { hold } = registerControl('hold'); * can also optionally supply the 'bpq' parameter separated by ':'. * * @name bpf - * @tags fx, superdough, supradough + * @tags filter, superdough, supradough * @param {number | Pattern} frequency center frequency * @synonyms bandf, bp * @example @@ -825,7 +827,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], ' * Sets the **b**and-**p**ass **q**-factor (resonance). * * @name bpq - * @tags fx, superdough, supradough + * @tags filter, superdough, supradough * @param {number | Pattern} q q factor * @synonyms bandq * @example @@ -838,7 +840,6 @@ export const { bandq, bpq } = registerControl('bandq', 'bpq'); /** * A pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample. * - * @memberof Pattern * @name begin * @tags samples * @param {number | Pattern} amount between 0 and 1, where 1 is the length of the sample @@ -905,7 +906,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope'); * Bit crusher effect. * * @name crush - * @tags fx, superdough, supradough + * @tags superdough, supradough * @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction). * @example * s(",hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>") @@ -917,7 +918,7 @@ export const { crush } = registerControl('crush'); * Fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers * * @name coarse - * @tags fx, superdough, supradough + * @tags superdough, supradough * @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on. * @example * s("bd sd [~ bd] sd,hh*8").coarse("<1 4 8 16 32>") @@ -929,7 +930,7 @@ export const { coarse } = registerControl('coarse'); * Modulate the amplitude of a sound with a continuous waveform * * @name tremolo - * @tags fx, superdough + * @tags amplitude, lfo, superdough * @synonyms trem * @param {number | Pattern} speed modulation speed in HZ * @example @@ -942,7 +943,7 @@ export const { tremolo, trem } = registerControl(['tremolo', 'tremolodepth', 'tr * Modulate the amplitude of a sound with a continuous waveform * * @name tremolosync - * @tags fx, superdough + * @tags amplitude, lfo, superdough * @synonyms tremsync * @param {number | Pattern} cycles modulation speed in cycles * @example @@ -958,7 +959,7 @@ export const { tremolosync } = registerControl( * Depth of amplitude modulation * * @name tremolodepth - * @tags fx, superdough + * @tags amplitude, lfo, superdough * @synonyms tremdepth * @param {number | Pattern} depth * @example @@ -970,7 +971,7 @@ export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth'); * Alter the shape of the modulation waveform * * @name tremoloskew - * @tags fx, superdough + * @tags amplitude, lfo, superdough * @synonyms tremskew * @param {number | Pattern} amount between 0 & 1, the shape of the waveform * @example @@ -983,7 +984,7 @@ export const { tremoloskew } = registerControl('tremoloskew', 'tremskew'); * Alter the phase of the modulation waveform * * @name tremolophase - * @tags fx, superdough + * @tags amplitude, lfo, superdough * @synonyms tremphase * @param {number | Pattern} offset the offset in cycles of the modulation * @example @@ -996,7 +997,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase'); * Shape of amplitude modulation * * @name tremoloshape - * @tags fx, superdough + * @tags amplitude, lfo, superdough * @synonyms tremshape * @param {number | Pattern} shape tri | square | sine | saw | ramp * @example @@ -1008,7 +1009,7 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape'); * Filter overdrive for supported filter types * * @name drive - * @tags fx, superdough + * @tags filter, superdough * @param {number | Pattern} amount * @example * note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>") @@ -1022,7 +1023,7 @@ export const { drive } = registerControl('drive'); * Can be applied to multiple orbits with the ':' mininotation, e.g. `duckorbit("2:3")` * * @name duckorbit - * @tags fx, superdough + * @tags amplitude, orbit, superdough * @synonyms duck * @param {number | Pattern} orbit target orbit * @example @@ -1043,7 +1044,7 @@ export const { duck } = registerControl('duckorbit', 'duck'); * Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`. * * @name duckdepth - * @tags fx, superdough + * @tags amplitude, orbit, superdough * @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>")) @@ -1063,7 +1064,7 @@ export const { duckdepth } = registerControl('duckdepth'); * Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`. * * @name duckonset - * @tags fx, superdough + * @tags amplitude, envelope, orbit, superdough * @synonyms duckons * * @param {number | Pattern} time The onset time in seconds @@ -1091,7 +1092,7 @@ export const { duckonset } = registerControl('duckonset', 'duckons'); * Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`. * * @name duckattack - * @tags fx, superdough + * @tags amplitude, envelope, orbit, superdough * @synonyms duckatt * * @param {number | Pattern} time The attack time in seconds @@ -1111,7 +1112,7 @@ export const { duckattack } = registerControl('duckattack', 'duckatt'); * * @name byteBeatExpression * @synonyms bbexpr - * @tags fx + * @tags superdough * * @param {number | Pattern} byteBeatExpression bitwise expression for creating bytebeat * @example @@ -1125,7 +1126,7 @@ export const { byteBeatExpression, bbexpr } = registerControl('byteBeatExpressio * * @name byteBeatStartTime * @synonyms bbst - * @tags fx + * @tags superdough * * @param {number | Pattern} byteBeatStartTime in samples (t) * @example @@ -1138,7 +1139,7 @@ export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime', * Allows you to set the output channels on the interface * * @name channels - * @tags external_io + * @tags external_io, superdough * @synonyms ch * * @param {number | Pattern} channels pattern the output channels @@ -1152,7 +1153,7 @@ export const { channels, ch } = registerControl('channels', 'ch'); * Controls the pulsewidth of the pulse oscillator * * @name pw - * @tags fx + * @tags superdough * @param {number | Pattern} pulsewidth * @example * note("{f a c e}%16").s("pulse").pw(".8:1:.2") @@ -1165,7 +1166,7 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']); * Controls the lfo rate for the pulsewidth of the pulse oscillator * * @name pwrate - * @tags fx + * @tags superdough, lfo * @param {number | Pattern} rate * @example * n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>") @@ -1178,7 +1179,7 @@ export const { pwrate } = registerControl('pwrate'); * Controls the lfo sweep for the pulsewidth of the pulse oscillator * * @name pwsweep - * @tags fx + * @tags superdough, lfo * @param {number | Pattern} sweep * @example * n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>") @@ -1190,7 +1191,7 @@ export const { pwsweep } = registerControl('pwsweep'); * Phaser audio effect that approximates popular guitar pedals. * * @name phaser - * @tags fx, superdough + * @tags superdough * @synonyms ph * @param {number | Pattern} speed speed of modulation * @example @@ -1208,7 +1209,7 @@ export const { phaserrate, ph, phaser } = registerControl( * The frequency sweep range of the lfo for the phaser effect. Defaults to 2000 * * @name phasersweep - * @tags fx, superdough + * @tags superdough, lfo * @synonyms phs * @param {number | Pattern} phasersweep most useful values are between 0 and 4000 * @example @@ -1222,7 +1223,7 @@ export const { phasersweep, phs } = registerControl('phasersweep', 'phs'); * The center frequency of the phaser in HZ. Defaults to 1000 * * @name phasercenter - * @tags fx, superdough + * @tags superdough * @synonyms phc * @param {number | Pattern} centerfrequency in HZ * @example @@ -1237,7 +1238,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc'); * The amount the signal is affected by the phaser effect. Defaults to 0.75 * * @name phaserdepth - * @tags fx, superdough, superdirt + * @tags superdough, superdirt * @synonyms phd, phasdp * @param {number | Pattern} depth number between 0 and 1 * @example @@ -1249,10 +1250,10 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc'); export const { phaserdepth, phd, phasdp } = registerControl('phaserdepth', 'phd', 'phasdp'); /** - * Choose the channel the pattern is sent to in superdirt + * Choose the channel the pattern is sent to * * @name channel - * @tags fx + * @tags superdough * @param {number | Pattern} channel channel number * */ @@ -1261,7 +1262,7 @@ export const { channel } = registerControl('channel'); * In the style of classic drum-machines, `cut` will stop a playing sample as soon as another samples with in same cutgroup is to be played. An example would be an open hi-hat followed by a closed one, essentially muting the open. * * @name cut - * @tags fx + * @tags superdough * @param {number | Pattern} group cut group number * @example * s("[oh hh]*4").cut(1) @@ -1274,7 +1275,7 @@ export const { cut } = registerControl('cut'); * When using mininotation, you can also optionally add the 'lpq' parameter, separated by ':'. * * @name lpf - * @tags fx, superdough, supradough + * @tags filter, superdough, supradough * @param {number | Pattern} frequency audible between 0 and 20000 * @synonyms cutoff, ctf, lp * @example @@ -1288,7 +1289,7 @@ export const { cutoff, ctf, lpf, lp } = registerControl(['cutoff', 'resonance', /** * Sets the lowpass filter envelope modulation depth. * @name lpenv - * @tags fx, superdough, supradough + * @tags filter, envelope, superdough, supradough * @param {number | Pattern} modulation depth of the lowpass filter envelope between 0 and _n_ * @synonyms lpe * @example @@ -1302,7 +1303,7 @@ export const { lpenv, lpe } = registerControl('lpenv', 'lpe'); /** * Sets the highpass filter envelope modulation depth. * @name hpenv - * @tags fx, superdough, supradough + * @tags filter, envelope, superdough, supradough * @param {number | Pattern} modulation depth of the highpass filter envelope between 0 and _n_ * @synonyms hpe * @example @@ -1316,7 +1317,7 @@ export const { hpenv, hpe } = registerControl('hpenv', 'hpe'); /** * Sets the bandpass filter envelope modulation depth. * @name bpenv - * @tags fx, superdough, supradough + * @tags filter, envelope, superdough, supradough * @param {number | Pattern} modulation depth of the bandpass filter envelope between 0 and _n_ * @synonyms bpe * @example @@ -1330,7 +1331,7 @@ export const { bpenv, bpe } = registerControl('bpenv', 'bpe'); /** * Sets the attack duration for the lowpass filter envelope. * @name lpattack - * @tags fx, superdough, supradough + * @tags filter, envelope, superdough, supradough * @param {number | Pattern} attack time of the filter envelope * @synonyms lpa * @example @@ -1344,7 +1345,7 @@ export const { lpattack, lpa } = registerControl('lpattack', 'lpa'); /** * Sets the attack duration for the highpass filter envelope. * @name hpattack - * @tags fx, superdough, supradough + * @tags filter, envelope, superdough, supradough * @param {number | Pattern} attack time of the highpass filter envelope * @synonyms hpa * @example @@ -1358,7 +1359,7 @@ export const { hpattack, hpa } = registerControl('hpattack', 'hpa'); /** * Sets the attack duration for the bandpass filter envelope. * @name bpattack - * @tags fx, superdough, supradough + * @tags filter, envelope, superdough, supradough * @param {number | Pattern} attack time of the bandpass filter envelope * @synonyms bpa * @example @@ -1372,7 +1373,7 @@ export const { bpattack, bpa } = registerControl('bpattack', 'bpa'); /** * Sets the decay duration for the lowpass filter envelope. * @name lpdecay - * @tags fx, superdough, supradough + * @tags filter, envelope, superdough, supradough * @param {number | Pattern} decay time of the filter envelope * @synonyms lpd * @example @@ -1386,7 +1387,7 @@ export const { lpdecay, lpd } = registerControl('lpdecay', 'lpd'); /** * Sets the decay duration for the highpass filter envelope. * @name hpdecay - * @tags fx, superdough, supradough + * @tags filter, envelope, superdough, supradough * @param {number | Pattern} decay time of the highpass filter envelope * @synonyms hpd * @example @@ -1401,7 +1402,7 @@ export const { hpdecay, hpd } = registerControl('hpdecay', 'hpd'); /** * Sets the decay duration for the bandpass filter envelope. * @name bpdecay - * @tags fx, superdough, supradough + * @tags filter, envelope, superdough, supradough * @param {number | Pattern} decay time of the bandpass filter envelope * @synonyms bpd * @example @@ -1416,7 +1417,7 @@ export const { bpdecay, bpd } = registerControl('bpdecay', 'bpd'); /** * Sets the sustain amplitude for the lowpass filter envelope. * @name lpsustain - * @tags fx, superdough, supradough + * @tags filter, envelope, superdough, supradough * @param {number | Pattern} sustain amplitude of the lowpass filter envelope * @synonyms lps * @example @@ -1431,7 +1432,7 @@ export const { lpsustain, lps } = registerControl('lpsustain', 'lps'); /** * Sets the sustain amplitude for the highpass filter envelope. * @name hpsustain - * @tags fx, superdough, supradough + * @tags filter, envelope, superdough, supradough * @param {number | Pattern} sustain amplitude of the highpass filter envelope * @synonyms hps * @example @@ -1446,7 +1447,7 @@ export const { hpsustain, hps } = registerControl('hpsustain', 'hps'); /** * Sets the sustain amplitude for the bandpass filter envelope. * @name bpsustain - * @tags fx, superdough, supradough + * @tags filter, envelope, superdough, supradough * @param {number | Pattern} sustain amplitude of the bandpass filter envelope * @synonyms bps * @example @@ -1461,7 +1462,7 @@ export const { bpsustain, bps } = registerControl('bpsustain', 'bps'); /** * Sets the release time for the lowpass filter envelope. * @name lprelease - * @tags fx, superdough, supradough + * @tags filter, envelope, superdough, supradough * @param {number | Pattern} release time of the filter envelope * @synonyms lpr * @example @@ -1477,7 +1478,7 @@ export const { lprelease, lpr } = registerControl('lprelease', 'lpr'); /** * Sets the release time for the highpass filter envelope. * @name hprelease - * @tags fx, superdough, supradough + * @tags filter, envelope, superdough, supradough * @param {number | Pattern} release time of the highpass filter envelope * @synonyms hpr * @example @@ -1493,7 +1494,7 @@ export const { hprelease, hpr } = registerControl('hprelease', 'hpr'); /** * Sets the release time for the bandpass filter envelope. * @name bprelease - * @tags fx, superdough, supradough + * @tags filter, envelope, superdough, supradough * @param {number | Pattern} release time of the bandpass filter envelope * @synonyms bpr * @example @@ -1509,7 +1510,7 @@ export const { bprelease, bpr } = registerControl('bprelease', 'bpr'); /** * Sets the filter type. The ladder filter is more aggressive. More types might be added in the future. * @name ftype - * @tags fx + * @tags filter, superdough * @param {number | Pattern} type 12db (0), ladder (1), or 24db (2) * @example * note("{f g g c d a a#}%8").s("sawtooth").lpenv(4).lpf(500).ftype("<0 1 2>").lpq(1) @@ -1525,7 +1526,7 @@ export const { ftype } = registerControl('ftype'); /** * controls the center of the filter envelope. 0 is unipolar positive, .5 is bipolar, 1 is unipolar negative * @name fanchor - * @tags fx, superdough + * @tags filter, envelope, superdough * @param {number | Pattern} center 0 to 1 * @example * note("{f g g c d a a#}%8").s("sawtooth").lpf("{1000}%2") @@ -1538,7 +1539,7 @@ export const { fanchor } = registerControl('fanchor'); * When using mininotation, you can also optionally add the 'hpq' parameter, separated by ':'. * * @name hpf - * @tags fx, superdough, supradough + * @tags filter, superdough, supradough * @param {number | Pattern} frequency audible between 0 and 20000 * @synonyms hp, hcutoff * @example @@ -1554,7 +1555,7 @@ export const { fanchor } = registerControl('fanchor'); * Rate of the LFO for the lowpass filter * * @name lprate - * @tags fx, superdough + * @tags filter, lfo, superdough * @param {number | Pattern} rate rate in hertz * @example * note("*16").s("sawtooth").lpf(600).lprate("<4 8 2 1>") @@ -1565,7 +1566,7 @@ export const { lprate } = registerControl('lprate'); * Cycle-synced rate of the LFO for the lowpass filter * * @name lpsync - * @tags fx, superdough + * @tags filter, lfo, superdough * @param {number | Pattern} rate rate in cycles * @example * note("*16").s("sawtooth").lpf(600).lpsync("<4 8 2 1>") @@ -1576,7 +1577,7 @@ export const { lpsync } = registerControl('lpsync'); * Depth of the LFO for the lowpass filter * * @name lpdepth - * @tags fx, superdough + * @tags filter, lfo, superdough * @param {number | Pattern} depth depth of modulation * @example * note("*16").s("sawtooth").lpf(600).lpdepth("<1 .5 1.8 0>") @@ -1587,7 +1588,7 @@ export const { lpdepth } = registerControl('lpdepth'); * Depth of the LFO for the lowpass filter, in HZ * * @name lpdepthfrequency - * @tags fx, superdough + * @tags filter, lfo, superdough * @synonyms lpdepthfreq * @param {number | Pattern} depth depth of modulation * @example @@ -1600,7 +1601,7 @@ export const { lpdepthfrequency, lpdepthfreq } = registerControl('lpdepthfrequen * Shape of the LFO for the lowpass filter * * @name lpshape - * @tags fx, superdough + * @tags filter, lfo, superdough * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) */ export const { lpshape } = registerControl('lpshape'); @@ -1609,7 +1610,7 @@ export const { lpshape } = registerControl('lpshape'); * DC offset of the LFO for the lowpass filter * * @name lpdc - * @tags fx, superdough + * @tags filter, lfo, superdough * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar */ export const { lpdc } = registerControl('lpdc'); @@ -1618,7 +1619,7 @@ export const { lpdc } = registerControl('lpdc'); * Skew of the LFO for the lowpass filter * * @name lpskew - * @tags fx, superdough + * @tags filter, lfo, superdough * @param {number | Pattern} skew How much to bend the LFO shape */ export const { lpskew } = registerControl('lpskew'); @@ -1627,7 +1628,7 @@ export const { lpskew } = registerControl('lpskew'); * Rate of the LFO for the bandpass filter * * @name bprate - * @tags fx, superdough + * @tags filter, lfo, superdough * @param {number | Pattern} rate rate in hertz */ export const { bprate } = registerControl('bprate'); @@ -1636,7 +1637,7 @@ export const { bprate } = registerControl('bprate'); * Cycle-synced rate of the LFO for the bandpass filter * * @name bpsync - * @tags fx, superdough + * @tags filter, lfo, superdough * @param {number | Pattern} rate rate in cycles */ export const { bpsync } = registerControl('bpsync'); @@ -1645,7 +1646,7 @@ export const { bpsync } = registerControl('bpsync'); * Depth of the LFO for the bandpass filter * * @name bpdepth - * @tags fx, superdough + * @tags filter, lfo, superdough * @param {number | Pattern} depth depth of modulation */ export const { bpdepth } = registerControl('bpdepth'); @@ -1654,7 +1655,7 @@ export const { bpdepth } = registerControl('bpdepth'); * Depth of the LFO for the bandpass filter, in HZ * * @name bpdepthfrequency - * @tags fx, superdough + * @tags filter, lfo, superdough * @synonyms bpdepthfreq * @param {number | Pattern} depth depth of modulation * @example @@ -1667,7 +1668,7 @@ export const { bpdepthfrequency, bpdepthfreq } = registerControl('bpdepthfrequen * Shape of the LFO for the bandpass filter * * @name bpshape - * @tags fx, superdough + * @tags filter, lfo, superdough * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) */ export const { bpshape } = registerControl('bpshape'); @@ -1676,7 +1677,7 @@ export const { bpshape } = registerControl('bpshape'); * DC offset of the LFO for the bandpass filter * * @name bpdc - * @tags fx, superdough + * @tags filter, lfo, superdough * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar */ export const { bpdc } = registerControl('bpdc'); @@ -1685,7 +1686,7 @@ export const { bpdc } = registerControl('bpdc'); * Skew of the LFO for the bandpass filter * * @name bpskew - * @tags fx, superdough + * @tags filter, lfo, superdough * @param {number | Pattern} skew How much to bend the LFO shape */ export const { bpskew } = registerControl('bpskew'); @@ -1694,7 +1695,7 @@ export const { bpskew } = registerControl('bpskew'); * Rate of the LFO for the highpass filter * * @name hprate - * @tags fx, superdough + * @tags filter, lfo, superdough * @param {number | Pattern} rate rate in hertz */ export const { hprate } = registerControl('hprate'); @@ -1703,7 +1704,7 @@ export const { hprate } = registerControl('hprate'); * Cycle-synced rate of the LFO for the highpass filter * * @name hpsync - * @tags fx, superdough + * @tags filter, lfo, superdough * @param {number | Pattern} rate rate in cycles */ export const { hpsync } = registerControl('hpsync'); @@ -1712,7 +1713,7 @@ export const { hpsync } = registerControl('hpsync'); * Depth of the LFO for the highpass filter * * @name hpdepth - * @tags fx, superdough + * @tags filter, lfo, superdough * @param {number | Pattern} depth depth of modulation */ export const { hpdepth } = registerControl('hpdepth'); @@ -1721,7 +1722,7 @@ export const { hpdepth } = registerControl('hpdepth'); * Depth of the LFO for the hipass filter, in hz * * @name hpdepthfrequency - * @tags fx, superdough + * @tags filter, lfo, superdough * @synonyms hpdepthfreq * @param {number | Pattern} depth depth of modulation * @example @@ -1734,7 +1735,7 @@ export const { hpdepthfrequency, hpdepthfreq } = registerControl('hpdepthfrequen * Shape of the LFO for the highpass filter * * @name hpshape - * @tags fx, superdough + * @tags filter, lfo, superdough * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) */ export const { hpshape } = registerControl('hpshape'); @@ -1743,7 +1744,7 @@ export const { hpshape } = registerControl('hpshape'); * DC offset of the LFO for the highpass filter * * @name hpdc - * @tags fx, superdough + * @tags filter, lfo, superdough * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar */ export const { hpdc } = registerControl('hpdc'); @@ -1752,7 +1753,7 @@ export const { hpdc } = registerControl('hpdc'); * Skew of the LFO for the highpass filter * * @name hpskew - * @tags fx, superdough + * @tags filter, lfo, superdough * @param {number | Pattern} skew How much to bend the LFO shape */ export const { hpskew } = registerControl('hpskew'); @@ -1761,7 +1762,7 @@ export const { hpskew } = registerControl('hpskew'); * Applies a vibrato to the frequency of the oscillator. * * @name vib - * @tags fx, superdough, supradough + * @tags pitch, lfo, superdough, supradough * @synonyms vibrato, v * @param {number | Pattern} frequency of the vibrato in hertz * @example @@ -1789,7 +1790,7 @@ export const { noise } = registerControl('noise'); * Sets the vibrato depth in semitones. Only has an effect if `vibrato` | `vib` | `v` is is also set * * @name vibmod - * @tags fx, superdough, supradough + * @tags pitch, lfo, superdough, supradough * @synonyms vmod * @param {number | Pattern} depth of vibrato (in semitones) * @example @@ -1808,7 +1809,7 @@ export const { hcutoff, hpf, hp } = registerControl(['hcutoff', 'hresonance', 'h * Controls the **h**igh-**p**ass **q**-value. * * @name hpq - * @tags fx, superdough, supradough + * @tags filter, superdough, supradough * @param {number | Pattern} q resonance factor between 0 and 50 * @synonyms hresonance * @example @@ -1820,7 +1821,7 @@ export const { hresonance, hpq } = registerControl('hresonance', 'hpq'); * Controls the **l**ow-**p**ass **q**-value. * * @name lpq - * @tags fx, superdough, supradough + * @tags filter, superdough, supradough * @param {number | Pattern} q resonance factor between 0 and 50 * @synonyms resonance * @example @@ -1833,7 +1834,7 @@ export const { resonance, lpq } = registerControl('resonance', 'lpq'); * DJ filter, below 0.5 is low pass filter, above is high pass filter. * * @name djf - * @tags fx, superdough + * @tags filter, superdough * @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter * @example * n(irand(16).seg(8)).scale("d:phrygian").s("supersaw").djf("<.5 .3 .2 .75>") @@ -1850,7 +1851,7 @@ export const { djf } = registerControl('djf'); * * * @name delay - * @tags fx, superdough, supradough + * @tags orbit, superdough, supradough * @param {number | Pattern} level between 0 and 1 * @example * s("bd bd").delay("<0 .25 .5 1>") @@ -1864,7 +1865,7 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback'] * Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it * * @name delayfeedback - * @tags fx, superdough, supradough + * @tags orbit, superdough, supradough * @param {number | Pattern} feedback between 0 and 1 * @synonyms delayfb, dfb * @example @@ -1878,7 +1879,7 @@ export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', * Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it * * @name delayfeedback - * @tags fx, superdough, supradough + * @tags orbit, superdough, supradough * @param {number | Pattern} feedback between 0 and 1 * @synonyms delayfb, dfb * @example @@ -1890,7 +1891,7 @@ export const { delayspeed } = registerControl('delayspeed'); * Sets the time of the delay effect. * * @name delayspeed - * @tags fx + * @tags supradough * @param {number | Pattern} delayspeed controls the pitch of the delay feedback * @synonyms delayt, dt * @example @@ -1903,7 +1904,7 @@ export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', * Sets the time of the delay effect in cycles. * * @name delaysync - * @tags fx, superdough + * @tags orbit, superdough * @param {number | Pattern} cycles delay length in cycles * @synonyms delayt, dt * @example @@ -1916,7 +1917,7 @@ export const { delaysync } = registerControl('delaysync'); * Specifies whether delaytime is calculated relative to cps. * * @name lock - * @tags fx, superdirt + * @tags superdirt * @param {number | Pattern} enable When set to 1, delaytime is a direct multiple of a cycle. * @superdirtOnly * @example @@ -1927,10 +1928,10 @@ export const { delaysync } = registerControl('delaysync'); export const { lock } = registerControl('lock'); /** - * Set detune for stacked voices of supported oscillators + * Set detune for stacked voices of supported oscillators. * * @name detune - * @tags fx + * @tags pitch, superdough * @param {number | Pattern} amount * @synonyms det * @example @@ -1939,10 +1940,10 @@ export const { lock } = registerControl('lock'); */ export const { detune, det } = registerControl('detune', 'det'); /** - * Set number of stacked voices for supported oscillators + * Set number of stacked voices for supported oscillators. * * @name unison - * @tags fx + * @tags superdough * @param {number | Pattern} numvoices * @example * note("d f a a# a d3").fast(2).s("supersaw").unison("<1 2 7>") @@ -1954,7 +1955,7 @@ export const { unison } = registerControl('unison'); * Set the stereo pan spread for supported oscillators * * @name spread - * @tags fx + * @tags superdough * @param {number | Pattern} spread between 0 and 1 * @example * note("d f a a# a d3").fast(2).s("supersaw").spread("<0 .3 1>") @@ -1965,7 +1966,7 @@ export const { spread } = registerControl('spread'); * Set dryness of reverb. See `room` and `size` for more information about reverb. * * @name dry - * @tags fx, superdough, superdirt + * @tags superdirt * @param {number | Pattern} dry 0 = wet, 1 = dry * @example * n("[0,3,7](3,8)").s("superpiano").room(.7).dry("<0 .5 .75 1>").osc() @@ -1973,11 +1974,11 @@ export const { spread } = registerControl('spread'); * */ export const { dry } = registerControl('dry'); -// TODO: does not seem to do anything -/* +/** * Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope. * * @name fadeTime + * @tags superdirt * @synonyms fadeOutTime * @param {number | Pattern} time between 0 and 1 * @example @@ -1985,13 +1986,12 @@ export const { dry } = registerControl('dry'); * */ export const { fadeTime, fadeOutTime } = registerControl('fadeTime', 'fadeOutTime'); -// TODO: see above export const { fadeInTime } = registerControl('fadeInTime'); /** * Set frequency of sound. * * @name freq - * @tags temporal + * @tags pitch, superdough * @param {number | Pattern} frequency in Hz. the audible range is between 20 and 20000 Hz * @example * freq("220 110 440 110").s("superzow").osc() @@ -2005,7 +2005,7 @@ export const { freq } = registerControl('freq'); * Attack time of pitch envelope. * * @name pattack - * @tags fx, superdough, supradough + * @tags pitch, envelope, superdough, supradough * @synonyms patt * @param {number | Pattern} time time in seconds * @example @@ -2017,7 +2017,7 @@ export const { pattack, patt } = registerControl('pattack', 'patt'); * Decay time of pitch envelope. * * @name pdecay - * @tags fx, superdough, supradough + * @tags pitch, envelope, superdough, supradough * @synonyms pdec * @param {number | Pattern} time time in seconds * @example @@ -2031,7 +2031,7 @@ export const { psustain, psus } = registerControl('psustain', 'psus'); * Release time of pitch envelope * * @name prelease - * @tags fx, superdough, supradough + * @tags pitch, envelope, superdough, supradough * @synonyms prel * @param {number | Pattern} time time in seconds * @example @@ -2046,7 +2046,7 @@ export const { prelease, prel } = registerControl('prelease', 'prel'); * If you don't set other pitch envelope controls, `pattack:.2` will be the default. * * @name penv - * @tags fx, superdough, supradough + * @tags pitch, envelope, superdough, supradough * @param {number | Pattern} semitones change in semitones * @example * note("c") @@ -2058,7 +2058,7 @@ export const { penv } = registerControl('penv'); * Curve of envelope. Defaults to linear. exponential is good for kicks * * @name pcurve - * @tags fx + * @tags pitch, envelope, superdough * @param {number | Pattern} type 0 = linear, 1 = exponential * @example * note("g1*4") @@ -2075,7 +2075,7 @@ export const { pcurve } = registerControl('pcurve'); * If you don't set an anchor, the value will default to the psustain value. * * @name panchor - * @tags fx + * @tags pitch, envelope, superdough * @param {number | Pattern} anchor anchor offset * @example * note("c c4").penv(12).panchor("<0 .5 1 .5>") @@ -2097,7 +2097,7 @@ export const { gate, gat } = registerControl('gate', 'gat'); * Emulation of a Leslie speaker: speakers rotating in a wooden amplified cabinet. * * @name leslie - * @tags fx, superdirt + * @tags superdirt * @param {number | Pattern} wet between 0 and 1 * @example * n("0,4,7").s("supersquare").leslie("<0 .4 .6 1>").osc() @@ -2109,7 +2109,7 @@ export const { leslie } = registerControl('leslie'); * Rate of modulation / rotation for leslie effect * * @name lrate - * @tags fx, superdirt + * @tags superdirt * @param {number | Pattern} rate 6.7 for fast, 0.7 for slow * @example * n("0,4,7").s("supersquare").leslie(1).lrate("<1 2 4 8>").osc() @@ -2122,7 +2122,7 @@ export const { lrate } = registerControl('lrate'); * Physical size of the cabinet in meters. Be careful, it might be slightly larger than your computer. Affects the Doppler amount (pitch warble) * * @name lsize - * @tags fx, superdirt + * @tags superdirt * @param {number | Pattern} meters somewhere between 0 and 1 * @example * n("0,4,7").s("supersquare").leslie(1).lrate(2).lsize("<.1 .5 1>").osc() @@ -2171,7 +2171,7 @@ export const { nudge } = registerControl('nudge'); * Sets the default octave of a synth. * * @name octave - * @tags fx, superdirt + * @tags superdirt * @synonyms oct * @param {number | Pattern} octave octave number * @example @@ -2185,7 +2185,7 @@ export const { octave, oct } = registerControl('octave', 'oct'); * An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects. * * @name orbit - * @tags fx, superdough + * @tags superdough * @synonyms o * @param {number | Pattern} number * @example @@ -2204,7 +2204,7 @@ export const { orbit } = registerControl('orbit', 'o'); * otherPat.bmod(..) (to modulate another pattern with the bus) * * @name bus - * @tags fx, superdough + * @tags superdirt * @param {number | Pattern} number */ export const { bus } = registerControl('bus'); @@ -2213,7 +2213,7 @@ export const { bus } = registerControl('bus'); * Postgain multiplier prior to sending the signal to the audio bus. * * @name busgain - * @tags fx, superdough + * @tags superdirt * @synonyms bgain * @param {number | Pattern} number */ @@ -2227,7 +2227,7 @@ export const { overshape } = registerControl('overshape'); * Sets position in stereo. * * @name pan - * @tags fx, superdough, supradough + * @tags superdough, supradough * @param {number | Pattern} pan between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel) * @example * s("[bd hh]*2").pan("<.5 1 .5 0>") @@ -2236,22 +2236,22 @@ export const { overshape } = registerControl('overshape'); * */ export const { pan } = registerControl('pan'); -// TODO: this has no effect (see example) -/* +/** * Controls how much multichannel output is fanned out * * @name panspan + * @tags superdirt * @param {number | Pattern} span between -inf and inf, negative is backwards ordering * @example * s("[bd hh]*2").pan("<.5 1 .5 0>").panspan("<0 .5 1>").osc() * */ export const { panspan } = registerControl('panspan'); -// TODO: this has no effect (see example) -/* +/** * Controls how much multichannel output is spread * * @name pansplay + * @tags superdirt * @param {number | Pattern} spread between 0 and 1 * @example * s("[bd hh]*2").pan("<.5 1 .5 0>").pansplay("<0 .5 1>").osc() @@ -2276,7 +2276,7 @@ export const { voice } = registerControl('voice'); /** * The chord to voice * @name chord - * @tags music_theory + * @tags tonal * @param {string | Pattern} symbols chord symbols to voice e.g., C, Eb, Fm7, G7. The symbols can be defined via addVoicings * @example * chord("").voicing() @@ -2286,7 +2286,7 @@ export const { chord } = registerControl('chord'); * Which dictionary to use for the voicings. This falls back to the default dictionary if not provided * * @name dictionary - * @tags music_theory + * @tags tonal * @param {string} dictionaryName which dictionary (having been defined with `addVoicings`) to use * @example * addVoicings('house', { @@ -2301,17 +2301,19 @@ export const { dictionary, dict } = registerControl('dictionary', 'dict'); /** The top note to align the voicing to. Defaults to c5 * * @name anchor - * @tags music_theory - * @param {string | Pattern} anchorNote the note to align the voicings to + * @tags tonal + * @param {string | Pattern} anchorNote the note to align the voicing or scale to * @example * anchor("").chord("C").voicing() + * @example + * n("0 .. 7").anchor("").scale("") **/ export const { anchor } = registerControl('anchor'); /** * Sets how the voicing is offset from the anchored position * * @name offset - * @tags music_theory + * @tags tonal * @param {number | Pattern} shift the amount to shift the voicing up or down * @example * chord("").offset("<0 1 2 3 4 5>") // alter the voicing each time @@ -2321,7 +2323,7 @@ export const { offset } = registerControl('offset'); * How many octaves are voicing steps spread apart, defaults to 1 * * @name octaves - * @tags music_theory + * @tags tonal * @param {number | Pattern} count the number of octaves * @example * chord("").octaves("<2 4>").voicing() @@ -2331,7 +2333,7 @@ export const { octaves } = registerControl('octaves'); * Remove anchor note from the voicing. Useful for melody harmonization * * @name mode - * @tags music_theory + * @tags tonal * @param {string | Pattern} modeName one of {below | above | duck | root} * @example * mode("").chord("C").voicing() @@ -2345,7 +2347,7 @@ export const { mode } = registerControl(['mode', 'anchor']); * When using mininotation, you can also optionally add the 'size' parameter, separated by ':'. * * @name room - * @tags fx, superdough + * @tags orbit, superdough * @param {number | Pattern} level between 0 and 1 * @example * s("bd sd [~ bd] sd").room("<0 .2 .4 .6 .8 1>") @@ -2359,7 +2361,7 @@ export const { room } = registerControl(['room', 'size']); * When this property is changed, the reverb will be recaculated, so only change this sparsely.. * * @name roomlp - * @tags fx, superdough + * @tags orbit, superdough * @synonyms rlp * @param {number} frequency between 0 and 20000hz * @example @@ -2373,7 +2375,7 @@ export const { roomlp, rlp } = registerControl('roomlp', 'rlp'); * When this property is changed, the reverb will be recaculated, so only change this sparsely.. * * @name roomdim - * @tags fx, superdough + * @tags orbit, superdough * @synonyms rdim * @param {number} frequency between 0 and 20000hz * @example @@ -2388,7 +2390,7 @@ export const { roomdim, rdim } = registerControl('roomdim', 'rdim'); * When this property is changed, the reverb will be recaculated, so only change this sparsely.. * * @name roomfade - * @tags fx, superdough + * @tags orbit, superdough * @synonyms rfade * @param {number} seconds for the reverb to fade * @example @@ -2401,7 +2403,7 @@ export const { roomfade, rfade } = registerControl('roomfade', 'rfade'); /** * Sets the sample to use as an impulse response for the reverb. * @name iresponse - * @tags fx, superdough + * @tags orbit, superdough * @param {string | Pattern} sample to use as an impulse response * @synonyms ir * @example @@ -2413,7 +2415,7 @@ export const { ir, iresponse } = registerControl(['ir', 'i'], 'iresponse'); /** * Sets speed of the sample for the impulse response. * @name irspeed - * @tags fx, superdough + * @tags orbit, superdough * @param {string | Pattern} speed * @example * samples('github:switchangel/pad') @@ -2425,7 +2427,7 @@ export const { irspeed } = registerControl('irspeed'); /** * Sets the beginning of the IR response sample * @name irbegin - * @tags fx, superdough + * @tags orbit, superdough * @param {string | Pattern} begin between 0 and 1 * @synonyms ir * @example @@ -2439,7 +2441,7 @@ export const { irbegin } = registerControl('irbegin'); * When this property is changed, the reverb will be recaculated, so only change this sparsely.. * * @name roomsize - * @tags fx, superdough + * @tags orbit, superdough * @param {number | Pattern} size between 0 and 10 * @synonyms rsize, sz, size * @example @@ -2463,7 +2465,7 @@ export const { roomsize, size, sz, rsize } = registerControl('roomsize', 'size', * * * @name shape - * @tags fx, superdough + * @tags distortion, superdough * @param {number | Pattern} distortion between 0 and 1 * @example * s("bd sd [~ bd] sd,hh*8").shape("<0 .2 .4 .6 .8>") @@ -2476,7 +2478,7 @@ export const { shape } = registerControl(['shape', 'shapevol']); * Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;) * * @name distort - * @tags fx, superdough, supradough + * @tags distortion, superdough, supradough * @synonyms dist * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion @@ -2495,8 +2497,8 @@ export const { distort, dist } = registerControl(['distort', 'distortvol', 'dist * Postgain for waveshaping distortion. * * @name distortvol - * @synonyms distvol - * @tags fx, superdough, supradough + * @synonyms distortion, distvol + * @tags superdough, supradough * @param {number | Pattern} volume linear postgain of the distortion * @example * s("bd*4").bank("tr909").distort(2).distortvol(0.8) @@ -2507,7 +2509,7 @@ export const { distortvol } = registerControl('distortvol', 'distvol'); * Type of waveshaping distortion to apply. * * @name distorttype - * @tags fx, superdough, supradough + * @tags distortion, superdough, supradough * @synonyms disttype * @param {number | string | Pattern} type type of distortion to apply * @example @@ -2526,7 +2528,7 @@ export const { distorttype } = registerControl('distorttype', 'disttype'); * More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties) * * @name compressor - * @tags fx, superdough + * @tags superdough * @example * s("bd sd [~ bd] sd,hh*8") * .compressor("-20:20:10:.002:.02") @@ -2547,7 +2549,7 @@ export const { compressorRelease } = registerControl('compressorRelease'); * Changes the speed of sample playback, i.e. a cheap way of changing pitch. * * @name speed - * @tags fx + * @tags pitch, samples * @param {number | Pattern} speed -inf to inf, negative numbers play the sample backwards. * @example * s("bd*6").speed("1 2 4 1 -2 -4") @@ -2561,7 +2563,7 @@ export const { speed } = registerControl('speed'); * Changes the speed of sample playback, i.e. a cheap way of changing pitch. * * @name stretch - * @tags fx + * @tags pitch, samples * @param {number | Pattern} factor -inf to inf, negative numbers play the sample backwards. * @example * s("gm_flute").stretch("1 2 .5") @@ -2572,7 +2574,7 @@ export const { stretch } = registerControl('stretch'); * Used in conjunction with `speed`, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`. * * @name unit - * @tags fx, superdirt + * @tags superdirt * @param {number | string | Pattern} unit see description above * @example * speed("1 2 .5 3").s("bd").unit("c").osc() @@ -2587,7 +2589,7 @@ export const { unit } = registerControl('unit'); * "A simplistic pitch-raising algorithm. It's not meant to sound natural; its sound is reminiscent of some weird mixture of filter, ring-modulator and pitch-shifter, depending on the input. The algorithm works by cutting the signal into fragments (delimited by upwards-going zero-crossings) and squeezing those fragments in the time domain (i.e. simply playing them back faster than they came in), leaving silences inbetween. All the parameters apart from memlen can be modulated." * * @name squiz - * @tags fx, superdirt + * @tags superdirt * @param {number | Pattern} squiz Try passing multiples of 2 to it - 2, 4, 8 etc. * @example * squiz("2 4/2 6 [8 16]").s("bd").osc() @@ -2612,7 +2614,7 @@ export const { squiz } = registerControl('squiz'); * Formant filter to make things sound like vowels. * * @name vowel - * @tags fx, superdough + * @tags superdough * @param {string | Pattern} vowel You can use a e i o u ae aa oe ue y uh un en an on, corresponding to [a] [e] [i] [o] [u] [æ] [ɑ] [ø] [y] [ɯ] [ʌ] [œ̃] [ɛ̃] [ɑ̃] [ɔ̃]. Aliases: aa = å = ɑ, oe = ø = ö, y = ı, ae = æ. * @example * note("[c2 >]*2").s('sawtooth') @@ -2634,10 +2636,10 @@ export const { vowel } = registerControl('vowel'); */ export const { waveloss } = registerControl('waveloss'); /** - * Noise crackle density + * crackle noise density * * @name density - * @tags fx, superdough + * @tags superdough * @param {number | Pattern} density between 0 and x * @example * s("crackle*4").density("<0.01 0.04 0.2 0.5>".slow(4)) @@ -2687,7 +2689,7 @@ export const { cps } = registerControl('cps'); * Multiplies the duration with the given number. Also cuts samples off at the end if they exceed the duration. * * @name clip - * @tags temporal + * @tags superdough * @synonyms legato * @param {number | Pattern} factor >= 0 * @example @@ -2700,7 +2702,7 @@ export const { clip, legato } = registerControl('clip', 'legato'); * Sets the duration of the event in cycles. Similar to clip / legato, it also cuts samples off at the end if they exceed the duration. * * @name duration - * @tags temporal + * @tags superdough * @synonyms dur * @param {number | Pattern} seconds >= 0 * @example @@ -2743,7 +2745,7 @@ export let createParams = (...names) => * ADSR envelope: Combination of Attack, Decay, Sustain, and Release. * * @name adsr - * @tags fx + * @tags envelope, amplitude * @param {number | Pattern} time attack time in seconds * @param {number | Pattern} time decay time in seconds * @param {number | Pattern} gain sustain level (0 to 1) @@ -2778,7 +2780,7 @@ export const ar = register('ar', (t, pat) => { * MIDI channel: Sets the MIDI channel for the event. * * @name midichan - * @tags external_io + * @tags external_io, midi * @param {number | Pattern} channel MIDI channel number (0-15) * @example * note("c4").midichan(1).midi() @@ -2791,7 +2793,7 @@ export const { midimap } = registerControl('midimap'); * MIDI port: Sets the MIDI port for the event. * * @name midiport - * @tags external_io + * @tags external_io, midi * @param {number | Pattern} port MIDI port * @example * note("c a f e").midiport("<0 1 2 3>").midi() @@ -2802,7 +2804,7 @@ export const { midiport } = registerControl('midiport'); * MIDI command: Sends a MIDI command message. * * @name midicmd - * @tags external_io + * @tags external_io, midi * @param {number | Pattern} command MIDI command * @example * midicmd("clock*48,/2").midi() @@ -2813,7 +2815,7 @@ export const { midicmd } = registerControl('midicmd'); * MIDI control: Sends a MIDI control change message. * * @name control - * @tags external_io + * @tags external_io, midi * @param {number | Pattern} MIDI control number (0-127) * @param {number | Pattern} MIDI controller value (0-127) */ @@ -2829,7 +2831,7 @@ export const control = register('control', (args, pat) => { * MIDI control number: Sends a MIDI control change message. * * @name ccn - * @tags external_io + * @tags external_io, midi * @param {number | Pattern} MIDI control number (0-127) */ export const { ccn } = registerControl('ccn'); @@ -2837,7 +2839,7 @@ export const { ccn } = registerControl('ccn'); * MIDI control value: Sends a MIDI control change message. * * @name ccv - * @tags external_io + * @tags external_io, midi * @param {number | Pattern} MIDI control value (0-127) */ export const { ccv } = registerControl('ccv'); @@ -2847,7 +2849,7 @@ export const { ctlNum } = registerControl('ctlNum'); /** * MIDI NRPN non-registered parameter number: Sends a MIDI NRPN non-registered parameter number message. * @name nrpnn - * @tags external_io + * @tags external_io, midi * @param {number | Pattern} nrpnn MIDI NRPN non-registered parameter number (0-127) * @example * note("c4").nrpnn("1:8").nrpv("123").midichan(1).midi() @@ -2856,7 +2858,7 @@ export const { nrpnn } = registerControl('nrpnn'); /** * MIDI NRPN non-registered parameter value: Sends a MIDI NRPN non-registered parameter value message. * @name nrpv - * @tags external_io + * @tags external_io, midi * @param {number | Pattern} nrpv MIDI NRPN non-registered parameter value (0-127) * @example * note("c4").nrpnn("1:8").nrpv("123").midichan(1).midi() @@ -2877,7 +2879,7 @@ export const { progNum } = registerControl('progNum'); /** * MIDI sysex: Sends a MIDI sysex message. * @name sysex - * @tags external_io + * @tags external_io, midi * @param {number | Pattern} id Sysex ID * @param {number | Pattern} data Sysex data * @example @@ -2893,7 +2895,7 @@ export const sysex = register('sysex', (args, pat) => { /** * MIDI sysex ID: Sends a MIDI sysex identifier message. * @name sysexid - * @tags external_io + * @tags external_io, midi * @param {number | Pattern} id Sysex ID * @example * note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi() @@ -2902,7 +2904,7 @@ export const { sysexid } = registerControl('sysexid'); /** * MIDI sysex data: Sends a MIDI sysex message. * @name sysexdata - * @tags external_io + * @tags external_io, midi * @param {number | Pattern} data Sysex data * @example * note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi() @@ -2912,7 +2914,7 @@ export const { sysexdata } = registerControl('sysexdata'); /** * MIDI pitch bend: Sends a MIDI pitch bend message. * @name midibend - * @tags external_io + * @tags external_io, midi * @param {number | Pattern} midibend MIDI pitch bend (-1 - 1) * @example * note("c4").midibend(sine.slow(4).range(-0.4,0.4)).midi() @@ -2921,7 +2923,7 @@ export const { midibend } = registerControl('midibend'); /** * MIDI key after touch: Sends a MIDI key after touch message. * @name miditouch - * @tags external_io + * @tags external_io, midi * @param {number | Pattern} miditouch MIDI key after touch (0-1) * @example * note("c4").miditouch(sine.slow(4).range(0,1)).midi() @@ -2962,7 +2964,7 @@ export const getControlName = (alias) => { * Sets properties in a batch. * * @name as - * @tags temporal + * @tags combiners * @param {String | Array} mapping the control names that are set * @example * "c:.5 a:1 f:.25 e:.8".as("note:clip") @@ -3128,7 +3130,7 @@ Pattern.prototype.modulate = function (type, config, idPat) { * a `sometimes`. See example below. * * @name lfo - * @tags fx, superdough + * @tags lfo, superdough * @param {Object} config LFO configuration. * @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 @@ -3183,7 +3185,7 @@ export const lfo = (config) => pure({}).lfo(config); * a `sometimes`. See example below. * * @name env - * @tags fx, superdough + * @tags envelope, superdough * @param {Object} config Envelope configuration. * @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 @@ -3244,7 +3246,7 @@ export const env = (config) => pure({}).env(config); * a `sometimes`. See example below. * * @name bmod - * @tags fx, superdough + * @tags superdough * @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: c @@ -3271,7 +3273,7 @@ export const bmod = (config) => pure({}).bmod(config); * and sustains * * @name transient - * @tags fx, superdough + * @tags superdough * @param {number | Pattern} attack Emphasis on transients; between -1 (deaccentuate) and 1 (accentuate) * @param {number | Pattern} sustain Emphasis on the sustains; between -1 (deaccentuate) and 1 (accentuate) * @example diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index df9990982..b63a702f8 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -818,7 +818,6 @@ export class Pattern { * @name layer * @tags combiners * @memberof Pattern - * @synonyms apply * @returns Pattern * @example * "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*8" @@ -1265,6 +1264,7 @@ const ALIGNMENT_KEYS = ALIGNMENTS.map((how) => how.toLowerCase()); * 'add.mix', 'set.squeeze', etc. * * @param {string} method Default join method to use. Options: 'in', 'out', 'mix', 'squeeze', 'squeezeout', 'reset', 'restart', 'poly' + * @tags combiners * @example * setDefaultJoin('mix') // also try 'in', 'out', 'squeeze', etc. * s("saw").vel("1 0.5").note("F A C E").delay("0 0.2 0.3") @@ -1564,10 +1564,10 @@ export function arrange(...sections) { * @tags combiners * @return {Pattern} * @example -seqPLoop([0, 2, "bd(3,8)"], - [1, 3, "cp(3,8)"] - ) - .sound() +seqPLoop( + [0, 2, "bd(3,8)"], + [1, 3, "cp(3,8)"] +).sound() */ export function seqPLoop(...parts) { let total = Fraction(0); @@ -1610,7 +1610,7 @@ export function sequence(...pats) { /** Like **cat**, but the items are crammed into one cycle. * @tags combiners - * @synonyms seq, fastcat + * @synonyms fastcat * @example * seq("e5", "b4", ["d5", "c5"]).note() * // "e5 b4 [d5 c5]".note() @@ -2137,13 +2137,12 @@ export const { firstOf, every } = register(['firstOf', 'every'], function (n, fu }); /** - * Like layer, but with a single function: - * @tags temporal + * Applies the given function to the pattern. Like layer, but with a single function: + * @tags combiners * @name apply * @example * "".scale('C minor').apply(scaleTranspose("0,2,4")).note() */ -// TODO: remove or dedupe with layer? export const apply = register('apply', function (func, pat) { return func(pat); }); @@ -2500,7 +2499,7 @@ export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func, /** * The jux function creates strange stereo effects, by applying a function to a pattern, but only in the right-hand channel. - * @tags temporal + * @tags temporal, superdough * @example * s("bd lt [~ ht] mt cp ~ bd hh").jux(rev) * @example @@ -2514,7 +2513,7 @@ export const jux = register('jux', function (func, pat) { /** * Superimpose and offset multiple times, applying the given function each time. - * @tags temporal + * @tags temporal, functional * @name echoWith * @synonyms echowith, stutWith, stutwith * @param {number} times how many times to repeat @@ -2688,7 +2687,7 @@ export const { repeatCycles } = register( /** * Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle). - * @tags temporal + * @tags temporal, functional * @name chunk * @synonyms slowChunk, slowchunk * @memberof Pattern @@ -2852,7 +2851,7 @@ Pattern.prototype.tag = function (tag) { /** * Filters haps using the given function * @name filter - * @tags temporal + * @tags temporal, functional * @param {Function} test function to test Hap * @example * s("hh!7 oh").filter(hap => hap.value.s === 'hh') @@ -2862,7 +2861,7 @@ export const filter = register('filter', (test, pat) => pat.withHaps((haps) => h /** * Filters haps by their begin time * @name filterWhen - * @tags temporal + * @tags temporal, functional * @param {Function} test function to test Hap.whole.begin * @example * oneCycle: s("bd*4").filterWhen((t) => t < 1) @@ -2872,7 +2871,7 @@ export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) = /** * Use within to apply a function to only a part of a pattern. * @name within - * @tags temporal + * @tags temporal, functional * @param {number} start start within cycle (0 - 1) * @param {number} end end within cycle (0 - 1). Must be > start * @param {Function} func function to be applied to the sub-pattern @@ -2947,7 +2946,7 @@ export function _match(span, hap_p) { * *Experimental* * * Speeds a pattern up or down, to fit to the given number of steps per cycle. - * @tags temporal + * @tags stepwise * @example * sound("bd sd cp").pace(4) * // The same as sound("{bd sd cp}%4") or sound("*4") @@ -2989,7 +2988,7 @@ export function _polymeterListSteps(steps, ...args) { * *Experimental* * * Aligns the steps of the patterns, creating polymeters. The patterns are repeated until they all fit the cycle. For example, in the below the first pattern is repeated twice, and the second is repeated three times, to fit the lowest common multiple of six steps. - * @tags temporal + * @tags stepwise * @synonyms pm * @example * // The same as note("{c eb g, c2 g2}%6") @@ -3022,7 +3021,7 @@ export function polymeter(...args) { * The steps can either be inferred from the pattern, or provided as a [length, pattern] pair. * Has the alias `timecat`. * @name stepcat - * @tags combiners + * @tags stepwise * @synonyms timeCat, timecat * @return {Pattern} * @example @@ -3080,7 +3079,7 @@ export function stepcat(...timepats) { * Concatenates patterns stepwise, according to an inferred 'steps per cycle'. * Similar to `stepcat`, but if an argument is a list, the whole pattern will alternate between the elements in the list. * - * @tags combiners + * @tags stepwise * @return {Pattern} * @example * stepalt(["bd cp", "mt"], "bd").sound() @@ -3107,7 +3106,7 @@ export function stepalt(...groups) { * * Takes the given number of steps from a pattern (dropping the rest). * A positive number will take steps from the start of a pattern, and a negative number from the end. - * @tags temporal + * @tags stepwise * @return {Pattern} * @example * "bd cp ht mt".take("2").sound() @@ -3152,7 +3151,7 @@ export const take = stepRegister('take', function (i, pat) { * * Drops the given number of steps from a pattern. * A positive number will drop steps from the start of a pattern, and a negative number from the end. - * @tags temporal + * @tags stepwise * @return {Pattern} * @example * "tha dhi thom nam".drop("1").sound().bank("mridangam") @@ -3181,7 +3180,7 @@ export const drop = stepRegister('drop', function (i, pat) { * `extend` is similar to `fast` in that it increases its density, but it also increases the step count * accordingly. So `stepcat("a b".extend(2), "c d")` would be the same as `"a b a b c d"`, whereas * `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`. - * @tags temporal + * @tags stepwise * @example * stepcat( * sound("bd bd - cp").extend(2), @@ -3200,7 +3199,7 @@ export const extend = stepRegister('extend', function (factor, pat) { * `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`. * * TODO: find out how this function differs from extend - * @tags temporal + * @tags stepwise * @example * stepcat( * sound("bd bd - cp").replicate(2), @@ -3215,7 +3214,7 @@ export const replicate = stepRegister('replicate', function (factor, pat) { * *Experimental* * * Expands the step size of the pattern by the given factor. - * @tags temporal + * @tags stepwise * @example * sound("tha dhi thom nam").bank("mridangam").expand("3 2 1 1 2 3").pace(8) */ @@ -3227,7 +3226,7 @@ export const expand = stepRegister('expand', function (factor, pat) { * *Experimental* * * Contracts the step size of the pattern by the given factor. See also `expand`. - * @tags temporal + * @tags stepwise * @example * sound("tha dhi thom nam").bank("mridangam").contract("3 2 1 1 2 3").pace(8) */ @@ -3282,7 +3281,7 @@ export const shrinklist = (amount, pat) => pat.shrinklist(amount); * Progressively shrinks the pattern by 'n' steps until there's nothing left, or if a second value is given (using mininotation list syntax with `:`), * that number of times. * A positive number will progressively drop steps from the start of a pattern, and a negative number from the end. - * @tags temporal + * @tags stepwise * @return {Pattern} * @example * "tha dhi thom nam".shrink("1").sound() @@ -3322,7 +3321,7 @@ export const shrink = register( * Progressively grows the pattern by 'n' steps until the full pattern is played, or if a second value is given (using mininotation list syntax with `:`), * that number of times. * A positive number will progressively grow steps from the start of a pattern, and a negative number from the end. - * @tags temporal + * @tags stepwise * @return {Pattern} * @example * "tha dhi thom nam".grow("1").sound() @@ -3363,7 +3362,7 @@ export const grow = register( * on successive repetitions. The patterns are added together stepwise, with all repetitions taking place over a single cycle. Using `pace` to set the * number of steps per cycle is therefore usually recommended. * - * @tags combiners + * @tags stepwise * @return {Pattern} * @example * "[c g]".tour("e f", "e f g", "g f e c").note() @@ -3390,7 +3389,7 @@ Pattern.prototype.tour = function (...many) { * 'zips' together the steps of the provided patterns. This can create a long repetition, taking place over a single, dense cycle. * Using `pace` to set the number of steps per cycle is therefore usually recommended. * - * @tags combiners + * @tags stepwise * @returns {Pattern} * @example * zip("e f", "e f g", "g [f e] a f4 c").note() @@ -3442,7 +3441,7 @@ Pattern.prototype.steps = Pattern.prototype.pace; * Cuts each sample into the given number of parts, allowing you to explore a technique known as 'granular synthesis'. * It turns a pattern of samples into a pattern of parts of samples. * @name chop - * @tags temporal + * @tags samples * @memberof Pattern * @returns Pattern * @example @@ -3473,7 +3472,7 @@ export const chop = register('chop', function (n, pat) { /** * Cuts each sample into the given number of parts, triggering progressive portions of each sample at each loop. * @name striate - * @tags temporal + * @tags samples * @memberof Pattern * @returns Pattern * @example @@ -3492,14 +3491,13 @@ export const striate = register('striate', function (n, pat) { /** * Makes the sample fit the given number of cycles by changing the speed. * @name loopAt - * @tags temporal + * @tags samples, pitch * @memberof Pattern * @returns Pattern * @example * samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) * s("rhodes").loopAt(2) */ -// TODO - global cps clock const _loopAt = function (factor, pat, cps = 0.5) { return pat .speed((1 / factor) * cps) @@ -3507,11 +3505,16 @@ const _loopAt = function (factor, pat, cps = 0.5) { .slow(factor); }; +export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) { + const steps = pat._steps ? pat._steps.div(factor) : undefined; + return new Pattern((state) => _loopAt(factor, pat, state.controls._cps).query(state), steps); +}); + /** * Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers. * Instead of a number, it also accepts a list of numbers from 0 to 1 to slice at specific points. * @name slice - * @tags temporal + * @tags samples * @memberof Pattern * @returns Pattern * @example @@ -3565,7 +3568,7 @@ Pattern.prototype.onTriggerTime = function (func) { /** * Works the same as slice, but changes the playback speed of each slice to match the duration of its step. * @name splice - * @tags temporal + * @tags samples, pitch * @example * samples('github:tidalcycles/dirt-samples') * s("breaks165") @@ -3594,16 +3597,11 @@ export const splice = register( false, // turns off auto-patternification ); -export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) { - const steps = pat._steps ? pat._steps.div(factor) : undefined; - return new Pattern((state) => _loopAt(factor, pat, state.controls._cps).query(state), steps); -}); - /** * Makes the sample fit its event duration. Good for rhythmical loops like drum breaks. * Similar to `loopAt`. * @name fit - * @tags temporal + * @tags samples, pitch * @example * samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) * s("rhodes/2").fit() @@ -3625,18 +3623,16 @@ export const fit = register('fit', (pat) => /** * Makes the sample fit the given number of cycles and cps value, by - * changing the speed. Please note that at some point cps will be - * given by a global clock and this function will be - * deprecated/removed. + * changing the speed. deprecated: use loopAt or fit instead, together with setCps / setCpm. * @name loopAtCps - * @tags temporal + * @tags samples, pitch * @memberof Pattern + * @deprecated * @returns Pattern * @example * samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) * s("rhodes").loopAtCps(4,1.5).cps(1.5) */ -// TODO - global cps clock export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) { return _loopAt(factor, pat, cps); }); @@ -3658,7 +3654,7 @@ let fadeGain = (p) => (p < 0.5 ? 1 : 1 - (p - 0.5) / 0.5); * - 1 = (no left, full right) * * @name xfade - * @tags combiners + * @tags amplitude * @example * xfade(s("bd*2"), "<0 .25 .5 .75 1>", s("hh*8")) */ @@ -3784,7 +3780,7 @@ const _distortWithAlg = function (name) { * Soft-clipping distortion * * @name soft - * @tags fx + * @tags distortion, superdough * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion * @@ -3795,7 +3791,7 @@ export const soft = _distortWithAlg('soft'); * Hard-clipping distortion * * @name hard - * @tags fx + * @tags distortion, superdough * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion * @@ -3806,7 +3802,7 @@ export const hard = _distortWithAlg('hard'); * Cubic polynomial distortion * * @name cubic - * @tags fx + * @tags distortion, superdough * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion * @@ -3817,7 +3813,7 @@ export const cubic = _distortWithAlg('cubic'); * Diode-emulating distortion * * @name diode - * @tags fx + * @tags distortion, superdough * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion * @@ -3828,7 +3824,7 @@ export const diode = _distortWithAlg('diode'); * Asymmetrical diode distortion * * @name asym - * @tags fx + * @tags distortion, superdough * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion * @@ -3839,7 +3835,7 @@ export const asym = _distortWithAlg('asym'); * Wavefolding distortion * * @name fold - * @tags fx + * @tags distortion, superdough * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion * @@ -3850,7 +3846,7 @@ export const fold = _distortWithAlg('fold'); * Wavefolding distortion composed with sinusoid * * @name sinefold - * @tags fx + * @tags distortion, superdough * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion * @@ -3861,7 +3857,7 @@ export const sinefold = _distortWithAlg('sinefold'); * Distortion via Chebyshev polynomials * * @name chebyshev - * @tags fx + * @tags distortion, superdough * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion * @@ -3895,7 +3891,7 @@ const _ensureListPattern = (list) => { * Can also be used to create a new synth via `s('user').partials(...)` * * @name partials - * @tags fx, superdough + * @tags superdough * @param {number[] | Pattern} magnitudes List of [0, 1] magnitudes for partials. 0th entry is the fundamental harmonic (i.e. DC offset is skipped) * @example * s("user").seg(16).n(irand(8)).scale("A:major") @@ -3917,7 +3913,7 @@ export const partials = (list) => { * Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases * * @name phases - * @tags fx, superdough + * @tags superdough * @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the fundamental phase (i.e. DC offset is skipped) * @example * // Phase cancellation @@ -3939,7 +3935,7 @@ export const phases = (list) => { * calls and/or in a single .FX(fx1, fx2, ..) call. The fx1, .. are _patterns_ which * establish the controls of the given effect. See examples. * @name FX - * @tags fx, superdough + * @tags superdough * @memberof Pattern * @returns Pattern * @example @@ -3990,7 +3986,7 @@ const _asArrayPattern = (pats) => { * by wrapping them inside a function in K (see example). * * @name K - * @tags generators, fx, superdough + * @tags generators, superdough * @param {KabelsalatExpression | Function} expr Kabelsalat graph definition * @memberof Pattern * @returns Pattern diff --git a/packages/core/pick.mjs b/packages/core/pick.mjs index cce691905..93b8acd4f 100644 --- a/packages/core/pick.mjs +++ b/packages/core/pick.mjs @@ -78,7 +78,7 @@ export const pickmod = register('pickmod', function (lookup, pat) { * s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)]) * @example * note("(3,8)").s("square") - * .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)]) + * .pickF("<0 2> 1", [jux(rev), fast(2), x=>x.lpf(800)]) */ export const pickF = register('pickF', function (lookup, funcs, pat) { return pat.apply(pick(lookup, funcs)); @@ -167,20 +167,22 @@ export const pickmodReset = register('pickmodReset', function (lookup, pat) { }); /** Picks patterns (or plain values) either from a list (by index) or a lookup table (by name). - * Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern. - * @name inhabit - * @tags combiners - * @synonyms pickSqueeze - * @param {Pattern} pat - * @param {*} xs - * @returns {Pattern} - * @example - * "".inhabit({a: s("bd(3,8)"), - b: s("cp sd") - }) - * @example - * s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4) - */ + * Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern. + * @name inhabit + * @tags combiners + * @synonyms pickSqueeze + * @param {Pattern} pat + * @param {*} xs + * @returns {Pattern} + * @example + * let a = s("bd(3,8)") + * let b = s("cp sd") + * "".inhabit({ a, b }) + * @example + * s("a@2 [a b] a" + * .inhabit({a: "bd(3,8)", b: "sd sd"})) + * .slow(4) + */ export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) { return _pick(lookup, pat, false).squeezeJoin(); }); diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 8ce26c2a6..bb52b994f 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -138,7 +138,7 @@ function githubPath(base, subpath = '') { /** * configures the default midimap, which is used when no "midimap" port is set - * @tags external_io + * @tags external_io, midi * @example * defaultmidimap({ lpf: 74 }) * $: note("c a f e").midi(); @@ -152,7 +152,7 @@ let loadCache = {}; /** * Adds midimaps to the registry. Inside each midimap, control names (e.g. lpf) are mapped to cc numbers. - * @tags external_io + * @tags external_io, midi * @example * midimaps({ mymap: { lpf: 74 } }) * $: note("c a f e") @@ -529,7 +529,7 @@ async function _initialize(input) { * The output is a function that accepts a midi cc value to query as well as (optionally) a midi channel * * @name midin - * @tags external_io + * @tags external_io, midi * @param {string | number} input MIDI device name or index defaulting to 0 * @returns {function(number, number=): Pattern} A function from (cc, channel?) to a pattern. * When queried, the pattern will produces the most recently received midi value (normalized to 0 to 1) @@ -577,7 +577,7 @@ export async function midin(input) { * note durations * * @name midikeys - * @tags external_io + * @tags external_io, midi * @param {string | number} input MIDI device name or index defaulting to 0 * @returns {function((number | Pattern)=): Pattern} A function that produces a pattern. * When queried, the pattern will produces the most recently played midi notes and velocities, diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 79da4f916..4ce8164bc 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -40,7 +40,7 @@ export let maxPolyphony = DEFAULT_MAX_POLYPHONY; * start to die out in first-in-first-out order once the max polyphony has been hit * * @name setMaxPolyphony - * @tags fx, superdough + * @tags superdough * @param {number} Max polyphony. Defaults to 128 * @example * setMaxPolyphony(4) @@ -74,7 +74,7 @@ export function applyGainCurve(val) { * quadratic, exponential, etc. rather than linear * * @name setGainCurve - * @tags fx, superdough + * @tags amplitude, superdough * @param {Function} function to apply to all gain values * @example * setGainCurve((x) => x * x) // quadratic gain diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 79519b5f0..9c2974fdf 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -186,7 +186,7 @@ export function registerWaveTable(key, tables, params) { * Loads a collection of wavetables to use with `s` * * @name tables - * @tags fx + * @tags wavetable */ export const tables = async (url, frameLen, json, options = {}) => { if (json !== undefined) return _processTables(json, url, frameLen); diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 2d197a7ed..4bba4561a 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -100,7 +100,7 @@ function scaleOffset(scale, offset, note) { * - 5P = perfect fifth * - 5d = diminished fifth * - * @tags music_theory + * @tags tonal * @param {string | number} amount Either number of semitones or interval string. * @returns Pattern * @memberof Pattern @@ -155,7 +155,7 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr * * @memberof Pattern * @name scaleTranspose - * @tags music_theory + * @tags tonal * @param {offset} offset number of steps inside the scale * @returns Pattern * @synonyms scaleTrans, strans @@ -246,7 +246,7 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) { * The root note defaults to octave 3, if no octave number is given. * * @name scale - * @tags music_theory + * @tags tonal * @param {string} scale Name of scale * @returns Pattern * @example diff --git a/packages/tonal/voicings.mjs b/packages/tonal/voicings.mjs index 6dce6ca8b..7dcc6a77b 100644 --- a/packages/tonal/voicings.mjs +++ b/packages/tonal/voicings.mjs @@ -90,7 +90,7 @@ export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistr * Adds a new custom voicing dictionary. * * @name addVoicings - * @tags music_theory + * @tags tonal * @memberof Pattern * @param {string} name identifier for the voicing dictionary * @param {Object} dictionary maps chord symbol to possible voicings @@ -134,7 +134,7 @@ const getVoicing = (chord, dictionaryName, lastVoicing) => { * Uses [chord-voicings package](https://github.com/felixroos/chord-voicings#chord-voicings). * * @name voicings - * @tags music_theory + * @tags tonal * @memberof Pattern * @param {string} dictionary which voicing dictionary to use. * @returns Pattern @@ -159,7 +159,7 @@ export const voicings = register('voicings', function (dictionary, pat) { * Maps the chords of the incoming pattern to root notes in the given octave. * * @name rootNotes - * @tags music_theory + * @tags tonal * @memberof Pattern * @param {octave} octave octave to use * @returns Pattern @@ -192,7 +192,7 @@ export const rootNotes = register('rootNotes', function (octave, pat) { * If you pass a pattern of strings to voicing, they will be interpreted as chords. * * @name voicing - * @tags music_theory + * @tags tonal * @returns Pattern * @example * n("0 1 2 3").chord("").voicing() diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index fcd6151b2..d31c5c00b 100644 --- a/website/src/repl/components/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -4,7 +4,11 @@ import jsdocJson from '../../../../../doc.json'; import { Textbox } from '@src/repl/components/panel/SettingsTab'; import { settingsMap, useSettings } from '@src/settings.mjs'; -const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description; +const isValid = ({ name, description, tags = [] }) => { + const isSupradoughOnly = tags.includes('supradough') && !tags.includes('superdough'); + const isSuperdirtOnly = tags.includes('superdirt') && !tags.includes('superdough'); + return name && !name.startsWith('_') && !!description && !isSupradoughOnly && !isSuperdirtOnly; +}; const availableFunctions = (() => { const seen = new Set(); // avoid repetition @@ -14,7 +18,7 @@ const availableFunctions = (() => { if (seen.has(doc.name)) continue; // jsdoc also uses "tags" for when you use @something in the comments and it doesn't know what - // @something is. We only want data from comments like `@tags fx, superdough` here. + // @something is. We only want data from comments like `@tags superdough` here. // If nothing is specified, we default to "untagged" for debugging doc.tags = doc.tags?.filter((t) => t && typeof t === 'string') || ['untagged']; functions.push(doc); @@ -39,11 +43,12 @@ const availableFunctions = (() => { })(); const tagCounts = {}; +const ignoredTags = ['supradough', 'superdirt']; // const tagOptions = { all: `all (${availableFunctions.length})` }; const tagOptions = { all: `all` }; for (const doc of availableFunctions) { (doc.tags || ['untagged']).forEach((t) => { - if (typeof t === 'string' && t) { + if (typeof t === 'string' && t && !ignoredTags.includes(t)) { tagCounts[t] = (tagCounts[t] || 0) + 1; //tagOptions[t] = `${t} (${tagCounts[t]})`; tagOptions[t] = t; @@ -204,7 +209,7 @@ export const Reference = memo(function Reference() { {entry.tags && ( - {entry.tags.join(', ')} + {entry.tags.filter((t) => !ignoredTags.includes(t)).join(', ')} )} From 4051fd5241295d8e9f43a07b7e5a8c6f91cb16cd Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 23 Jan 2026 23:52:59 +0100 Subject: [PATCH 10/12] oh snap --- test/__snapshots__/examples.test.mjs.snap | 100 ++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 4d2675308..5c7b5cb54 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1037,6 +1037,43 @@ exports[`runs examples > example "anchor" example index 0 1`] = ` ] `; +exports[`runs examples > example "anchor" example index 1 1`] = ` +[ + "[ 0/1 → 1/8 | anchor:c4 note:60 ]", + "[ 1/8 → 1/4 | anchor:c4 note:62 ]", + "[ 1/4 → 3/8 | anchor:c4 note:64 ]", + "[ 3/8 → 1/2 | anchor:c4 note:65 ]", + "[ 1/2 → 5/8 | anchor:c4 note:67 ]", + "[ 5/8 → 3/4 | anchor:c4 note:69 ]", + "[ 3/4 → 7/8 | anchor:c4 note:71 ]", + "[ 7/8 → 1/1 | anchor:c4 note:72 ]", + "[ 1/1 → 9/8 | anchor:g4 note:67 ]", + "[ 9/8 → 5/4 | anchor:g4 note:68 ]", + "[ 5/4 → 11/8 | anchor:g4 note:70 ]", + "[ 11/8 → 3/2 | anchor:g4 note:72 ]", + "[ 3/2 → 13/8 | anchor:g4 note:73 ]", + "[ 13/8 → 7/4 | anchor:g4 note:75 ]", + "[ 7/4 → 15/8 | anchor:g4 note:77 ]", + "[ 15/8 → 2/1 | anchor:g4 note:79 ]", + "[ 2/1 → 17/8 | anchor:c5 note:72 ]", + "[ 17/8 → 9/4 | anchor:c5 note:74 ]", + "[ 9/4 → 19/8 | anchor:c5 note:76 ]", + "[ 19/8 → 5/2 | anchor:c5 note:77 ]", + "[ 5/2 → 21/8 | anchor:c5 note:79 ]", + "[ 21/8 → 11/4 | anchor:c5 note:81 ]", + "[ 11/4 → 23/8 | anchor:c5 note:83 ]", + "[ 23/8 → 3/1 | anchor:c5 note:84 ]", + "[ 3/1 → 25/8 | anchor:g5 note:79 ]", + "[ 25/8 → 13/4 | anchor:g5 note:80 ]", + "[ 13/4 → 27/8 | anchor:g5 note:82 ]", + "[ 27/8 → 7/2 | anchor:g5 note:84 ]", + "[ 7/2 → 29/8 | anchor:g5 note:85 ]", + "[ 29/8 → 15/4 | anchor:g5 note:87 ]", + "[ 15/4 → 31/8 | anchor:g5 note:89 ]", + "[ 31/8 → 4/1 | anchor:g5 note:91 ]", +] +`; + exports[`runs examples > example "apply" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:C3 ]", @@ -4193,6 +4230,27 @@ exports[`runs examples > example "extend" example index 0 1`] = ` ] `; +exports[`runs examples > example "fadeTime" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:oh end:0.1 fadeTime:0 ]", + "[ 1/4 → 1/2 | s:oh end:0.1 fadeTime:0 ]", + "[ 1/2 → 3/4 | s:oh end:0.1 fadeTime:0 ]", + "[ 3/4 → 1/1 | s:oh end:0.1 fadeTime:0 ]", + "[ 1/1 → 5/4 | s:oh end:0.1 fadeTime:0.2 ]", + "[ 5/4 → 3/2 | s:oh end:0.1 fadeTime:0.2 ]", + "[ 3/2 → 7/4 | s:oh end:0.1 fadeTime:0.2 ]", + "[ 7/4 → 2/1 | s:oh end:0.1 fadeTime:0.2 ]", + "[ 2/1 → 9/4 | s:oh end:0.1 fadeTime:0.4 ]", + "[ 9/4 → 5/2 | s:oh end:0.1 fadeTime:0.4 ]", + "[ 5/2 → 11/4 | s:oh end:0.1 fadeTime:0.4 ]", + "[ 11/4 → 3/1 | s:oh end:0.1 fadeTime:0.4 ]", + "[ 3/1 → 13/4 | s:oh end:0.1 fadeTime:0.8 ]", + "[ 13/4 → 7/2 | s:oh end:0.1 fadeTime:0.8 ]", + "[ 7/2 → 15/4 | s:oh end:0.1 fadeTime:0.8 ]", + "[ 15/4 → 4/1 | s:oh end:0.1 fadeTime:0.8 ]", +] +`; + exports[`runs examples > example "fanchor" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:f s:sawtooth cutoff:1000 lpenv:8 fanchor:0 ]", @@ -8099,6 +8157,48 @@ exports[`runs examples > example "panchor" example index 0 1`] = ` ] `; +exports[`runs examples > example "panspan" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd pan:0.5 panspan:0 ]", + "[ 1/4 → 1/2 | s:hh pan:0.5 panspan:0 ]", + "[ 1/2 → 3/4 | s:bd pan:0.5 panspan:0 ]", + "[ 3/4 → 1/1 | s:hh pan:0.5 panspan:0 ]", + "[ 1/1 → 5/4 | s:bd pan:1 panspan:0.5 ]", + "[ 5/4 → 3/2 | s:hh pan:1 panspan:0.5 ]", + "[ 3/2 → 7/4 | s:bd pan:1 panspan:0.5 ]", + "[ 7/4 → 2/1 | s:hh pan:1 panspan:0.5 ]", + "[ 2/1 → 9/4 | s:bd pan:0.5 panspan:1 ]", + "[ 9/4 → 5/2 | s:hh pan:0.5 panspan:1 ]", + "[ 5/2 → 11/4 | s:bd pan:0.5 panspan:1 ]", + "[ 11/4 → 3/1 | s:hh pan:0.5 panspan:1 ]", + "[ 3/1 → 13/4 | s:bd pan:0 panspan:0 ]", + "[ 13/4 → 7/2 | s:hh pan:0 panspan:0 ]", + "[ 7/2 → 15/4 | s:bd pan:0 panspan:0 ]", + "[ 15/4 → 4/1 | s:hh pan:0 panspan:0 ]", +] +`; + +exports[`runs examples > example "pansplay" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd pan:0.5 pansplay:0 ]", + "[ 1/4 → 1/2 | s:hh pan:0.5 pansplay:0 ]", + "[ 1/2 → 3/4 | s:bd pan:0.5 pansplay:0 ]", + "[ 3/4 → 1/1 | s:hh pan:0.5 pansplay:0 ]", + "[ 1/1 → 5/4 | s:bd pan:1 pansplay:0.5 ]", + "[ 5/4 → 3/2 | s:hh pan:1 pansplay:0.5 ]", + "[ 3/2 → 7/4 | s:bd pan:1 pansplay:0.5 ]", + "[ 7/4 → 2/1 | s:hh pan:1 pansplay:0.5 ]", + "[ 2/1 → 9/4 | s:bd pan:0.5 pansplay:1 ]", + "[ 9/4 → 5/2 | s:hh pan:0.5 pansplay:1 ]", + "[ 5/2 → 11/4 | s:bd pan:0.5 pansplay:1 ]", + "[ 11/4 → 3/1 | s:hh pan:0.5 pansplay:1 ]", + "[ 3/1 → 13/4 | s:bd pan:0 pansplay:0 ]", + "[ 13/4 → 7/2 | s:hh pan:0 pansplay:0 ]", + "[ 7/2 → 15/4 | s:bd pan:0 pansplay:0 ]", + "[ 15/4 → 4/1 | s:hh pan:0 pansplay:0 ]", +] +`; + exports[`runs examples > example "partials" example index 0 1`] = ` [ "[ 0/1 → 1/16 | s:user note:A3 partials:[1 0 1 0 0 1] ]", From a9975a2b4159ff9cc3b8ea3029203c8f8e303b48 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 24 Jan 2026 02:01:36 +0100 Subject: [PATCH 11/12] reference: show synonyms in the same row, grayed out, instead of adding a row for each synonym --- .../src/repl/components/panel/Reference.jsx | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index d31c5c00b..5335a77e8 100644 --- a/website/src/repl/components/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -21,23 +21,18 @@ const availableFunctions = (() => { // @something is. We only want data from comments like `@tags superdough` here. // If nothing is specified, we default to "untagged" for debugging doc.tags = doc.tags?.filter((t) => t && typeof t === 'string') || ['untagged']; - functions.push(doc); const synonyms = doc.synonyms || []; + let names = [doc.name]; seen.add(doc.name); for (const s of synonyms) { if (!s || seen.has(s)) continue; + names.push(s); seen.add(s); - // Swap `doc.name` in for `s` in the list of synonyms - const synonymsWithDoc = [doc.name, ...synonyms].filter((x) => x && x !== s); - functions.push({ - ...doc, - name: s, // update names for the synonym - longname: s, - synonyms: synonymsWithDoc, - synonyms_text: synonymsWithDoc.join(', '), - }); } + doc.allNames = names.join(' '); + doc.synonyms = names.slice(1); + functions.push(doc); } return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name)); })(); @@ -87,7 +82,7 @@ export const Reference = memo(function Reference() { } const lowerCaseSearch = search.toLowerCase(); return ( - entry.name.toLowerCase().includes(lowerCaseSearch) || + (entry.allNames || entry.name).toLowerCase().includes(lowerCaseSearch) || (entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false) ); }); @@ -156,7 +151,7 @@ export const Reference = memo(function Reference() { { @@ -167,7 +162,7 @@ export const Reference = memo(function Reference() { } }} > - {entry.name} + {entry.name} {entry.synonyms && {entry.synonyms?.join(', ')}} {' '} ))} From 60345db1bdb3b0ba2977265faf9160ac93b789a0 Mon Sep 17 00:00:00 2001 From: robojumper Date: Sat, 17 Jan 2026 14:09:56 +0100 Subject: [PATCH 12/12] Fix pickF / pickmodF with object lookup --- packages/core/pick.mjs | 19 +++++++++------- test/__snapshots__/examples.test.mjs.snap | 27 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/core/pick.mjs b/packages/core/pick.mjs index 93b8acd4f..3974c695b 100644 --- a/packages/core/pick.mjs +++ b/packages/core/pick.mjs @@ -71,29 +71,32 @@ export const pickmod = register('pickmod', function (lookup, pat) { /** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern. * @tags combiners, functional * @param {Pattern} pat - * @param {Pattern} lookup a pattern of indices - * @param {function[]} funcs the array of functions from which to pull + * @param {Pattern} lookup a pattern of indices or names + * @param {function[] | object} lookup the array or lookup object of functions from which to pull * @returns {Pattern} * @example * s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)]) * @example * note("(3,8)").s("square") * .pickF("<0 2> 1", [jux(rev), fast(2), x=>x.lpf(800)]) + * @example + * note("(3,8)").s("square") + * .pickF(" f", { jr:jux(rev), f:fast(2), l:x=>x.lpf(800) }) */ -export const pickF = register('pickF', function (lookup, funcs, pat) { - return pat.apply(pick(lookup, funcs)); +export const pickF = register('pickF', function (pickPattern, lookup, pat) { + return pat.apply(pick(lookup, pickPattern)); }); /** * The same as `pickF`, but if you pick a number greater than the size of the functions list, * it wraps around, rather than sticking at the maximum value. * @tags combiners * @param {Pattern} pat - * @param {Pattern} lookup a pattern of indices - * @param {function[]} funcs the array of functions from which to pull + * @param {Pattern} lookup a pattern of indices or names + * @param {function[] | object} lookup the array or lookup object of functions from which to pull * @returns {Pattern} */ -export const pickmodF = register('pickmodF', function (lookup, funcs, pat) { - return pat.apply(pickmod(lookup, funcs)); +export const pickmodF = register('pickmodF', function (pickPattern, lookup, pat) { + return pat.apply(pickmod(lookup, pickPattern)); }); /** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin. diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 5c7b5cb54..b205e4e5f 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -8823,6 +8823,33 @@ exports[`runs examples > example "pickF" example index 1 1`] = ` ] `; +exports[`runs examples > example "pickF" example index 2 1`] = ` +[ + "[ 0/1 → 1/8 | note:c2 s:square pan:0 ]", + "[ 1/8 → 1/4 | note:c2 s:square pan:1 ]", + "[ 3/8 → 1/2 | note:c2 s:square pan:0 ]", + "[ 1/2 → 9/16 | note:d2 s:square ]", + "[ 11/16 → 3/4 | note:d2 s:square ]", + "[ 7/8 → 15/16 | note:d2 s:square ]", + "[ 1/1 → 9/8 | note:d2 s:square cutoff:800 ]", + "[ 11/8 → 3/2 | note:d2 s:square cutoff:800 ]", + "[ 3/2 → 25/16 | note:d2 s:square ]", + "[ 27/16 → 7/4 | note:d2 s:square ]", + "[ 15/8 → 31/16 | note:d2 s:square ]", + "[ 2/1 → 17/8 | note:c2 s:square pan:0 ]", + "[ 17/8 → 9/4 | note:c2 s:square pan:1 ]", + "[ 19/8 → 5/2 | note:c2 s:square pan:0 ]", + "[ 5/2 → 41/16 | note:d2 s:square ]", + "[ 43/16 → 11/4 | note:d2 s:square ]", + "[ 23/8 → 47/16 | note:d2 s:square ]", + "[ 3/1 → 25/8 | note:d2 s:square cutoff:800 ]", + "[ 27/8 → 7/2 | note:d2 s:square cutoff:800 ]", + "[ 7/2 → 57/16 | note:d2 s:square ]", + "[ 59/16 → 15/4 | note:d2 s:square ]", + "[ 31/8 → 63/16 | note:d2 s:square ]", +] +`; + exports[`runs examples > example "pickmodRestart" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C3 s:piano ]",