From f5ae50c3cee2dd4b53476518b2765d8c58ddcaa6 Mon Sep 17 00:00:00 2001 From: JoStro Date: Tue, 1 Jul 2025 22:02:54 +0100 Subject: [PATCH 01/41] Modify `extend` to better match behavior of `!` operator --- packages/core/pattern.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index cbaf8a8a2..abe613d19 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2928,7 +2928,7 @@ export const drop = stepRegister('drop', function (i, pat) { * ).pace(8) */ export const extend = stepRegister('extend', function (factor, pat) { - return pat.fast(factor).expand(factor); + return pat.repeatCycles(factor).fast(factor).expand(factor); }); /** From 7ff7e8852bd41b1eb524da84fd73840dcd7cff1c Mon Sep 17 00:00:00 2001 From: drdozer Date: Thu, 11 Sep 2025 18:42:23 +0100 Subject: [PATCH 02/41] feat: implement context-aware autocomplete Adds context-sensitive autocompletion for: - Sound names in s("...") and sound("...") after the opening quote or after whitespace/bracket - Bank names in bank("...") with quote handling and live filtering - Scale types in .scale("...") after the colon, inserting colon-form for multi-word names - Pitch names (C, C#, Db, etc.) for scale keys on explicit completion (Ctrl+Space) before the colon - Blocks fallback completions in scale context before colon to avoid function list cross-talk --- packages/codemirror/autocomplete.mjs | 166 +++++++++++++++++++++++++-- 1 file changed, 155 insertions(+), 11 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 59ca8adf2..c1c7bb14e 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -1,6 +1,11 @@ + import jsdoc from '../../doc.json'; import { autocompletion } from '@codemirror/autocomplete'; import { h } from './html'; +import { Scale } from '@tonaljs/tonal'; +import { soundMap } from 'superdough'; + + const escapeHtml = (str) => { const div = document.createElement('div'); @@ -74,6 +79,31 @@ const isValidDoc = (doc) => { const hasExcludedTags = (doc) => ['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)); + + +export function bankCompletions() { + const soundDict = soundMap.get(); + const banks = new Set(); + for (const key of Object.keys(soundDict)) { + const [bank, suffix] = key.split('_'); + if (suffix && bank) banks.add(bank); + } + return Array.from(banks).sort().map((name) => ({ label: name, type: 'bank' })); +} + +// Placeholder: Replace with actual logic to get live sound names +const soundCompletions = [ + // Example: { label: 'clap', type: 'sound' }, ... +]; + +// Attempt to get all scale names from Tonal +let scaleCompletions = []; +try { + scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' })); +} catch (e) { + console.warn('[autocomplete] Could not load scale names from Tonal:', e); +} + const jsdocCompletions = jsdoc.docs .filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc)) // https://codemirror.net/docs/ref/#autocomplete.Completion @@ -85,18 +115,132 @@ const jsdocCompletions = jsdoc.docs })); export const strudelAutocomplete = (context) => { - const word = context.matchBefore(/\w*/); - if (word.from === word.to && !context.explicit) return null; + // --- Pitch names for scale key completion --- + // Ideally, these should be imported from packages/tonal/tonleiter.mjs + // but are duplicated here as they are not exported. + const pitchNames = [ + 'C', 'C#', 'Db', 'D', 'D#', 'Eb', 'E', 'Fb', 'F', 'F#', 'Gb', + 'G', 'G#', 'Ab', 'A', 'A#', 'Bb', 'B', 'Cb' + ]; + + // Block fallback completions inside .scale("..."), even before the colon + let scalePreColonContext = context.matchBefore(/\.scale\(\s*['"][^'"]*$/); + if (scalePreColonContext) { + // Only yield completions if a colon is present (handled below) + if (!scalePreColonContext.text.includes(':')) { + if (context.explicit) { + // Provide pitch completions on explicit request + // (Union of sharps and flats from tonleiter.mjs) + const text = scalePreColonContext.text; + const match = text.match(/([A-Ga-g][#b]*)?$/); + const fragment = match ? match[0] : ''; + const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase())); + const from = scalePreColonContext.to - fragment.length; + const options = filtered.map((p) => ({ label: p, type: 'pitch' })); + return { from, options }; + } else { + // Block fallback completions + return { from: scalePreColonContext.to, options: [] }; + } + } - return { - from: word.from, - options: jsdocCompletions, - /* options: [ - { label: 'match', type: 'keyword' }, - { label: 'hello', type: 'variable', info: '(World)' }, - { label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' }, - ], */ - }; + // (second block removed, handled above) + if (!scalePreColonContext.text.includes(':')) { + return { from: scalePreColonContext.to, options: [] }; + } + } + + // 1. Check for sound context: s("..."), sound("...") + // Trigger after at least one letter is typed after whitespace or bracket inside the quotes + let soundContext = context.matchBefore(/(s|sound)\(\s*['"][^'"]*$/); + if (soundContext) { + const text = soundContext.text; + const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'")); + if (quoteIdx === -1) return null; + const inside = text.slice(quoteIdx + 1); + const fragMatch = inside.match(/(?:[\s\[\{\(<])([\w]*)$/); + const fragment = fragMatch ? fragMatch[1] : inside; + if (!fragment || fragment.length === 0) return null; + const soundNames = Object.keys(soundMap.get()).sort(); + const filteredSounds = soundNames.filter((name) => name.startsWith(fragment)); + let options = filteredSounds.map((name) => ({ label: name, type: 'sound' })); + const from = soundContext.to - fragment.length; + return { + from, + options, + }; + } + + let bankMatch = context.matchBefore(/bank\(\s*(['"])?([\w]*)$/); + if (bankMatch) { + let banks = bankCompletions(); + console.log('[autocomplete] Bank context detected:', bankMatch.text); + console.log('[autocomplete] soundMap keys:', Object.keys(soundMap.get())); + console.log('[autocomplete] bankCompletions:', banks); + // Extract quote and fragment using regex groups on match.text + const groups = bankMatch.text.match(/(['"])?([\w]*)$/); + const quote = groups ? groups[1] : undefined; + const fragment = groups ? groups[2] || '' : ''; + let from; + if (quote) { + from = bankMatch.from + bankMatch.text.indexOf(quote) + 1; + } else { + from = bankMatch.to - fragment.length; + } + const filteredBanks = banks.filter((b) => b.label.startsWith(fragment)); + let options; + if (!quote) { + options = filteredBanks.map((b) => ({ ...b, apply: '"' + b.label + '"' })); + } else { + const afterCursor = context.state.sliceDoc(bankMatch.to, bankMatch.to + 1); + options = filteredBanks.map((b) => { + if (afterCursor !== quote) { + return { ...b, apply: b.label + quote }; + } + return b; + }); + } + return { + from, + options, + }; + } + + // 3. Check for scale context: .scale("..."), only after colon + let scaleContext = context.matchBefore(/\.scale\(\s*['"][^'"]*:[^'"]*$/); + if (scaleContext) { + // Find the last colon in the text + const text = scaleContext.text; + const colonIdx = text.lastIndexOf(':'); + if (colonIdx === -1) return null; + // Get the fragment after the colon + const fragment = text.slice(colonIdx + 1); + // Filter scale completions by fragment + const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment)); + // Insert colon-form (replace spaces with colons) for completions + const options = filteredScales.map((s) => ({ + ...s, + apply: s.label.replace(/\s+/g, ':') + })); + const from = scaleContext.from + colonIdx + 1; + return { + from, + options, + }; + } + + // fallback: original logic + const word = context.matchBefore(/\w*/); + if (word && word.from === word.to && !context.explicit) return null; + + if (word) { + console.log('[autocomplete] Default context:', word.text); + return { + from: word.from, + options: jsdocCompletions, + }; + } + return null; }; export const isAutoCompletionEnabled = (enabled) => From 085e839bfe3f0e4f9404afecba943b70fe106c0f Mon Sep 17 00:00:00 2001 From: drdozer Date: Thu, 11 Sep 2025 18:59:26 +0100 Subject: [PATCH 03/41] refactor: modularize autocomplete context handlers using strategy array --- packages/codemirror/autocomplete.mjs | 68 ++++++++++++++++------------ 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index c1c7bb14e..591552cc7 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -114,23 +114,18 @@ const jsdocCompletions = jsdoc.docs type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type })); -export const strudelAutocomplete = (context) => { - // --- Pitch names for scale key completion --- - // Ideally, these should be imported from packages/tonal/tonleiter.mjs - // but are duplicated here as they are not exported. - const pitchNames = [ - 'C', 'C#', 'Db', 'D', 'D#', 'Eb', 'E', 'Fb', 'F', 'F#', 'Gb', - 'G', 'G#', 'Ab', 'A', 'A#', 'Bb', 'B', 'Cb' - ]; - - // Block fallback completions inside .scale("..."), even before the colon + +// --- Handler functions for each context --- +const pitchNames = [ + 'C', 'C#', 'Db', 'D', 'D#', 'Eb', 'E', 'Fb', 'F', 'F#', 'Gb', + 'G', 'G#', 'Ab', 'A', 'A#', 'Bb', 'B', 'Cb' +]; + +function scalePreColonHandler(context) { let scalePreColonContext = context.matchBefore(/\.scale\(\s*['"][^'"]*$/); if (scalePreColonContext) { - // Only yield completions if a colon is present (handled below) if (!scalePreColonContext.text.includes(':')) { if (context.explicit) { - // Provide pitch completions on explicit request - // (Union of sharps and flats from tonleiter.mjs) const text = scalePreColonContext.text; const match = text.match(/([A-Ga-g][#b]*)?$/); const fragment = match ? match[0] : ''; @@ -139,19 +134,17 @@ export const strudelAutocomplete = (context) => { const options = filtered.map((p) => ({ label: p, type: 'pitch' })); return { from, options }; } else { - // Block fallback completions return { from: scalePreColonContext.to, options: [] }; } } - - // (second block removed, handled above) if (!scalePreColonContext.text.includes(':')) { return { from: scalePreColonContext.to, options: [] }; } } - - // 1. Check for sound context: s("..."), sound("...") - // Trigger after at least one letter is typed after whitespace or bracket inside the quotes + return null; +} + +function soundHandler(context) { let soundContext = context.matchBefore(/(s|sound)\(\s*['"][^'"]*$/); if (soundContext) { const text = soundContext.text; @@ -170,13 +163,13 @@ export const strudelAutocomplete = (context) => { options, }; } + return null; +} +function bankHandler(context) { let bankMatch = context.matchBefore(/bank\(\s*(['"])?([\w]*)$/); if (bankMatch) { let banks = bankCompletions(); - console.log('[autocomplete] Bank context detected:', bankMatch.text); - console.log('[autocomplete] soundMap keys:', Object.keys(soundMap.get())); - console.log('[autocomplete] bankCompletions:', banks); // Extract quote and fragment using regex groups on match.text const groups = bankMatch.text.match(/(['"])?([\w]*)$/); const quote = groups ? groups[1] : undefined; @@ -205,19 +198,17 @@ export const strudelAutocomplete = (context) => { options, }; } + return null; +} - // 3. Check for scale context: .scale("..."), only after colon +function scaleAfterColonHandler(context) { let scaleContext = context.matchBefore(/\.scale\(\s*['"][^'"]*:[^'"]*$/); if (scaleContext) { - // Find the last colon in the text const text = scaleContext.text; const colonIdx = text.lastIndexOf(':'); if (colonIdx === -1) return null; - // Get the fragment after the colon const fragment = text.slice(colonIdx + 1); - // Filter scale completions by fragment const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment)); - // Insert colon-form (replace spaces with colons) for completions const options = filteredScales.map((s) => ({ ...s, apply: s.label.replace(/\s+/g, ':') @@ -228,19 +219,36 @@ export const strudelAutocomplete = (context) => { options, }; } + return null; +} - // fallback: original logic +function fallbackHandler(context) { const word = context.matchBefore(/\w*/); if (word && word.from === word.to && !context.explicit) return null; - if (word) { - console.log('[autocomplete] Default context:', word.text); return { from: word.from, options: jsdocCompletions, }; } return null; +} + +const handlers = [ + scalePreColonHandler, + soundHandler, + bankHandler, + scaleAfterColonHandler, + // this handler *must* be last + fallbackHandler +]; + +export const strudelAutocomplete = (context) => { + for (const handler of handlers) { + const result = handler(context); + if (result) return result; + } + return null; }; export const isAutoCompletionEnabled = (enabled) => From 80fe5b81401741175411692f00604cb16dfc4cb6 Mon Sep 17 00:00:00 2001 From: drdozer Date: Thu, 11 Sep 2025 18:59:50 +0100 Subject: [PATCH 04/41] refactor: modularize autocomplete context handlers using strategy array From 9200a67a600471309162dabeeb8bce915f915c55 Mon Sep 17 00:00:00 2001 From: drdozer Date: Thu, 11 Sep 2025 19:00:05 +0100 Subject: [PATCH 05/41] feat: add E# and B# to pitchNames for complete enharmonic coverage --- packages/codemirror/autocomplete.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 591552cc7..5d6c7233d 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -117,8 +117,8 @@ const jsdocCompletions = jsdoc.docs // --- Handler functions for each context --- const pitchNames = [ - 'C', 'C#', 'Db', 'D', 'D#', 'Eb', 'E', 'Fb', 'F', 'F#', 'Gb', - 'G', 'G#', 'Ab', 'A', 'A#', 'Bb', 'B', 'Cb' + 'C', 'C#', 'Db', 'D', 'D#', 'Eb', 'E', 'E#', 'Fb', 'F', 'F#', 'Gb', + 'G', 'G#', 'Ab', 'A', 'A#', 'Bb', 'B', 'B#', 'Cb' ]; function scalePreColonHandler(context) { From 2a00fcdf5d70a2b0789b11376cf71b9c60cdc3cf Mon Sep 17 00:00:00 2001 From: drdozer Date: Thu, 11 Sep 2025 19:01:22 +0100 Subject: [PATCH 06/41] Updated the codemirror packages to be able to access additional imports. --- packages/codemirror/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 803b33f3d..bb1059f11 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -47,6 +47,8 @@ "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", "@strudel/transpiler": "workspace:*", + "@tonaljs/tonal": "^4.10.0", + "superdough": "workspace:*", "nanostores": "^0.11.3" }, "devDependencies": { From 41d6f1b5da98e3929ae71d2e2adf5ea00768664b Mon Sep 17 00:00:00 2001 From: drdozer Date: Thu, 11 Sep 2025 21:37:35 +0100 Subject: [PATCH 07/41] feat: sound autocomplete matches anywhere in name using includes() --- packages/codemirror/autocomplete.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 5d6c7233d..110a9165a 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -155,7 +155,7 @@ function soundHandler(context) { const fragment = fragMatch ? fragMatch[1] : inside; if (!fragment || fragment.length === 0) return null; const soundNames = Object.keys(soundMap.get()).sort(); - const filteredSounds = soundNames.filter((name) => name.startsWith(fragment)); + const filteredSounds = soundNames.filter((name) => name.includes(fragment)); let options = filteredSounds.map((name) => ({ label: name, type: 'sound' })); const from = soundContext.to - fragment.length; return { From 7676641c9ef8a631a47fc7ef0d6877442eb5c509 Mon Sep 17 00:00:00 2001 From: drdozer Date: Fri, 12 Sep 2025 12:11:36 +0100 Subject: [PATCH 08/41] Commiting lockfile. --- pnpm-lock.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4a24f61c..4cf7ea602 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -215,9 +215,15 @@ importers: '@strudel/transpiler': specifier: workspace:* version: link:../transpiler + '@tonaljs/tonal': + specifier: ^4.10.0 + version: 4.10.0 nanostores: specifier: ^0.11.3 version: 0.11.3 + superdough: + specifier: workspace:* + version: link:../superdough devDependencies: vite: specifier: ^6.0.11 From d87ce960d218f00c8adfd2c3cfd1658e39eaa8b3 Mon Sep 17 00:00:00 2001 From: drdozer Date: Thu, 11 Sep 2025 18:42:23 +0100 Subject: [PATCH 09/41] feat: implement context-aware autocomplete Adds context-sensitive autocompletion for: - Sound names in s("...") and sound("...") after the opening quote or after whitespace/bracket - Bank names in bank("...") with quote handling and live filtering - Scale types in .scale("...") after the colon, inserting colon-form for multi-word names - Pitch names (C, C#, Db, etc.) for scale keys on explicit completion (Ctrl+Space) before the colon - Blocks fallback completions in scale context before colon to avoid function list cross-talk --- packages/codemirror/autocomplete.mjs | 166 +++++++++++++++++++++++++-- 1 file changed, 155 insertions(+), 11 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 59ca8adf2..c1c7bb14e 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -1,6 +1,11 @@ + import jsdoc from '../../doc.json'; import { autocompletion } from '@codemirror/autocomplete'; import { h } from './html'; +import { Scale } from '@tonaljs/tonal'; +import { soundMap } from 'superdough'; + + const escapeHtml = (str) => { const div = document.createElement('div'); @@ -74,6 +79,31 @@ const isValidDoc = (doc) => { const hasExcludedTags = (doc) => ['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)); + + +export function bankCompletions() { + const soundDict = soundMap.get(); + const banks = new Set(); + for (const key of Object.keys(soundDict)) { + const [bank, suffix] = key.split('_'); + if (suffix && bank) banks.add(bank); + } + return Array.from(banks).sort().map((name) => ({ label: name, type: 'bank' })); +} + +// Placeholder: Replace with actual logic to get live sound names +const soundCompletions = [ + // Example: { label: 'clap', type: 'sound' }, ... +]; + +// Attempt to get all scale names from Tonal +let scaleCompletions = []; +try { + scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' })); +} catch (e) { + console.warn('[autocomplete] Could not load scale names from Tonal:', e); +} + const jsdocCompletions = jsdoc.docs .filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc)) // https://codemirror.net/docs/ref/#autocomplete.Completion @@ -85,18 +115,132 @@ const jsdocCompletions = jsdoc.docs })); export const strudelAutocomplete = (context) => { - const word = context.matchBefore(/\w*/); - if (word.from === word.to && !context.explicit) return null; + // --- Pitch names for scale key completion --- + // Ideally, these should be imported from packages/tonal/tonleiter.mjs + // but are duplicated here as they are not exported. + const pitchNames = [ + 'C', 'C#', 'Db', 'D', 'D#', 'Eb', 'E', 'Fb', 'F', 'F#', 'Gb', + 'G', 'G#', 'Ab', 'A', 'A#', 'Bb', 'B', 'Cb' + ]; + + // Block fallback completions inside .scale("..."), even before the colon + let scalePreColonContext = context.matchBefore(/\.scale\(\s*['"][^'"]*$/); + if (scalePreColonContext) { + // Only yield completions if a colon is present (handled below) + if (!scalePreColonContext.text.includes(':')) { + if (context.explicit) { + // Provide pitch completions on explicit request + // (Union of sharps and flats from tonleiter.mjs) + const text = scalePreColonContext.text; + const match = text.match(/([A-Ga-g][#b]*)?$/); + const fragment = match ? match[0] : ''; + const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase())); + const from = scalePreColonContext.to - fragment.length; + const options = filtered.map((p) => ({ label: p, type: 'pitch' })); + return { from, options }; + } else { + // Block fallback completions + return { from: scalePreColonContext.to, options: [] }; + } + } - return { - from: word.from, - options: jsdocCompletions, - /* options: [ - { label: 'match', type: 'keyword' }, - { label: 'hello', type: 'variable', info: '(World)' }, - { label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' }, - ], */ - }; + // (second block removed, handled above) + if (!scalePreColonContext.text.includes(':')) { + return { from: scalePreColonContext.to, options: [] }; + } + } + + // 1. Check for sound context: s("..."), sound("...") + // Trigger after at least one letter is typed after whitespace or bracket inside the quotes + let soundContext = context.matchBefore(/(s|sound)\(\s*['"][^'"]*$/); + if (soundContext) { + const text = soundContext.text; + const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'")); + if (quoteIdx === -1) return null; + const inside = text.slice(quoteIdx + 1); + const fragMatch = inside.match(/(?:[\s\[\{\(<])([\w]*)$/); + const fragment = fragMatch ? fragMatch[1] : inside; + if (!fragment || fragment.length === 0) return null; + const soundNames = Object.keys(soundMap.get()).sort(); + const filteredSounds = soundNames.filter((name) => name.startsWith(fragment)); + let options = filteredSounds.map((name) => ({ label: name, type: 'sound' })); + const from = soundContext.to - fragment.length; + return { + from, + options, + }; + } + + let bankMatch = context.matchBefore(/bank\(\s*(['"])?([\w]*)$/); + if (bankMatch) { + let banks = bankCompletions(); + console.log('[autocomplete] Bank context detected:', bankMatch.text); + console.log('[autocomplete] soundMap keys:', Object.keys(soundMap.get())); + console.log('[autocomplete] bankCompletions:', banks); + // Extract quote and fragment using regex groups on match.text + const groups = bankMatch.text.match(/(['"])?([\w]*)$/); + const quote = groups ? groups[1] : undefined; + const fragment = groups ? groups[2] || '' : ''; + let from; + if (quote) { + from = bankMatch.from + bankMatch.text.indexOf(quote) + 1; + } else { + from = bankMatch.to - fragment.length; + } + const filteredBanks = banks.filter((b) => b.label.startsWith(fragment)); + let options; + if (!quote) { + options = filteredBanks.map((b) => ({ ...b, apply: '"' + b.label + '"' })); + } else { + const afterCursor = context.state.sliceDoc(bankMatch.to, bankMatch.to + 1); + options = filteredBanks.map((b) => { + if (afterCursor !== quote) { + return { ...b, apply: b.label + quote }; + } + return b; + }); + } + return { + from, + options, + }; + } + + // 3. Check for scale context: .scale("..."), only after colon + let scaleContext = context.matchBefore(/\.scale\(\s*['"][^'"]*:[^'"]*$/); + if (scaleContext) { + // Find the last colon in the text + const text = scaleContext.text; + const colonIdx = text.lastIndexOf(':'); + if (colonIdx === -1) return null; + // Get the fragment after the colon + const fragment = text.slice(colonIdx + 1); + // Filter scale completions by fragment + const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment)); + // Insert colon-form (replace spaces with colons) for completions + const options = filteredScales.map((s) => ({ + ...s, + apply: s.label.replace(/\s+/g, ':') + })); + const from = scaleContext.from + colonIdx + 1; + return { + from, + options, + }; + } + + // fallback: original logic + const word = context.matchBefore(/\w*/); + if (word && word.from === word.to && !context.explicit) return null; + + if (word) { + console.log('[autocomplete] Default context:', word.text); + return { + from: word.from, + options: jsdocCompletions, + }; + } + return null; }; export const isAutoCompletionEnabled = (enabled) => From 0b67473bd3a7de3df71982b4bdc50d98bc41d3d6 Mon Sep 17 00:00:00 2001 From: drdozer Date: Thu, 11 Sep 2025 18:59:26 +0100 Subject: [PATCH 10/41] refactor: modularize autocomplete context handlers using strategy array --- packages/codemirror/autocomplete.mjs | 68 ++++++++++++++++------------ 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index c1c7bb14e..591552cc7 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -114,23 +114,18 @@ const jsdocCompletions = jsdoc.docs type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type })); -export const strudelAutocomplete = (context) => { - // --- Pitch names for scale key completion --- - // Ideally, these should be imported from packages/tonal/tonleiter.mjs - // but are duplicated here as they are not exported. - const pitchNames = [ - 'C', 'C#', 'Db', 'D', 'D#', 'Eb', 'E', 'Fb', 'F', 'F#', 'Gb', - 'G', 'G#', 'Ab', 'A', 'A#', 'Bb', 'B', 'Cb' - ]; - - // Block fallback completions inside .scale("..."), even before the colon + +// --- Handler functions for each context --- +const pitchNames = [ + 'C', 'C#', 'Db', 'D', 'D#', 'Eb', 'E', 'Fb', 'F', 'F#', 'Gb', + 'G', 'G#', 'Ab', 'A', 'A#', 'Bb', 'B', 'Cb' +]; + +function scalePreColonHandler(context) { let scalePreColonContext = context.matchBefore(/\.scale\(\s*['"][^'"]*$/); if (scalePreColonContext) { - // Only yield completions if a colon is present (handled below) if (!scalePreColonContext.text.includes(':')) { if (context.explicit) { - // Provide pitch completions on explicit request - // (Union of sharps and flats from tonleiter.mjs) const text = scalePreColonContext.text; const match = text.match(/([A-Ga-g][#b]*)?$/); const fragment = match ? match[0] : ''; @@ -139,19 +134,17 @@ export const strudelAutocomplete = (context) => { const options = filtered.map((p) => ({ label: p, type: 'pitch' })); return { from, options }; } else { - // Block fallback completions return { from: scalePreColonContext.to, options: [] }; } } - - // (second block removed, handled above) if (!scalePreColonContext.text.includes(':')) { return { from: scalePreColonContext.to, options: [] }; } } - - // 1. Check for sound context: s("..."), sound("...") - // Trigger after at least one letter is typed after whitespace or bracket inside the quotes + return null; +} + +function soundHandler(context) { let soundContext = context.matchBefore(/(s|sound)\(\s*['"][^'"]*$/); if (soundContext) { const text = soundContext.text; @@ -170,13 +163,13 @@ export const strudelAutocomplete = (context) => { options, }; } + return null; +} +function bankHandler(context) { let bankMatch = context.matchBefore(/bank\(\s*(['"])?([\w]*)$/); if (bankMatch) { let banks = bankCompletions(); - console.log('[autocomplete] Bank context detected:', bankMatch.text); - console.log('[autocomplete] soundMap keys:', Object.keys(soundMap.get())); - console.log('[autocomplete] bankCompletions:', banks); // Extract quote and fragment using regex groups on match.text const groups = bankMatch.text.match(/(['"])?([\w]*)$/); const quote = groups ? groups[1] : undefined; @@ -205,19 +198,17 @@ export const strudelAutocomplete = (context) => { options, }; } + return null; +} - // 3. Check for scale context: .scale("..."), only after colon +function scaleAfterColonHandler(context) { let scaleContext = context.matchBefore(/\.scale\(\s*['"][^'"]*:[^'"]*$/); if (scaleContext) { - // Find the last colon in the text const text = scaleContext.text; const colonIdx = text.lastIndexOf(':'); if (colonIdx === -1) return null; - // Get the fragment after the colon const fragment = text.slice(colonIdx + 1); - // Filter scale completions by fragment const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment)); - // Insert colon-form (replace spaces with colons) for completions const options = filteredScales.map((s) => ({ ...s, apply: s.label.replace(/\s+/g, ':') @@ -228,19 +219,36 @@ export const strudelAutocomplete = (context) => { options, }; } + return null; +} - // fallback: original logic +function fallbackHandler(context) { const word = context.matchBefore(/\w*/); if (word && word.from === word.to && !context.explicit) return null; - if (word) { - console.log('[autocomplete] Default context:', word.text); return { from: word.from, options: jsdocCompletions, }; } return null; +} + +const handlers = [ + scalePreColonHandler, + soundHandler, + bankHandler, + scaleAfterColonHandler, + // this handler *must* be last + fallbackHandler +]; + +export const strudelAutocomplete = (context) => { + for (const handler of handlers) { + const result = handler(context); + if (result) return result; + } + return null; }; export const isAutoCompletionEnabled = (enabled) => From 6b6d4b86a6a09d24484013a2f23b38f7269d1436 Mon Sep 17 00:00:00 2001 From: drdozer Date: Thu, 11 Sep 2025 18:59:50 +0100 Subject: [PATCH 11/41] refactor: modularize autocomplete context handlers using strategy array From 90614612e24673bab8ea0dd4eef00381c47c1a10 Mon Sep 17 00:00:00 2001 From: drdozer Date: Thu, 11 Sep 2025 19:00:05 +0100 Subject: [PATCH 12/41] feat: add E# and B# to pitchNames for complete enharmonic coverage --- packages/codemirror/autocomplete.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 591552cc7..5d6c7233d 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -117,8 +117,8 @@ const jsdocCompletions = jsdoc.docs // --- Handler functions for each context --- const pitchNames = [ - 'C', 'C#', 'Db', 'D', 'D#', 'Eb', 'E', 'Fb', 'F', 'F#', 'Gb', - 'G', 'G#', 'Ab', 'A', 'A#', 'Bb', 'B', 'Cb' + 'C', 'C#', 'Db', 'D', 'D#', 'Eb', 'E', 'E#', 'Fb', 'F', 'F#', 'Gb', + 'G', 'G#', 'Ab', 'A', 'A#', 'Bb', 'B', 'B#', 'Cb' ]; function scalePreColonHandler(context) { From ed182d4b3f2e118d4d56df2a141b6200043020dd Mon Sep 17 00:00:00 2001 From: drdozer Date: Thu, 11 Sep 2025 19:01:22 +0100 Subject: [PATCH 13/41] Updated the codemirror packages to be able to access additional imports. --- packages/codemirror/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 803b33f3d..bb1059f11 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -47,6 +47,8 @@ "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", "@strudel/transpiler": "workspace:*", + "@tonaljs/tonal": "^4.10.0", + "superdough": "workspace:*", "nanostores": "^0.11.3" }, "devDependencies": { From cd659ac2a1eeccfc65ccd82d8a8c5a51129404c2 Mon Sep 17 00:00:00 2001 From: drdozer Date: Thu, 11 Sep 2025 21:37:35 +0100 Subject: [PATCH 14/41] feat: sound autocomplete matches anywhere in name using includes() --- packages/codemirror/autocomplete.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 5d6c7233d..110a9165a 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -155,7 +155,7 @@ function soundHandler(context) { const fragment = fragMatch ? fragMatch[1] : inside; if (!fragment || fragment.length === 0) return null; const soundNames = Object.keys(soundMap.get()).sort(); - const filteredSounds = soundNames.filter((name) => name.startsWith(fragment)); + const filteredSounds = soundNames.filter((name) => name.includes(fragment)); let options = filteredSounds.map((name) => ({ label: name, type: 'sound' })); const from = soundContext.to - fragment.length; return { From 39460eb1005529dbc2d81d2eec92c92cb0a9b364 Mon Sep 17 00:00:00 2001 From: drdozer Date: Fri, 12 Sep 2025 12:11:36 +0100 Subject: [PATCH 15/41] Commiting lockfile. --- pnpm-lock.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c225057f3..d67f42d28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -215,9 +215,15 @@ importers: '@strudel/transpiler': specifier: workspace:* version: link:../transpiler + '@tonaljs/tonal': + specifier: ^4.10.0 + version: 4.10.0 nanostores: specifier: ^0.11.3 version: 0.11.3 + superdough: + specifier: workspace:* + version: link:../superdough devDependencies: vite: specifier: ^6.0.11 From 1670aebad80a5dc398317e0f70d5a20b37d27049 Mon Sep 17 00:00:00 2001 From: drdozer Date: Sat, 13 Sep 2025 21:35:28 +0100 Subject: [PATCH 16/41] Updated the gitignore rules for vscode. --- .gitignore | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.gitignore b/.gitignore index 2be3ee698..7532adaf0 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,20 @@ tidal-drum-machines webaudiofontdata src-tauri/target + + +### BEGIN Visual Studio Code ### +# Blanket, recursive exclude for .vscode directory and files +.vscode/**/* +# Unexclude specific files and directories within .vscode +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +### END Visual Studio Code ### + + + # BEGIN JetBrains -> END JetBrains # for JetBrains IDE users, e.g. WebStorm. Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore From c8875b29c1ca1ff55d4bb74cc304a3a435e83112 Mon Sep 17 00:00:00 2001 From: drdozer Date: Mon, 15 Sep 2025 14:27:59 +0100 Subject: [PATCH 17/41] Improve autocompletion behavior - Refactored handlers to use consistent early return pattern for better readability - Added blocking behavior for unquoted contexts (sound(, scale(, bank()) to guide users toward correct syntax instead of showing irrelevant fallback completions - Fixed soundHandler to show all sounds when fragment is empty (e.g., sound("")) - Simplified bankHandler by removing complex quote handling logic in favor of consistent blocking pattern - Made scale handler regex patterns consistent (removed leading dots) - All handlers now follow same structure: block unquoted contexts, provide completions for quoted contexts --- packages/codemirror/autocomplete.mjs | 176 +++++++++++++++------------ 1 file changed, 99 insertions(+), 77 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 2d53fe613..b5fb2f1fe 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -1,12 +1,9 @@ - import jsdoc from '../../doc.json'; import { autocompletion } from '@codemirror/autocomplete'; import { h } from './html'; import { Scale } from '@tonaljs/tonal'; import { soundMap } from 'superdough'; - - const escapeHtml = (str) => { const div = document.createElement('div'); div.innerText = str; @@ -80,7 +77,6 @@ const isValidDoc = (doc) => { const hasExcludedTags = (doc) => ['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)); - export function bankCompletions() { const soundDict = soundMap.get(); const banks = new Set(); @@ -88,10 +84,11 @@ export function bankCompletions() { const [bank, suffix] = key.split('_'); if (suffix && bank) banks.add(bank); } - return Array.from(banks).sort().map((name) => ({ label: name, type: 'bank' })); + return Array.from(banks) + .sort() + .map((name) => ({ label: name, type: 'bank' })); } - // Attempt to get all scale names from Tonal let scaleCompletions = []; try { @@ -138,15 +135,43 @@ const jsdocCompletions = (() => { return completions; })(); - // --- Handler functions for each context --- const pitchNames = [ - 'C', 'C#', 'Db', 'D', 'D#', 'Eb', 'E', 'E#', 'Fb', 'F', 'F#', 'Gb', - 'G', 'G#', 'Ab', 'A', 'A#', 'Bb', 'B', 'B#', 'Cb' + 'C', + 'C#', + 'Db', + 'D', + 'D#', + 'Eb', + 'E', + 'E#', + 'Fb', + 'F', + 'F#', + 'Gb', + 'G', + 'G#', + 'Ab', + 'A', + 'A#', + 'Bb', + 'B', + 'B#', + 'Cb', ]; function scalePreColonHandler(context) { - let scalePreColonContext = context.matchBefore(/\.scale\(\s*['"][^'"]*$/); + // First check for scale context without quotes - block with empty completions + let scaleNoQuotesContext = context.matchBefore(/scale\(\s*$/); + if (scaleNoQuotesContext) { + return { + from: scaleNoQuotesContext.to, + options: [], + }; + } + + // Then check for scale context with quotes - provide completions + let scalePreColonContext = context.matchBefore(/scale\(\s*['"][^'"]*$/); if (scalePreColonContext) { if (!scalePreColonContext.text.includes(':')) { if (context.explicit) { @@ -161,89 +186,86 @@ function scalePreColonHandler(context) { return { from: scalePreColonContext.to, options: [] }; } } - if (!scalePreColonContext.text.includes(':')) { - return { from: scalePreColonContext.to, options: [] }; - } } return null; } function soundHandler(context) { - let soundContext = context.matchBefore(/(s|sound)\(\s*['"][^'"]*$/); - if (soundContext) { - const text = soundContext.text; - const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'")); - if (quoteIdx === -1) return null; - const inside = text.slice(quoteIdx + 1); - const fragMatch = inside.match(/(?:[\s\[\{\(<])([\w]*)$/); - const fragment = fragMatch ? fragMatch[1] : inside; - if (!fragment || fragment.length === 0) return null; - const soundNames = Object.keys(soundMap.get()).sort(); - const filteredSounds = soundNames.filter((name) => name.includes(fragment)); - let options = filteredSounds.map((name) => ({ label: name, type: 'sound' })); - const from = soundContext.to - fragment.length; + // First check for sound context without quotes - block with empty completions + let soundNoQuotesContext = context.matchBefore(/(s|sound)\(\s*$/); + if (soundNoQuotesContext) { return { - from, - options, + from: soundNoQuotesContext.to, + options: [], }; } - return null; + + // Then check for sound context with quotes - provide completions + let soundContext = context.matchBefore(/(s|sound)\(\s*['"][^'"]*$/); + if (!soundContext) return null; + + const text = soundContext.text; + const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'")); + if (quoteIdx === -1) return null; + const inside = text.slice(quoteIdx + 1); + const fragMatch = inside.match(/(?:[\s\[\{\(<])([\w]*)$/); + const fragment = fragMatch ? fragMatch[1] : inside; + const soundNames = Object.keys(soundMap.get()).sort(); + const filteredSounds = soundNames.filter((name) => name.includes(fragment)); + let options = filteredSounds.map((name) => ({ label: name, type: 'sound' })); + const from = soundContext.to - fragment.length; + return { + from, + options, + }; } function bankHandler(context) { - let bankMatch = context.matchBefore(/bank\(\s*(['"])?([\w]*)$/); - if (bankMatch) { - let banks = bankCompletions(); - // Extract quote and fragment using regex groups on match.text - const groups = bankMatch.text.match(/(['"])?([\w]*)$/); - const quote = groups ? groups[1] : undefined; - const fragment = groups ? groups[2] || '' : ''; - let from; - if (quote) { - from = bankMatch.from + bankMatch.text.indexOf(quote) + 1; - } else { - from = bankMatch.to - fragment.length; - } - const filteredBanks = banks.filter((b) => b.label.startsWith(fragment)); - let options; - if (!quote) { - options = filteredBanks.map((b) => ({ ...b, apply: '"' + b.label + '"' })); - } else { - const afterCursor = context.state.sliceDoc(bankMatch.to, bankMatch.to + 1); - options = filteredBanks.map((b) => { - if (afterCursor !== quote) { - return { ...b, apply: b.label + quote }; - } - return b; - }); - } + // First check for bank context without quotes - block with empty completions + let bankNoQuotesContext = context.matchBefore(/bank\(\s*$/); + if (bankNoQuotesContext) { return { - from, - options, + from: bankNoQuotesContext.to, + options: [], }; } - return null; + + // Then check for bank context with quotes - provide completions + let bankMatch = context.matchBefore(/bank\(\s*['"][^'"]*$/); + if (!bankMatch) return null; + + const text = bankMatch.text; + const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'")); + if (quoteIdx === -1) return null; + const inside = text.slice(quoteIdx + 1); + const fragment = inside; + let banks = bankCompletions(); + const filteredBanks = banks.filter((b) => b.label.startsWith(fragment)); + const from = bankMatch.to - fragment.length; + return { + from, + options: filteredBanks, + }; } function scaleAfterColonHandler(context) { - let scaleContext = context.matchBefore(/\.scale\(\s*['"][^'"]*:[^'"]*$/); - if (scaleContext) { - const text = scaleContext.text; - const colonIdx = text.lastIndexOf(':'); - if (colonIdx === -1) return null; - const fragment = text.slice(colonIdx + 1); - const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment)); - const options = filteredScales.map((s) => ({ - ...s, - apply: s.label.replace(/\s+/g, ':') - })); - const from = scaleContext.from + colonIdx + 1; - return { - from, - options, - }; - } - return null; + let scaleContext = context.matchBefore(/scale\(\s*['"][^'"]*:[^'"]*$/); + if (!scaleContext) return null; + + const text = scaleContext.text; + const colonIdx = text.lastIndexOf(':'); + if (colonIdx === -1) return null; + const fragment = text.slice(colonIdx + 1); + const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment)); + const options = filteredScales.map((s) => ({ + ...s, + apply: s.label.replace(/\s+/g, ':'), + })); + const from = scaleContext.from + colonIdx + 1; + return { + from, + options, + }; } function fallbackHandler(context) { @@ -264,7 +286,7 @@ const handlers = [ bankHandler, scaleAfterColonHandler, // this handler *must* be last - fallbackHandler + fallbackHandler, ]; export const strudelAutocomplete = (context) => { From cb011c8bb0b4187938aa03f264fccc4569ddffdc Mon Sep 17 00:00:00 2001 From: Matthew Pocock Date: Mon, 15 Sep 2025 18:44:30 +0100 Subject: [PATCH 18/41] Add autocomplete for mode function - Added modePreColonHandler for autocomplete of mode values (below, above, duck, root) - Added modeAfterColonHandler for pitch name completion after colon in mode:anchor syntax - Follows same pattern as sound/bank/scale handlers for complex expressions - Supports expressions like '' with proper fragment matching - Uses regex pattern (?:[\s\[\{\()<])([\w:]*)$ to handle delimited expressions --- packages/codemirror/autocomplete.mjs | 57 ++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index b5fb2f1fe..71b371e6c 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -97,6 +97,14 @@ try { console.warn('[autocomplete] Could not load scale names from Tonal:', e); } +// Valid mode values for voicing +const modeCompletions = [ + { label: 'below', type: 'mode' }, + { label: 'above', type: 'mode' }, + { label: 'duck', type: 'mode' }, + { label: 'root', type: 'mode' }, +]; + export const getSynonymDoc = (doc, synonym) => { const synonyms = doc.synonyms || []; const docLabel = getDocLabel(doc); @@ -248,6 +256,53 @@ function bankHandler(context) { }; } +function modePreColonHandler(context) { + // First check for mode context without quotes - block with empty completions + let modeNoQuotesContext = context.matchBefore(/mode\(\s*$/); + if (modeNoQuotesContext) { + return { + from: modeNoQuotesContext.to, + options: [], + }; + } + + // Then check for mode context with quotes - provide completions + let modeContext = context.matchBefore(/mode\(\s*['"][^'"]*$/); + if (!modeContext) return null; + + const text = modeContext.text; + const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'")); + if (quoteIdx === -1) return null; + const inside = text.slice(quoteIdx + 1); + const fragMatch = inside.match(/(?:[\s\[\{\(<])([\w:]*)$/); + const fragment = fragMatch ? fragMatch[1] : inside; + const filteredModes = modeCompletions.filter((m) => m.label.startsWith(fragment)); + const from = modeContext.to - fragment.length; + return { + from, + options: filteredModes, + }; +} + +function modeAfterColonHandler(context) { + let modeContext = context.matchBefore(/mode\(\s*['"][^'"]*:[^'"]*$/); + if (!modeContext) return null; + + const text = modeContext.text; + const colonIdx = text.lastIndexOf(':'); + if (colonIdx === -1) return null; + const fragment = text.slice(colonIdx + 1); + + // For anchor after colon, we can suggest pitch names + const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase())); + const options = filtered.map((p) => ({ label: p, type: 'pitch' })); + const from = modeContext.from + colonIdx + 1; + return { + from, + options, + }; +} + function scaleAfterColonHandler(context) { let scaleContext = context.matchBefore(/scale\(\s*['"][^'"]*:[^'"]*$/); if (!scaleContext) return null; @@ -284,7 +339,9 @@ const handlers = [ scalePreColonHandler, soundHandler, bankHandler, + modePreColonHandler, scaleAfterColonHandler, + modeAfterColonHandler, // this handler *must* be last fallbackHandler, ]; From 6ebe84e09629bae580ecafa595ea171f34be9cf1 Mon Sep 17 00:00:00 2001 From: Matthew Pocock Date: Mon, 15 Sep 2025 21:34:15 +0100 Subject: [PATCH 19/41] Added completion for chord. --- packages/codemirror/autocomplete.mjs | 81 ++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 71b371e6c..142c5f267 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -3,6 +3,7 @@ import { autocompletion } from '@codemirror/autocomplete'; import { h } from './html'; import { Scale } from '@tonaljs/tonal'; import { soundMap } from 'superdough'; +import { complex } from '../tonal/ireal.mjs'; const escapeHtml = (str) => { const div = document.createElement('div'); @@ -105,6 +106,23 @@ const modeCompletions = [ { label: 'root', type: 'mode' }, ]; +// Valid chord symbols from ireal dictionary plus empty string for major triads +const chordSymbols = ['', ...Object.keys(complex)].sort(); +const chordSymbolCompletions = chordSymbols.map((symbol) => { + if (symbol === '') { + return { + label: 'major', + apply: '', + type: 'chord-symbol', + }; + } + return { + label: symbol, + apply: symbol, + type: 'chord-symbol', + }; +}); + export const getSynonymDoc = (doc, synonym) => { const synonyms = doc.synonyms || []; const docLabel = getDocLabel(doc); @@ -216,7 +234,7 @@ function soundHandler(context) { const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'")); if (quoteIdx === -1) return null; const inside = text.slice(quoteIdx + 1); - const fragMatch = inside.match(/(?:[\s\[\{\(<])([\w]*)$/); + const fragMatch = inside.match(/(?:[\s[{(<])([\w]*)$/); const fragment = fragMatch ? fragMatch[1] : inside; const soundNames = Object.keys(soundMap.get()).sort(); const filteredSounds = soundNames.filter((name) => name.includes(fragment)); @@ -274,7 +292,7 @@ function modePreColonHandler(context) { const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'")); if (quoteIdx === -1) return null; const inside = text.slice(quoteIdx + 1); - const fragMatch = inside.match(/(?:[\s\[\{\(<])([\w:]*)$/); + const fragMatch = inside.match(/(?:[\s[{(<])([\w:]*)$/); const fragment = fragMatch ? fragMatch[1] : inside; const filteredModes = modeCompletions.filter((m) => m.label.startsWith(fragment)); const from = modeContext.to - fragment.length; @@ -303,6 +321,60 @@ function modeAfterColonHandler(context) { }; } +function chordHandler(context) { + // First check for chord context without quotes - block with empty completions + let chordNoQuotesContext = context.matchBefore(/chord\(\s*$/); + if (chordNoQuotesContext) { + return { + from: chordNoQuotesContext.to, + options: [], + }; + } + + // Then check for chord context with quotes - provide completions + let chordContext = context.matchBefore(/chord\(\s*['"][^'"]*$/); + if (!chordContext) return null; + + const text = chordContext.text; + const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'")); + if (quoteIdx === -1) return null; + const inside = text.slice(quoteIdx + 1); + + // Use same fragment matching as sound/mode for expressions like "" + const fragMatch = inside.match(/(?:[\s[{(<])([\w#b+^:-]*)$/); + const fragment = fragMatch ? fragMatch[1] : inside; + + // Check if fragment contains any pitch name at start (for root + symbol) + let rootMatch = null; + let symbolFragment = fragment; + for (const pitch of pitchNames) { + if (fragment.toLowerCase().startsWith(pitch.toLowerCase())) { + rootMatch = pitch; + symbolFragment = fragment.slice(pitch.length); + break; + } + } + + if (rootMatch) { + // We have a root, now complete chord symbols + const filteredSymbols = chordSymbolCompletions.filter((s) => + s.label.toLowerCase().startsWith(symbolFragment.toLowerCase()), + ); + + // Create completions that replace the entire chord, not just the symbol part + const options = filteredSymbols; + + const from = chordContext.to - symbolFragment.length; + return { from, options }; + } else { + // No root yet, complete with pitch names + const filteredPitches = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase())); + const options = filteredPitches.map((p) => ({ label: p, type: 'pitch' })); + const from = chordContext.to - fragment.length; + return { from, options }; + } +} + function scaleAfterColonHandler(context) { let scaleContext = context.matchBefore(/scale\(\s*['"][^'"]*:[^'"]*$/); if (!scaleContext) return null; @@ -339,6 +411,7 @@ const handlers = [ scalePreColonHandler, soundHandler, bankHandler, + chordHandler, modePreColonHandler, scaleAfterColonHandler, modeAfterColonHandler, @@ -349,7 +422,9 @@ const handlers = [ export const strudelAutocomplete = (context) => { for (const handler of handlers) { const result = handler(context); - if (result) return result; + if (result) { + return result; + } } return null; }; From edb6c0db2e0c9088db7f3b3c1240f63c07962127 Mon Sep 17 00:00:00 2001 From: Matthew Pocock Date: Tue, 16 Sep 2025 14:45:22 +0100 Subject: [PATCH 20/41] Reordered handlers to be a bit more organised. --- packages/codemirror/autocomplete.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 142c5f267..8b3111b51 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -408,12 +408,12 @@ function fallbackHandler(context) { } const handlers = [ - scalePreColonHandler, soundHandler, bankHandler, chordHandler, - modePreColonHandler, + scalePreColonHandler, scaleAfterColonHandler, + modePreColonHandler, modeAfterColonHandler, // this handler *must* be last fallbackHandler, From 7080490c68839226705b9df0acc14aebb1f8f4e8 Mon Sep 17 00:00:00 2001 From: Matthew Pocock Date: Thu, 18 Sep 2025 21:52:40 +0100 Subject: [PATCH 21/41] Merge handlers and cache regexes in autocomplete - Merged scalePreColonHandler and scaleAfterColonHandler into single scaleHandler - Merged modePreColonHandler and modeAfterColonHandler into single modeHandler - Cached all regex patterns as constants above their respective handler functions - Eliminated regex compilation overhead on every keystroke/navigation event - Improved code maintainability by reducing duplication between similar handlers - Preserved exact same functionality while improving performance Each handler now follows a consistent pattern of checking after-colon context first (more specific) then pre-colon context, with all regex patterns pre-compiled for better performance during intensive autocomplete sessions. --- packages/codemirror/autocomplete.mjs | 147 ++++++++++++++++----------- 1 file changed, 87 insertions(+), 60 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 8b3111b51..022c3bbaf 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -186,9 +186,16 @@ const pitchNames = [ 'Cb', ]; -function scalePreColonHandler(context) { +// Cached regex patterns for scaleHandler +const SCALE_NO_QUOTES_REGEX = /scale\(\s*$/; +const SCALE_AFTER_COLON_REGEX = /scale\(\s*['"][^'"]*:[^'"]*$/; +const SCALE_PRE_COLON_REGEX = /scale\(\s*['"][^'"]*$/; +const SCALE_PITCH_MATCH_REGEX = /([A-Ga-g][#b]*)?$/; +const SCALE_SPACES_TO_COLON_REGEX = /\s+/g; + +function scaleHandler(context) { // First check for scale context without quotes - block with empty completions - let scaleNoQuotesContext = context.matchBefore(/scale\(\s*$/); + let scaleNoQuotesContext = context.matchBefore(SCALE_NO_QUOTES_REGEX); if (scaleNoQuotesContext) { return { from: scaleNoQuotesContext.to, @@ -196,13 +203,33 @@ function scalePreColonHandler(context) { }; } - // Then check for scale context with quotes - provide completions - let scalePreColonContext = context.matchBefore(/scale\(\s*['"][^'"]*$/); + // Check for after-colon context first (more specific) + let scaleAfterColonContext = context.matchBefore(SCALE_AFTER_COLON_REGEX); + if (scaleAfterColonContext) { + const text = scaleAfterColonContext.text; + const colonIdx = text.lastIndexOf(':'); + if (colonIdx !== -1) { + const fragment = text.slice(colonIdx + 1); + const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment)); + const options = filteredScales.map((s) => ({ + ...s, + apply: s.label.replace(SCALE_SPACES_TO_COLON_REGEX, ':'), + })); + const from = scaleAfterColonContext.from + colonIdx + 1; + return { + from, + options, + }; + } + } + + // Then check for pre-colon context + let scalePreColonContext = context.matchBefore(SCALE_PRE_COLON_REGEX); if (scalePreColonContext) { if (!scalePreColonContext.text.includes(':')) { if (context.explicit) { const text = scalePreColonContext.text; - const match = text.match(/([A-Ga-g][#b]*)?$/); + const match = text.match(SCALE_PITCH_MATCH_REGEX); const fragment = match ? match[0] : ''; const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase())); const from = scalePreColonContext.to - fragment.length; @@ -216,9 +243,14 @@ function scalePreColonHandler(context) { return null; } +// Cached regex patterns for soundHandler +const SOUND_NO_QUOTES_REGEX = /(s|sound)\(\s*$/; +const SOUND_WITH_QUOTES_REGEX = /(s|sound)\(\s*['"][^'"]*$/; +const SOUND_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w]*)$/; + function soundHandler(context) { // First check for sound context without quotes - block with empty completions - let soundNoQuotesContext = context.matchBefore(/(s|sound)\(\s*$/); + let soundNoQuotesContext = context.matchBefore(SOUND_NO_QUOTES_REGEX); if (soundNoQuotesContext) { return { from: soundNoQuotesContext.to, @@ -227,14 +259,14 @@ function soundHandler(context) { } // Then check for sound context with quotes - provide completions - let soundContext = context.matchBefore(/(s|sound)\(\s*['"][^'"]*$/); + let soundContext = context.matchBefore(SOUND_WITH_QUOTES_REGEX); if (!soundContext) return null; const text = soundContext.text; const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'")); if (quoteIdx === -1) return null; const inside = text.slice(quoteIdx + 1); - const fragMatch = inside.match(/(?:[\s[{(<])([\w]*)$/); + const fragMatch = inside.match(SOUND_FRAGMENT_MATCH_REGEX); const fragment = fragMatch ? fragMatch[1] : inside; const soundNames = Object.keys(soundMap.get()).sort(); const filteredSounds = soundNames.filter((name) => name.includes(fragment)); @@ -246,9 +278,13 @@ function soundHandler(context) { }; } +// Cached regex patterns for bankHandler +const BANK_NO_QUOTES_REGEX = /bank\(\s*$/; +const BANK_WITH_QUOTES_REGEX = /bank\(\s*['"][^'"]*$/; + function bankHandler(context) { // First check for bank context without quotes - block with empty completions - let bankNoQuotesContext = context.matchBefore(/bank\(\s*$/); + let bankNoQuotesContext = context.matchBefore(BANK_NO_QUOTES_REGEX); if (bankNoQuotesContext) { return { from: bankNoQuotesContext.to, @@ -257,7 +293,7 @@ function bankHandler(context) { } // Then check for bank context with quotes - provide completions - let bankMatch = context.matchBefore(/bank\(\s*['"][^'"]*$/); + let bankMatch = context.matchBefore(BANK_WITH_QUOTES_REGEX); if (!bankMatch) return null; const text = bankMatch.text; @@ -274,9 +310,15 @@ function bankHandler(context) { }; } -function modePreColonHandler(context) { +// Cached regex patterns for modeHandler +const MODE_NO_QUOTES_REGEX = /mode\(\s*$/; +const MODE_AFTER_COLON_REGEX = /mode\(\s*['"][^'"]*:[^'"]*$/; +const MODE_PRE_COLON_REGEX = /mode\(\s*['"][^'"]*$/; +const MODE_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w:]*)$/; + +function modeHandler(context) { // First check for mode context without quotes - block with empty completions - let modeNoQuotesContext = context.matchBefore(/mode\(\s*$/); + let modeNoQuotesContext = context.matchBefore(MODE_NO_QUOTES_REGEX); if (modeNoQuotesContext) { return { from: modeNoQuotesContext.to, @@ -284,15 +326,33 @@ function modePreColonHandler(context) { }; } - // Then check for mode context with quotes - provide completions - let modeContext = context.matchBefore(/mode\(\s*['"][^'"]*$/); + // Check for after-colon context first (more specific) + let modeAfterColonContext = context.matchBefore(MODE_AFTER_COLON_REGEX); + if (modeAfterColonContext) { + const text = modeAfterColonContext.text; + const colonIdx = text.lastIndexOf(':'); + if (colonIdx !== -1) { + const fragment = text.slice(colonIdx + 1); + // For anchor after colon, we can suggest pitch names + const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase())); + const options = filtered.map((p) => ({ label: p, type: 'pitch' })); + const from = modeAfterColonContext.from + colonIdx + 1; + return { + from, + options, + }; + } + } + + // Then check for pre-colon context + let modeContext = context.matchBefore(MODE_PRE_COLON_REGEX); if (!modeContext) return null; const text = modeContext.text; const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'")); if (quoteIdx === -1) return null; const inside = text.slice(quoteIdx + 1); - const fragMatch = inside.match(/(?:[\s[{(<])([\w:]*)$/); + const fragMatch = inside.match(MODE_FRAGMENT_MATCH_REGEX); const fragment = fragMatch ? fragMatch[1] : inside; const filteredModes = modeCompletions.filter((m) => m.label.startsWith(fragment)); const from = modeContext.to - fragment.length; @@ -302,28 +362,14 @@ function modePreColonHandler(context) { }; } -function modeAfterColonHandler(context) { - let modeContext = context.matchBefore(/mode\(\s*['"][^'"]*:[^'"]*$/); - if (!modeContext) return null; - - const text = modeContext.text; - const colonIdx = text.lastIndexOf(':'); - if (colonIdx === -1) return null; - const fragment = text.slice(colonIdx + 1); - - // For anchor after colon, we can suggest pitch names - const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase())); - const options = filtered.map((p) => ({ label: p, type: 'pitch' })); - const from = modeContext.from + colonIdx + 1; - return { - from, - options, - }; -} +// Cached regex patterns for chordHandler +const CHORD_NO_QUOTES_REGEX = /chord\(\s*$/; +const CHORD_WITH_QUOTES_REGEX = /chord\(\s*['"][^'"]*$/; +const CHORD_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w#b+^:-]*)$/; function chordHandler(context) { // First check for chord context without quotes - block with empty completions - let chordNoQuotesContext = context.matchBefore(/chord\(\s*$/); + let chordNoQuotesContext = context.matchBefore(CHORD_NO_QUOTES_REGEX); if (chordNoQuotesContext) { return { from: chordNoQuotesContext.to, @@ -332,7 +378,7 @@ function chordHandler(context) { } // Then check for chord context with quotes - provide completions - let chordContext = context.matchBefore(/chord\(\s*['"][^'"]*$/); + let chordContext = context.matchBefore(CHORD_WITH_QUOTES_REGEX); if (!chordContext) return null; const text = chordContext.text; @@ -341,7 +387,7 @@ function chordHandler(context) { const inside = text.slice(quoteIdx + 1); // Use same fragment matching as sound/mode for expressions like "" - const fragMatch = inside.match(/(?:[\s[{(<])([\w#b+^:-]*)$/); + const fragMatch = inside.match(CHORD_FRAGMENT_MATCH_REGEX); const fragment = fragMatch ? fragMatch[1] : inside; // Check if fragment contains any pitch name at start (for root + symbol) @@ -375,28 +421,11 @@ function chordHandler(context) { } } -function scaleAfterColonHandler(context) { - let scaleContext = context.matchBefore(/scale\(\s*['"][^'"]*:[^'"]*$/); - if (!scaleContext) return null; - - const text = scaleContext.text; - const colonIdx = text.lastIndexOf(':'); - if (colonIdx === -1) return null; - const fragment = text.slice(colonIdx + 1); - const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment)); - const options = filteredScales.map((s) => ({ - ...s, - apply: s.label.replace(/\s+/g, ':'), - })); - const from = scaleContext.from + colonIdx + 1; - return { - from, - options, - }; -} +// Cached regex patterns for fallbackHandler +const FALLBACK_WORD_REGEX = /\w*/; function fallbackHandler(context) { - const word = context.matchBefore(/\w*/); + const word = context.matchBefore(FALLBACK_WORD_REGEX); if (word && word.from === word.to && !context.explicit) return null; if (word) { return { @@ -411,10 +440,8 @@ const handlers = [ soundHandler, bankHandler, chordHandler, - scalePreColonHandler, - scaleAfterColonHandler, - modePreColonHandler, - modeAfterColonHandler, + scaleHandler, + modeHandler, // this handler *must* be last fallbackHandler, ]; From ba93bd1bc59d932dc253ec305ab5da38bafc112f Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 15 Oct 2025 18:54:02 +0100 Subject: [PATCH 22/41] Fix onPaint for widgets --- packages/draw/draw.mjs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index a429395ff..95f3aed50 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -84,9 +84,18 @@ Pattern.prototype.onPaint = function (painter) { state.controls.painters = []; } state.controls.painters.push(painter); + return state; }); }; +// TODO - Why isn't this pure deep copy not working? +// Pattern.prototype.onPaint = function (painter) { +// return this.withState((state) => { +// const painters = state.controls.painters ? [...state.controls.painters, painter] : [painter]; +// return new State(state.span, { ...state.controls, painters }); +// }); +// }; + Pattern.prototype.getPainters = function () { let painters = []; this.queryArc(0, 0, { painters }); From 105db2a4ca1c05c4f2dd072209b059676d717042 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 18 Oct 2025 08:49:31 +0200 Subject: [PATCH 23/41] fix: tonal import in autocomplete + add tonal to codemirror package dependencies --- packages/codemirror/autocomplete.mjs | 2 +- packages/codemirror/package.json | 1 + packages/tonal/index.mjs | 4 ++-- pnpm-lock.yaml | 3 +++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 022c3bbaf..b8a569bea 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -3,7 +3,7 @@ import { autocompletion } from '@codemirror/autocomplete'; import { h } from './html'; import { Scale } from '@tonaljs/tonal'; import { soundMap } from 'superdough'; -import { complex } from '../tonal/ireal.mjs'; +import { complex } from '@strudel/tonal'; const escapeHtml = (str) => { const div = document.createElement('div'); diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index bb1059f11..b4e7d27a8 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -46,6 +46,7 @@ "@replit/codemirror-vscode-keymap": "^6.0.2", "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", + "@strudel/tonal": "workspace:*", "@strudel/transpiler": "workspace:*", "@tonaljs/tonal": "^4.10.0", "superdough": "workspace:*", diff --git a/packages/tonal/index.mjs b/packages/tonal/index.mjs index a3c0cba39..ebbcf0851 100644 --- a/packages/tonal/index.mjs +++ b/packages/tonal/index.mjs @@ -1,9 +1,9 @@ import './tonal.mjs'; import './voicings.mjs'; +import './ireal.mjs'; export * from './tonal.mjs'; export * from './voicings.mjs'; - -import './ireal.mjs'; +export * from './ireal.mjs'; export const packageName = '@strudel/tonal'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d67f42d28..055bb4bc7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -212,6 +212,9 @@ importers: '@strudel/draw': specifier: workspace:* version: link:../draw + '@strudel/tonal': + specifier: workspace:* + version: link:../tonal '@strudel/transpiler': specifier: workspace:* version: link:../transpiler From 72423c3a25718c8a26f1ca6350dbc7bf2591d051 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 18 Oct 2025 10:41:33 -0500 Subject: [PATCH 24/41] Adds back shape to superdough --- packages/superdough/superdough.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index a37504b1d..36bd68969 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -630,6 +630,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // effects coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse })); crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); + shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol })); distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype)); if (tremolosync != null) { From c2a5562bad41bea5192e089b040ddc7156f51580 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 20 Oct 2025 21:24:13 +0200 Subject: [PATCH 25/41] add new flavour under replicate --- packages/core/pattern.mjs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index fdf27e9fc..f5b0c676e 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2995,6 +2995,24 @@ export const drop = stepRegister('drop', function (i, pat) { * ).pace(8) */ export const extend = stepRegister('extend', function (factor, pat) { + return pat.fast(factor).expand(factor); +}); + +/** + * *Experimental* + * + * `replicate` is similar to `fast` in that it increases its density, but it also increases the step count + * accordingly. So `stepcat("a b".replicate(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"`. + * + * TODO: find out how this function differs from extend + * @example + * stepcat( + * sound("bd bd - cp").replicate(2), + * sound("bd - sd -") + * ).pace(8) + */ +export const replicate = stepRegister('replicate', function (factor, pat) { return pat.repeatCycles(factor).fast(factor).expand(factor); }); From 188f0e90984b61bf0275b40da7af6edbb9f77aec Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 20 Oct 2025 21:35:28 +0200 Subject: [PATCH 26/41] snapshot --- test/__snapshots__/examples.test.mjs.snap | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 937d42797..d7e13d4d9 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -8307,6 +8307,33 @@ exports[`runs examples > example "repeatCycles" example index 0 1`] = ` ] `; +exports[`runs examples > example "replicate" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:bd ]", + "[ 1/8 → 1/4 | s:bd ]", + "[ 3/8 → 1/2 | s:cp ]", + "[ 1/2 → 5/8 | s:bd ]", + "[ 5/8 → 3/4 | s:bd ]", + "[ 7/8 → 1/1 | s:cp ]", + "[ 1/1 → 9/8 | s:bd ]", + "[ 5/4 → 11/8 | s:sd ]", + "[ 3/2 → 13/8 | s:bd ]", + "[ 13/8 → 7/4 | s:bd ]", + "[ 15/8 → 2/1 | s:cp ]", + "[ 2/1 → 17/8 | s:bd ]", + "[ 17/8 → 9/4 | s:bd ]", + "[ 19/8 → 5/2 | s:cp ]", + "[ 5/2 → 21/8 | s:bd ]", + "[ 11/4 → 23/8 | s:sd ]", + "[ 3/1 → 25/8 | s:bd ]", + "[ 25/8 → 13/4 | s:bd ]", + "[ 27/8 → 7/2 | s:cp ]", + "[ 7/2 → 29/8 | s:bd ]", + "[ 29/8 → 15/4 | s:bd ]", + "[ 31/8 → 4/1 | s:cp ]", +] +`; + exports[`runs examples > example "reset" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh ]", From 20e5fdedfbce14cb0ce8fd84ccdf0cf35da3875c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 20 Oct 2025 21:38:43 +0200 Subject: [PATCH 27/41] fix: use replicate for ! in mondo --- packages/mondough/mondough.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index b7ee83787..e2c6dc9ad 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -5,7 +5,7 @@ import { slow, seq, stepcat, - extend, + replicate, expand, pace, chooseIn, @@ -36,7 +36,7 @@ lib.square = (...args) => stepcat(...args).setSteps(1); lib.angle = (...args) => stepcat(...args).pace(1); lib['*'] = fast; lib['/'] = slow; -lib['!'] = extend; +lib['!'] = replicate; lib['@'] = expand; lib['%'] = pace; lib['?'] = degradeBy; // todo: default 0.5 not working.. From 46af6ed9ef9f91b19ec39005951730d04f2ec40e Mon Sep 17 00:00:00 2001 From: milliganf Date: Mon, 20 Oct 2025 22:30:58 +0200 Subject: [PATCH 28/41] Fix a bug introduced by #4e17cfbdd6 When there's no subpath for a Github path, the URL should end with a '/'. Otherwise when we concatenate values on from a sample map it doesn't separate the base URL from the filename. Signed-off-by: milliganf --- packages/superdough/sampler.mjs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 3232fa476..14e6d3fa3 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -129,12 +129,9 @@ function githubPath(base, subpath = '') { let repo = components.length >= 2 ? components[1] : 'samples'; let branch = components.length >= 3 ? components[2] : 'main'; let other = components.slice(3); - if (subpath) { - other.push(subpath); - } other = other.join('/'); - return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`; + return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}/${subpath}`; } export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => { From 06ea5a36e238e90aae35fd779cce4b9376ea780d Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 22 Oct 2025 10:15:48 +0100 Subject: [PATCH 29/41] Make osc port and host configurable. Changes dependencies. --- packages/core/controls.mjs | 18 ++ packages/osc/osc.mjs | 33 +-- packages/osc/package.json | 3 +- packages/osc/server.js | 111 +++++----- pnpm-lock.yaml | 244 +++++++++++++++++++++- test/__snapshots__/examples.test.mjs.snap | 18 ++ 6 files changed, 343 insertions(+), 84 deletions(-) mode change 100644 => 100755 packages/osc/server.js diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 3475fc2f4..a3ca638aa 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -2320,6 +2320,24 @@ export const { miditouch } = registerControl('miditouch'); // TODO: what is this? export const { polyTouch } = registerControl('polyTouch'); +/** + * The host to send open sound control messages to. Requires running the OSC bridge. + * @name oschost + * @param {string | Pattern} oschost e.g. 'localhost' + * @example + * note("c4").oschost('127.0.0.1').oscport(57120).osc(); + */ +export const { oschost } = registerControl('oschost'); + +/** + * The port to send open sound control messages to. Requires running the OSC bridge. + * @name oscport + * @param {number | Pattern} oscport e.g. 57120 + * @example + * note("c4").oschost('127.0.0.1').oscport(57120).osc(); + */ +export const { oscport } = registerControl('oscport'); + export const getControlName = (alias) => { if (controlAlias.has(alias)) { return controlAlias.get(alias); diff --git a/packages/osc/osc.mjs b/packages/osc/osc.mjs index fe0691522..68caab3a7 100644 --- a/packages/osc/osc.mjs +++ b/packages/osc/osc.mjs @@ -4,8 +4,6 @@ Copyright (C) 2022 Strudel contributors - see . */ -import OSC from 'osc-js'; - import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core'; let connection; // Promise @@ -13,19 +11,18 @@ function connect() { if (!connection) { // make sure this runs only once connection = new Promise((resolve, reject) => { - const osc = new OSC(); - osc.open(); - osc.on('open', () => { - const url = osc.options?.plugin?.socket?.url; - logger(`[osc] connected${url ? ` to ${url}` : ''}`); - resolve(osc); + const ws = new WebSocket('ws://localhost:8080'); + ws.addEventListener('open', (event) => { + logger(`[osc] websocket connected`); + resolve(ws); }); - osc.on('close', () => { + ws.addEventListener('close', (event) => { + logger(`[osc] websocket closed`); connection = undefined; // allows new connection afterwards console.log('[osc] disconnected'); reject('OSC connection closed'); }); - osc.on('error', (err) => reject(err)); + ws.addEventListener('error', (err) => reject(err)); }).catch((err) => { connection = undefined; throw new Error('Could not connect to OSC server. Is it running?'); @@ -61,15 +58,19 @@ export function parseControlsFromHap(hap, cps) { const collator = new ClockCollator({}); export async function oscTrigger(hap, currentTime, cps = 1, targetTime) { - const osc = await connect(); + const ws = await connect(); const controls = parseControlsFromHap(hap, cps); const keyvals = Object.entries(controls).flat(); + const ts = collator.calculateTimestamp(currentTime, targetTime) * 1000; + const msg = { address: '/dirt/play', args: keyvals, timestamp: ts }; - const ts = Math.round(collator.calculateTimestamp(currentTime, targetTime) * 1000); - const message = new OSC.Message('/dirt/play', ...keyvals); - const bundle = new OSC.Bundle([message], ts); - bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60 - osc.send(bundle); + if ('oschost' in hap.value) { + msg['host'] = hap.value['oschost']; + } + if ('oscport' in hap.value) { + msg['port'] = hap.value['oscport']; + } + ws.send(JSON.stringify(msg)); } /** diff --git a/packages/osc/package.json b/packages/osc/package.json index bc828d798..ea453e1eb 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -38,7 +38,8 @@ "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", - "osc-js": "^2.4.1" + "osc": "^2.4.5", + "ws": "^8.18.3" }, "devDependencies": { "pkg": "^5.8.1", diff --git a/packages/osc/server.js b/packages/osc/server.js old mode 100644 new mode 100755 index d7ec21c4b..ede35e975 --- a/packages/osc/server.js +++ b/packages/osc/server.js @@ -6,70 +6,59 @@ Copyright (C) 2022 Strudel contributors - see . */ -import OSC from 'osc-js'; +// import OSC from 'osc-js'; -const args = process.argv.slice(2); -function getArgValue(flag) { - const i = args.indexOf(flag); - if (i !== -1) { - const nextIsFlag = args[i + 1]?.startsWith('--') ?? true; - if (nextIsFlag) return true; - return args[i + 1]; - } -} +import { WebSocketServer } from 'ws'; +import osc from 'osc'; -let udpClientPort = Number(getArgValue('--port')) || 57120; -let debug = Number(getArgValue('--debug')) || 0; +const WS_PORT = 8080; // WebSocket server port +const OSC_REMOTE_IP = '127.0.0.1'; +const OSC_REMOTE_PORT = 57120; -const config = { - receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client - udpServer: { - host: 'localhost', // @param {string} Hostname of udp server to bind to - port: 57121, // @param {number} Port of udp client for messaging - // enabling the following line will receive tidal messages: - // port: 57120, // @param {number} Port of udp client for messaging - exclusive: false, // @param {boolean} Exclusive flag - }, - udpClient: { - host: 'localhost', // @param {string} Hostname of udp client for messaging - port: udpClientPort, // @param {number} Port of udp client for messaging - }, - wsServer: { - host: 'localhost', // @param {string} Hostname of WebSocket server - port: 8080, // @param {number} Port of WebSocket server - }, -}; - -const osc = new OSC({ plugin: new OSC.BridgePlugin(config) }); - -if (debug) { - osc.on('*', (message) => { - const { address, args } = message; - let str = ''; - for (let i = 0; i < args.length; i += 2) { - str += `${args[i]}: ${args[i + 1]} `; - } - console.log(`${address} ${str}`); - }); -} - -osc.on('error', (message) => { - if (message.toString().includes('EADDRINUSE')) { - console.log(`------ ERROR ------- -a server is already running on port 57121! to stop it: -1. run "lsof -ti :57121 | xargs kill -9" (macos / linux) -2. re-run the osc server -`); - } else { - console.log(message); - } +const udpPort = new osc.UDPPort({ + localAddress: '0.0.0.0', + localPort: 0, + remoteAddress: OSC_REMOTE_IP, + remotePort: OSC_REMOTE_PORT, }); -osc.open(); +udpPort.open(); +console.log(`[Sending OSC] ${OSC_REMOTE_IP}:${OSC_REMOTE_PORT}`); -console.log('osc client running on port', config.udpClient.port); -console.log('osc server running on port', config.udpServer.port); -console.log('websocket server running on port', config.wsServer.port); -if (debug) { - console.log('debug logs enabled. incoming messages will appear below'); -} +udpPort.on('error', (e) => { + console.log('Error: ', e); +}); + +const wss = new WebSocketServer({ port: WS_PORT }); +console.log(`[Listening WS] ws://localhost:${WS_PORT}`); + +wss.on('connection', (ws) => { + console.log('New WebSocket connection'); + + ws.on('message', (message) => { + let osc_host = '127.0.0.1'; + let osc_port = 57120; + + try { + const data = JSON.parse(message); + if ('host' in data) { + osc_host = data['host']; + } + if ('port' in data) { + osc_port = data['port']; + } + let msg = { address: data['address'], args: data['args'] }; + if ('timestamp' in data) { + msg = { timeTag: osc.timeTag(0, data['timestamp']), packets: [msg] }; + } + + udpPort.send(msg, osc_host, osc_port); + } catch (err) { + console.error('Error parsing message:', err); + } + }); + + ws.on('close', () => { + console.log('WebSocket connection closed'); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 055bb4bc7..7b524ebf9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,7 +41,7 @@ importers: version: 2.2.7 '@vitest/coverage-v8': specifier: 3.0.4 - version: 3.0.4(vitest@3.0.4) + version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)) '@vitest/ui': specifier: ^3.0.4 version: 3.0.4(vitest@3.0.4) @@ -415,9 +415,12 @@ importers: '@strudel/core': specifier: workspace:* version: link:../core - osc-js: - specifier: ^2.4.1 - version: 2.4.1 + osc: + specifier: ^2.4.5 + version: 2.4.5 + ws: + specifier: ^8.18.3 + version: 8.18.3 devDependencies: pkg: specifier: ^5.8.1 @@ -2429,6 +2432,70 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@serialport/binding-mock@10.2.2': + resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==} + engines: {node: '>=12.0.0'} + + '@serialport/bindings-cpp@12.0.1': + resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==} + engines: {node: '>=16.0.0'} + + '@serialport/bindings-interface@1.2.2': + resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==} + engines: {node: ^12.22 || ^14.13 || >=16} + + '@serialport/parser-byte-length@12.0.0': + resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-cctalk@12.0.0': + resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@11.0.0': + resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@12.0.0': + resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-inter-byte-timeout@12.0.0': + resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-packet-length@12.0.0': + resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==} + engines: {node: '>=8.6.0'} + + '@serialport/parser-readline@11.0.0': + resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-readline@12.0.0': + resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-ready@12.0.0': + resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-regex@12.0.0': + resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-slip-encoder@12.0.0': + resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-spacepacket@12.0.0': + resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==} + engines: {node: '>=12.0.0'} + + '@serialport/stream@12.0.0': + resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==} + engines: {node: '>=12.0.0'} + '@shikijs/core@1.29.1': resolution: {integrity: sha512-Mo1gGGkuOYjDu5H8YwzmOuly9vNr8KDVkqj9xiKhhhFS8jisAtDSEWB9hzqRHLVQgFdA310e8XRJcW4tYhRB2A==} @@ -3681,6 +3748,15 @@ packages: supports-color: optional: true + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -5290,6 +5366,9 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + long@4.0.0: + resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -5673,6 +5752,9 @@ packages: ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -5731,6 +5813,9 @@ packages: resolution: {integrity: sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==} engines: {node: '>=10'} + node-addon-api@7.0.0: + resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==} + node-addon-api@8.3.0: resolution: {integrity: sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==} engines: {node: ^18 || ^20 || >= 21} @@ -5769,6 +5854,10 @@ packages: resolution: {integrity: sha512-yqkmYrMbK1wPrfz7mgeYvA4tBperLg9FQ4S3Sau3nSAkpOA0x0zC8nQ1siBwozy1f4SE8vq2n1WKv99r+PCa1Q==} engines: {node: '>= 0.6.0'} + node-gyp-build@4.6.0: + resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} + hasBin: true + node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true @@ -5930,6 +6019,9 @@ packages: osc-js@2.4.1: resolution: {integrity: sha512-QlSeRKJclL47FNvO1MUCAAp9frmCF9zcYbnf6R9HpcklAst8ZyX3ISsk1v/Vghr/5GmXn0bhVjFXF9h+hfnl4Q==} + osc@2.4.5: + resolution: {integrity: sha512-Nc4/qcl+vA/CMxiKS1xrYgzjfnyB3W94gZnrkn3eTzihlndbEml6+wj1YQNKBI4r+qrw3obCcEoU7SVH/OxpxA==} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -6689,6 +6781,10 @@ packages: serialize-javascript@4.0.0: resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} + serialport@12.0.0: + resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==} + engines: {node: '>=16.0.0'} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -6776,6 +6872,9 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + slip@1.0.2: + resolution: {integrity: sha512-XrcHe3NAcyD3wO+O4I13RcS4/3AF+S9RvGNj9JhJeS02HyImwD2E3QWLrmn9hBfL+fB6yapagwxRkeyYzhk98g==} + smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -7651,6 +7750,9 @@ packages: resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} engines: {node: '>=18'} + wolfy87-eventemitter@5.2.9: + resolution: {integrity: sha512-P+6vtWyuDw+MB01X7UeF8TaHBvbCovf4HPEMF/SV7BdDc1SMTiBy13SRD71lQh4ExFTG1d/WNzDGDCyOKSMblw==} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -7773,6 +7875,18 @@ packages: utf-8-validate: optional: true + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xmlcreate@2.0.4: resolution: {integrity: sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==} @@ -9769,6 +9883,76 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@serialport/binding-mock@10.2.2': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + optional: true + + '@serialport/bindings-cpp@12.0.1': + dependencies: + '@serialport/bindings-interface': 1.2.2 + '@serialport/parser-readline': 11.0.0 + debug: 4.3.4 + node-addon-api: 7.0.0 + node-gyp-build: 4.6.0 + transitivePeerDependencies: + - supports-color + optional: true + + '@serialport/bindings-interface@1.2.2': + optional: true + + '@serialport/parser-byte-length@12.0.0': + optional: true + + '@serialport/parser-cctalk@12.0.0': + optional: true + + '@serialport/parser-delimiter@11.0.0': + optional: true + + '@serialport/parser-delimiter@12.0.0': + optional: true + + '@serialport/parser-inter-byte-timeout@12.0.0': + optional: true + + '@serialport/parser-packet-length@12.0.0': + optional: true + + '@serialport/parser-readline@11.0.0': + dependencies: + '@serialport/parser-delimiter': 11.0.0 + optional: true + + '@serialport/parser-readline@12.0.0': + dependencies: + '@serialport/parser-delimiter': 12.0.0 + optional: true + + '@serialport/parser-ready@12.0.0': + optional: true + + '@serialport/parser-regex@12.0.0': + optional: true + + '@serialport/parser-slip-encoder@12.0.0': + optional: true + + '@serialport/parser-spacepacket@12.0.0': + optional: true + + '@serialport/stream@12.0.0': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + optional: true + '@shikijs/core@1.29.1': dependencies: '@shikijs/engine-javascript': 1.29.1 @@ -10373,7 +10557,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.0.4(vitest@3.0.4)': + '@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -11281,6 +11465,11 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.3.4: + dependencies: + ms: 2.1.2 + optional: true + debug@4.4.0: dependencies: ms: 2.1.3 @@ -13150,6 +13339,8 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + long@4.0.0: {} + longest-streak@3.1.0: {} loupe@3.1.2: {} @@ -13827,6 +14018,9 @@ snapshots: ms@2.0.0: {} + ms@2.1.2: + optional: true + ms@2.1.3: {} multimatch@5.0.0: @@ -13876,6 +14070,9 @@ snapshots: dependencies: semver: 7.6.3 + node-addon-api@7.0.0: + optional: true + node-addon-api@8.3.0: {} node-domexception@1.0.0: {} @@ -13902,6 +14099,9 @@ snapshots: node-getopt@0.3.2: {} + node-gyp-build@4.6.0: + optional: true + node-gyp-build@4.8.4: {} node-gyp@10.3.1: @@ -14153,11 +14353,17 @@ snapshots: os-tmpdir@1.0.2: {} - osc-js@2.4.1: + osc@2.4.5: dependencies: + long: 4.0.0 + slip: 1.0.2 + wolfy87-eventemitter: 5.2.9 ws: 8.18.0 + optionalDependencies: + serialport: 12.0.0 transitivePeerDependencies: - bufferutil + - supports-color - utf-8-validate own-keys@1.0.1: @@ -15052,6 +15258,26 @@ snapshots: dependencies: randombytes: 2.1.0 + serialport@12.0.0: + dependencies: + '@serialport/binding-mock': 10.2.2 + '@serialport/bindings-cpp': 12.0.1 + '@serialport/parser-byte-length': 12.0.0 + '@serialport/parser-cctalk': 12.0.0 + '@serialport/parser-delimiter': 12.0.0 + '@serialport/parser-inter-byte-timeout': 12.0.0 + '@serialport/parser-packet-length': 12.0.0 + '@serialport/parser-readline': 12.0.0 + '@serialport/parser-ready': 12.0.0 + '@serialport/parser-regex': 12.0.0 + '@serialport/parser-slip-encoder': 12.0.0 + '@serialport/parser-spacepacket': 12.0.0 + '@serialport/stream': 12.0.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + optional: true + set-blocking@2.0.0: {} set-function-length@1.2.2: @@ -15194,6 +15420,8 @@ snapshots: slash@3.0.0: {} + slip@1.0.2: {} + smart-buffer@4.2.0: {} socks-proxy-agent@8.0.5: @@ -16103,6 +16331,8 @@ snapshots: dependencies: string-width: 7.2.0 + wolfy87-eventemitter@5.2.9: {} + word-wrap@1.2.5: {} wordwrap@0.0.3: {} @@ -16302,6 +16532,8 @@ snapshots: ws@8.18.0: {} + ws@8.18.3: {} + xmlcreate@2.0.4: {} xtend@4.0.2: {} diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index d7e13d4d9..c39bb87bf 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -6958,6 +6958,24 @@ exports[`runs examples > example "orbit" example index 0 1`] = ` ] `; +exports[`runs examples > example "oschost" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]", + "[ 1/1 → 2/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]", + "[ 2/1 → 3/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]", + "[ 3/1 → 4/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]", +] +`; + +exports[`runs examples > example "oscport" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]", + "[ 1/1 → 2/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]", + "[ 2/1 → 3/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]", + "[ 3/1 → 4/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]", +] +`; + exports[`runs examples > example "outside" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:A3 ]", From f0bd5bffaa5e9b930b38c0d00ecad3a732bb75c9 Mon Sep 17 00:00:00 2001 From: jeromew Date: Wed, 22 Oct 2025 21:39:08 +0200 Subject: [PATCH 30/41] Fix sampler.mjs githubPath PR https://codeberg.org/uzu/strudel/pulls/1679 introduced a subtle bug when both other and subpath are empty. The url should not end with '//' --- packages/superdough/sampler.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 14e6d3fa3..8aba9d26d 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -129,9 +129,10 @@ function githubPath(base, subpath = '') { let repo = components.length >= 2 ? components[1] : 'samples'; let branch = components.length >= 3 ? components[2] : 'main'; let other = components.slice(3); + other.push(subpath ? subpath : '') other = other.join('/'); - return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}/${subpath}`; + return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`; } export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => { From ea91d30ce48ee0d6abc1d92669bb2f19e7e0a0e7 Mon Sep 17 00:00:00 2001 From: jeromew Date: Wed, 22 Oct 2025 21:52:53 +0200 Subject: [PATCH 31/41] Add semicolon --- packages/superdough/sampler.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 8aba9d26d..84bac3582 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -129,7 +129,7 @@ function githubPath(base, subpath = '') { let repo = components.length >= 2 ? components[1] : 'samples'; let branch = components.length >= 3 ? components[2] : 'main'; let other = components.slice(3); - other.push(subpath ? subpath : '') + other.push(subpath ? subpath : ''); other = other.join('/'); return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`; From 8f7b439dce8c40c135e2d0c0bb8672e4804fc591 Mon Sep 17 00:00:00 2001 From: yaxu Date: Thu, 23 Oct 2025 10:39:26 +0200 Subject: [PATCH 32/41] Move deploy location to deploy/strudel.cc --- .forgejo/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index 561fee658..f4a3ca0f3 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -34,4 +34,4 @@ jobs: apt update && apt install -y rsync mkdir ~/.ssh ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts - rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy \ No newline at end of file + rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc \ No newline at end of file From b09230fe903d40f0122e66002c2f00ec55cabfbf Mon Sep 17 00:00:00 2001 From: yaxu Date: Thu, 23 Oct 2025 10:40:08 +0200 Subject: [PATCH 33/41] Update .forgejo/workflows/deploy.yml --- .forgejo/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index f4a3ca0f3..0643f9be1 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: Build and Deploy +name: Build and Deploy to live (strudel.cc) on: [workflow_dispatch] From ccb5ec16859073f44edea9635d8dc5a6f5516ca6 Mon Sep 17 00:00:00 2001 From: yaxu Date: Thu, 23 Oct 2025 10:41:29 +0200 Subject: [PATCH 34/41] Add action to deploy 'beta' version to warm.strudel.cc --- .forgejo/workflows/deploy-warm-beta.yml | 37 +++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .forgejo/workflows/deploy-warm-beta.yml diff --git a/.forgejo/workflows/deploy-warm-beta.yml b/.forgejo/workflows/deploy-warm-beta.yml new file mode 100644 index 000000000..2de6670c8 --- /dev/null +++ b/.forgejo/workflows/deploy-warm-beta.yml @@ -0,0 +1,37 @@ +name: Build and Deploy to beta (warm.strudel.cc) + +on: [workflow_dispatch] + +# Allow one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + build: + runs-on: docker + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9.12.2 + - uses: actions/setup-node@v4 + with: + node-version: 20 + # cache: "pnpm" + - name: Install Dependencies + run: pnpm install + + - name: Build + run: pnpm build + + - name: Deploy + run: | + eval $(ssh-agent -s) + echo "$SSH_PRIVATE_KEY" | ssh-add - + apt update && apt install -y rsync + mkdir ~/.ssh + ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts + rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/warm.strudel.cc \ No newline at end of file From 39e2221c29fab34bb4fda6532084d6e624ca38fa Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Thu, 23 Oct 2025 15:05:39 +0100 Subject: [PATCH 35/41] add _processParts() --- packages/core/pattern.mjs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index f5b0c676e..ddea674ac 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -62,7 +62,12 @@ export class Pattern { this.__steps = steps === undefined ? undefined : Fraction(steps); } + _processParts() { + return this.withHap((hap) => hap.withContext((c) => ({ ...c, processParts: true }))); + } + setSteps(steps) { + // TODO should this be pure? this._steps = steps; return this; } From 7071dec77598e0cc0cc0da03bde37873b1c129c6 Mon Sep 17 00:00:00 2001 From: yaxu Date: Thu, 23 Oct 2025 16:07:42 +0200 Subject: [PATCH 36/41] revert 39e2221c29fab34bb4fda6532084d6e624ca38fa revert add _processParts() --- packages/core/pattern.mjs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index ddea674ac..f5b0c676e 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -62,12 +62,7 @@ export class Pattern { this.__steps = steps === undefined ? undefined : Fraction(steps); } - _processParts() { - return this.withHap((hap) => hap.withContext((c) => ({ ...c, processParts: true }))); - } - setSteps(steps) { - // TODO should this be pure? this._steps = steps; return this; } From 28056deb66583cb9ae0c2c94e3d5538413050924 Mon Sep 17 00:00:00 2001 From: moumar Date: Thu, 23 Oct 2025 23:15:53 +0200 Subject: [PATCH 37/41] Fix ZZFX example ZZFX example did not produce any sound out of the box, fixed by updating tremolo param. --- website/src/pages/learn/synths.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/learn/synths.mdx b/website/src/pages/learn/synths.mdx index 21a9b917e..0fcc41363 100644 --- a/website/src/pages/learn/synths.mdx +++ b/website/src/pages/learn/synths.mdx @@ -157,7 +157,7 @@ It has 20 parameters in total, here is a snippet that uses all: .pitchJump(0) // +/- pitch change after pitchJumpTime .pitchJumpTime(0) // >0 time after pitchJump is applied .lfo(0) // >0 resets slide + pitchJump + sets tremolo speed - .tremolo(0) // 0-1 lfo volume modulation amount + .tremolo(0.5) // 0-1 lfo volume modulation amount //.duration(.2) // overwrite strudel event duration //.gain(1) // change volume ._scope() // vizualise waveform (not zzfx related) From 5cc93996ce4148c79b43ad514aef4277b29b2240 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 26 Oct 2025 21:49:10 +0000 Subject: [PATCH 38/41] degithub --- website/src/repl/prebake.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index 1fbc84021..6218d72a4 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -23,8 +23,8 @@ export async function prebake() { // https://github.com/sgossner/VCSL/ // https://api.github.com/repositories/126427031/contents/ // LICENSE: CC0 general-purpose - samples(`${baseNoTrailing}/vcsl.json`, 'github:sgossner/VCSL/master/', { prebake: true }), - samples(`${baseNoTrailing}/tidal-drum-machines.json`, 'github:ritchse/tidal-drum-machines/main/machines/', { + samples(`${baseNoTrailing}/vcsl.json`, 'https://strudel.b-cdn.net/VCSL/', { prebake: true }), + samples(`${baseNoTrailing}/tidal-drum-machines.json`, 'https://strudel.b-cdn.net/tidal-drum-machines/machines/', { prebake: true, tag: 'drum-machines', }), @@ -145,7 +145,7 @@ export async function prebake() { 'num/20.wav', ], }, - 'github:tidalcycles/dirt-samples', + 'https://strudel.b-cdn.net/Dirt-Samples/', { prebake: true, }, From 8398385dcd7f9fc488bf334257c4489ed47dfe13 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 26 Oct 2025 22:18:37 +0000 Subject: [PATCH 39/41] piano via bunnycdn --- website/src/repl/prebake.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index 6218d72a4..b6d2d55f2 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -19,7 +19,7 @@ export async function prebake() { // => getting "window is not defined", as soon as "@strudel/soundfonts" is imported statically // seems to be a problem with soundfont2 import('@strudel/soundfonts').then(({ registerSoundfonts }) => registerSoundfonts()), - samples(`${baseNoTrailing}/piano.json`, undefined, { prebake: true }), + samples(`${baseNoTrailing}/piano.json`, 'https://strudel.b-cdn.net/piano/', { prebake: true }), // https://github.com/sgossner/VCSL/ // https://api.github.com/repositories/126427031/contents/ // LICENSE: CC0 general-purpose From 9ff0449ca35741b7d6cc7c1776c44c38ef897400 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 27 Oct 2025 09:24:17 +0000 Subject: [PATCH 40/41] use json files in dough-samples via bunny cdn --- website/src/repl/prebake.mjs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index b6d2d55f2..8a6ccc7e0 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -6,6 +6,7 @@ import './files.mjs'; const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; +const baseCDN = 'https://strudel.b-cdn.net'; export async function prebake() { // https://archive.org/details/SalamanderGrandPianoV3 @@ -19,23 +20,23 @@ export async function prebake() { // => getting "window is not defined", as soon as "@strudel/soundfonts" is imported statically // seems to be a problem with soundfont2 import('@strudel/soundfonts').then(({ registerSoundfonts }) => registerSoundfonts()), - samples(`${baseNoTrailing}/piano.json`, 'https://strudel.b-cdn.net/piano/', { prebake: true }), + samples(`${baseCDN}/piano.json`, `${baseCDN}/piano/`, { prebake: true }), // https://github.com/sgossner/VCSL/ // https://api.github.com/repositories/126427031/contents/ // LICENSE: CC0 general-purpose - samples(`${baseNoTrailing}/vcsl.json`, 'https://strudel.b-cdn.net/VCSL/', { prebake: true }), - samples(`${baseNoTrailing}/tidal-drum-machines.json`, 'https://strudel.b-cdn.net/tidal-drum-machines/machines/', { + samples(`${baseCDN}/vcsl.json`, `${baseCDN}/VCSL/`, { prebake: true }), + samples(`${baseCDN}/tidal-drum-machines.json`, `${baseCDN}/tidal-drum-machines/machines/`, { prebake: true, tag: 'drum-machines', }), - samples(`${baseNoTrailing}/uzu-drumkit.json`, undefined, { + samples(`${baseCDN}/uzu-drumkit.json`, `${baseCDN}/uzu-drumkit/`, { prebake: true, tag: 'drum-machines', }), - samples(`${baseNoTrailing}/uzu-wavetables.json`, undefined, { + samples(`${baseCDN}/uzu-wavetables.json`, `${baseCDN}/uzu-wavetables/`, { prebake: true, }), - samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }), + samples(`${baseCDN}/mridangam.json`, `${baseCDN}/mrid/`, { prebake: true, tag: 'drum-machines' }), samples( { casio: ['casio/high.wav', 'casio/low.wav', 'casio/noise.wav'], @@ -145,14 +146,14 @@ export async function prebake() { 'num/20.wav', ], }, - 'https://strudel.b-cdn.net/Dirt-Samples/', + `${baseCDN}/Dirt-Samples/`, { prebake: true, }, ), ]); - aliasBank(`${baseNoTrailing}/tidal-drum-machines-alias.json`); + aliasBank(`${baseCDN}/tidal-drum-machines-alias.json`); } const maxPan = noteToMidi('C8'); From cbe7aaacfbf8a51f5f5cf39bf79319f273214e24 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 27 Oct 2025 15:38:32 +0000 Subject: [PATCH 41/41] Publish - @strudel/codemirror@1.2.6 - @strudel/core@1.2.5 - @strudel/csound@1.2.6 - @strudel/draw@1.2.5 - @strudel/gamepad@1.2.5 - @strudel/hydra@1.2.5 - @strudel/midi@1.2.6 - @strudel/mini@1.2.5 - @strudel/mondo@1.1.5 - @strudel/motion@1.2.5 - @strudel/mqtt@1.2.5 - @strudel/osc@1.3.0 - @strudel/repl@1.2.7 - @strudel/serial@1.2.5 - @strudel/soundfonts@1.2.6 - superdough@1.2.6 - supradough@1.2.4 - @strudel/tonal@1.2.5 - @strudel/transpiler@1.2.5 - @strudel/web@1.2.6 - @strudel/webaudio@1.2.6 - @strudel/xen@1.2.5 --- packages/codemirror/package.json | 6 +++--- packages/core/package.json | 2 +- packages/csound/package.json | 2 +- packages/draw/package.json | 2 +- packages/gamepad/package.json | 2 +- packages/hydra/package.json | 2 +- packages/midi/package.json | 2 +- packages/mini/package.json | 2 +- packages/mondough/package.json | 2 +- packages/motion/package.json | 2 +- packages/mqtt/package.json | 2 +- packages/osc/package.json | 2 +- packages/repl/package.json | 2 +- packages/serial/package.json | 2 +- packages/soundfonts/package.json | 2 +- packages/superdough/package.json | 2 +- packages/supradough/package.json | 5 ++--- packages/tonal/package.json | 2 +- packages/transpiler/package.json | 2 +- packages/web/package.json | 2 +- packages/webaudio/package.json | 2 +- packages/xen/package.json | 2 +- pnpm-lock.yaml | 3 --- 23 files changed, 25 insertions(+), 29 deletions(-) diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index b4e7d27a8..e54c4a428 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "1.2.5", + "version": "1.2.6", "description": "Codemirror Extensions for Strudel", "main": "index.mjs", "publishConfig": { @@ -49,8 +49,8 @@ "@strudel/tonal": "workspace:*", "@strudel/transpiler": "workspace:*", "@tonaljs/tonal": "^4.10.0", - "superdough": "workspace:*", - "nanostores": "^0.11.3" + "nanostores": "^0.11.3", + "superdough": "workspace:*" }, "devDependencies": { "vite": "^6.0.11" diff --git a/packages/core/package.json b/packages/core/package.json index 7cf20cea7..c33432f14 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/core", - "version": "1.2.4", + "version": "1.2.5", "description": "Port of Tidal Cycles to JavaScript", "main": "index.mjs", "type": "module", diff --git a/packages/csound/package.json b/packages/csound/package.json index 90130a101..058b8d774 100644 --- a/packages/csound/package.json +++ b/packages/csound/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/csound", - "version": "1.2.5", + "version": "1.2.6", "description": "csound bindings for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/draw/package.json b/packages/draw/package.json index 6a4c57540..ac2123985 100644 --- a/packages/draw/package.json +++ b/packages/draw/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/draw", - "version": "1.2.4", + "version": "1.2.5", "description": "Helpers for drawing with Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/gamepad/package.json b/packages/gamepad/package.json index 555eac03f..8605e35cd 100644 --- a/packages/gamepad/package.json +++ b/packages/gamepad/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/gamepad", - "version": "1.2.4", + "version": "1.2.5", "description": "Gamepad Inputs for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/hydra/package.json b/packages/hydra/package.json index 7ba79d5d0..eccdde52f 100644 --- a/packages/hydra/package.json +++ b/packages/hydra/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/hydra", - "version": "1.2.4", + "version": "1.2.5", "description": "Hydra integration for strudel", "main": "hydra.mjs", "type": "module", diff --git a/packages/midi/package.json b/packages/midi/package.json index 2342cf7e9..ebb07fc3a 100644 --- a/packages/midi/package.json +++ b/packages/midi/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/midi", - "version": "1.2.5", + "version": "1.2.6", "description": "Midi API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mini/package.json b/packages/mini/package.json index 6eeaab0da..05bbd019b 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mini", - "version": "1.2.4", + "version": "1.2.5", "description": "Mini notation for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mondough/package.json b/packages/mondough/package.json index c81d76cb6..593d2264a 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mondo", - "version": "1.1.4", + "version": "1.1.5", "description": "mondo notation for strudel", "main": "mondough.mjs", "type": "module", diff --git a/packages/motion/package.json b/packages/motion/package.json index 57cac9cc8..b0a4d5565 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/motion", - "version": "1.2.4", + "version": "1.2.5", "description": "DeviceMotion API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json index 2e32825fe..1b34a4eae 100644 --- a/packages/mqtt/package.json +++ b/packages/mqtt/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mqtt", - "version": "1.2.4", + "version": "1.2.5", "description": "MQTT API for strudel", "main": "mqtt.mjs", "type": "module", diff --git a/packages/osc/package.json b/packages/osc/package.json index ea453e1eb..173b76744 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/osc", - "version": "1.2.10", + "version": "1.3.0", "description": "OSC messaging for strudel", "main": "osc.mjs", "bin": "./server.js", diff --git a/packages/repl/package.json b/packages/repl/package.json index 41a165c34..1ad173c61 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/repl", - "version": "1.2.6", + "version": "1.2.7", "description": "Strudel REPL as a Web Component", "module": "index.mjs", "publishConfig": { diff --git a/packages/serial/package.json b/packages/serial/package.json index 9ed89cf2a..d73b05a1c 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/serial", - "version": "1.2.4", + "version": "1.2.5", "description": "Webserial API for strudel", "main": "serial.mjs", "type": "module", diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json index 07e35674b..8c41a5d50 100644 --- a/packages/soundfonts/package.json +++ b/packages/soundfonts/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/soundfonts", - "version": "1.2.5", + "version": "1.2.6", "description": "Soundsfont support for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/superdough/package.json b/packages/superdough/package.json index a82117774..a95fc5ed0 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -1,6 +1,6 @@ { "name": "superdough", - "version": "1.2.5", + "version": "1.2.6", "description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.", "main": "index.mjs", "type": "module", diff --git a/packages/supradough/package.json b/packages/supradough/package.json index 7e465c0a9..503043718 100644 --- a/packages/supradough/package.json +++ b/packages/supradough/package.json @@ -1,6 +1,6 @@ { "name": "supradough", - "version": "1.2.3", + "version": "1.2.4", "description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.", "main": "index.mjs", "type": "module", @@ -32,6 +32,5 @@ "vite": "^6.0.11", "vite-plugin-bundle-audioworklet": "workspace:*", "wav-encoder": "^1.3.0" - }, - "dependencies": {} + } } diff --git a/packages/tonal/package.json b/packages/tonal/package.json index 1461bdc8e..611df6766 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/tonal", - "version": "1.2.4", + "version": "1.2.5", "description": "Tonal functions for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 18722bdc2..bd28a19f4 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/transpiler", - "version": "1.2.4", + "version": "1.2.5", "description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.", "main": "index.mjs", "type": "module", diff --git a/packages/web/package.json b/packages/web/package.json index df21f4055..aef044c79 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/web", - "version": "1.2.5", + "version": "1.2.6", "description": "Easy to setup, opiniated bundle of Strudel for the browser.", "module": "web.mjs", "publishConfig": { diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index 49da00f23..2d5cd420c 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/webaudio", - "version": "1.2.5", + "version": "1.2.6", "description": "Web Audio helpers for Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/xen/package.json b/packages/xen/package.json index 0a6736d9c..4015c437d 100644 --- a/packages/xen/package.json +++ b/packages/xen/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/xen", - "version": "1.2.4", + "version": "1.2.5", "description": "Xenharmonic API for strudel", "main": "index.mjs", "type": "module", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b524ebf9..23903fcac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6016,9 +6016,6 @@ packages: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} - osc-js@2.4.1: - resolution: {integrity: sha512-QlSeRKJclL47FNvO1MUCAAp9frmCF9zcYbnf6R9HpcklAst8ZyX3ISsk1v/Vghr/5GmXn0bhVjFXF9h+hfnl4Q==} - osc@2.4.5: resolution: {integrity: sha512-Nc4/qcl+vA/CMxiKS1xrYgzjfnyB3W94gZnrkn3eTzihlndbEml6+wj1YQNKBI4r+qrw3obCcEoU7SVH/OxpxA==}