From 4671d343c4f1abdad5e7f42874ec9ce3eb6b2940 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 21 Aug 2026 14:05:35 +0200 Subject: [PATCH 01/24] update dough to fix fit --- packages/dough/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/dough/package.json b/packages/dough/package.json index abbb543be..81ff5c375 100644 --- a/packages/dough/package.json +++ b/packages/dough/package.json @@ -31,7 +31,7 @@ "dependencies": { "@strudel/core": "workspace:*", "@strudel/webaudio": "workspace:*", - "dough-synth": "0.2.4" + "dough-synth": "0.2.7" }, "devDependencies": { "vite": "^6.0.11" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c667981ce..a8c6e82a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -312,8 +312,8 @@ importers: specifier: workspace:* version: link:../webaudio dough-synth: - specifier: 0.2.4 - version: 0.2.4 + specifier: 0.2.7 + version: 0.2.7 devDependencies: vite: specifier: ^6.0.11 @@ -4072,8 +4072,8 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} - dough-synth@0.2.4: - resolution: {integrity: sha512-qMnMKVj1B2ivUgobNCwqJqNzZHOMN+5339OmCOk07zIDEK7iCYdufa+ZDE2BEgmKrWDyhGS7oTNFjHtTwzEv9w==} + dough-synth@0.2.7: + resolution: {integrity: sha512-Kj0tnDqu9Q8a9E8HepU5BN9lZcy+kYIYka7rn0o8NGfeSdxSe4m9luz7Hel0zqt33FCStk1nLPYsGmbdPfj3gA==} hasBin: true dset@3.1.4: @@ -11794,7 +11794,7 @@ snapshots: dotenv@16.4.7: {} - dough-synth@0.2.4: {} + dough-synth@0.2.7: {} dset@3.1.4: {} From 58dfe9a55c62aa16b55af8291a5693e7c6408826 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 25 Aug 2026 11:21:59 +0200 Subject: [PATCH 02/24] update dough --- packages/dough/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/dough/package.json b/packages/dough/package.json index 81ff5c375..104fd034d 100644 --- a/packages/dough/package.json +++ b/packages/dough/package.json @@ -31,7 +31,7 @@ "dependencies": { "@strudel/core": "workspace:*", "@strudel/webaudio": "workspace:*", - "dough-synth": "0.2.7" + "dough-synth": "0.2.8" }, "devDependencies": { "vite": "^6.0.11" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8c6e82a6..50e3f0bfe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -312,8 +312,8 @@ importers: specifier: workspace:* version: link:../webaudio dough-synth: - specifier: 0.2.7 - version: 0.2.7 + specifier: 0.2.8 + version: 0.2.8 devDependencies: vite: specifier: ^6.0.11 @@ -4072,8 +4072,8 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} - dough-synth@0.2.7: - resolution: {integrity: sha512-Kj0tnDqu9Q8a9E8HepU5BN9lZcy+kYIYka7rn0o8NGfeSdxSe4m9luz7Hel0zqt33FCStk1nLPYsGmbdPfj3gA==} + dough-synth@0.2.8: + resolution: {integrity: sha512-Z5ibipaKr1cEkz95uXO89U2nBV+ytnm/9GR558YjPxS8QqtG/eTT239Bv1YoVg1nMH5a5dVh738sXFQ0BdWfvQ==} hasBin: true dset@3.1.4: @@ -11794,7 +11794,7 @@ snapshots: dotenv@16.4.7: {} - dough-synth@0.2.7: {} + dough-synth@0.2.8: {} dset@3.1.4: {} From c5fd80902bc0a286b75b2ff97f6069f0e2449490 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 26 Aug 2026 15:23:00 +0200 Subject: [PATCH 03/24] update dough to 0.3.0 + add group, voices, glide --- packages/dough/dough.mjs | 6 +++++- packages/dough/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/dough/dough.mjs b/packages/dough/dough.mjs index 908138bae..745bdf60b 100644 --- a/packages/dough/dough.mjs +++ b/packages/dough/dough.mjs @@ -4,12 +4,16 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { register, noteToMidi } from '@strudel/core'; +import { register, noteToMidi, registerControl } from '@strudel/core'; import { Dough, doughsamples } from 'dough-synth'; import { getAudioContext, ensureMinimalOutput } from '@strudel/webaudio'; import wasm from 'dough-synth/dough.wasm?url'; import workletCode from 'dough-synth/dough.js?raw'; +export const { group } = registerControl('group'); +export const { voices } = registerControl('voices'); +export const { glide } = registerControl('glide'); + Object.assign(globalThis, { doughsamples }); let D; diff --git a/packages/dough/package.json b/packages/dough/package.json index 104fd034d..4fb496701 100644 --- a/packages/dough/package.json +++ b/packages/dough/package.json @@ -31,7 +31,7 @@ "dependencies": { "@strudel/core": "workspace:*", "@strudel/webaudio": "workspace:*", - "dough-synth": "0.2.8" + "dough-synth": "0.3.0" }, "devDependencies": { "vite": "^6.0.11" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50e3f0bfe..6d42e108f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -312,8 +312,8 @@ importers: specifier: workspace:* version: link:../webaudio dough-synth: - specifier: 0.2.8 - version: 0.2.8 + specifier: 0.3.0 + version: 0.3.0 devDependencies: vite: specifier: ^6.0.11 @@ -4072,8 +4072,8 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} - dough-synth@0.2.8: - resolution: {integrity: sha512-Z5ibipaKr1cEkz95uXO89U2nBV+ytnm/9GR558YjPxS8QqtG/eTT239Bv1YoVg1nMH5a5dVh738sXFQ0BdWfvQ==} + dough-synth@0.3.0: + resolution: {integrity: sha512-23mwuF0QZOGngsiB+H1EW9ZOV2kciqHBJpi2kjZoWYOATi2v43trpai2UoY6vtWUR6raeAKYmazm4+Z5Sh8eUA==} hasBin: true dset@3.1.4: @@ -11794,7 +11794,7 @@ snapshots: dotenv@16.4.7: {} - dough-synth@0.2.8: {} + dough-synth@0.3.0: {} dset@3.1.4: {} From ce4ef629bfdbd8c64a7385e1cc09e5f00c68e72f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 12 Oct 2026 18:29:42 +0200 Subject: [PATCH 04/24] fix some annoying build warnings --- packages/codemirror/basicSetup.mjs | 14 ++------------ packages/core/pattern.mjs | 4 ++-- packages/soundfonts/fontloader.mjs | 2 +- packages/webaudio/supradough.mjs | 4 ++-- packages/xen/xen.mjs | 2 +- website/src/my_patterns.js | 2 +- website/src/repl/util.mjs | 15 +++++++++------ 7 files changed, 18 insertions(+), 25 deletions(-) diff --git a/packages/codemirror/basicSetup.mjs b/packages/codemirror/basicSetup.mjs index 02294b93a..1bad65f8b 100644 --- a/packages/codemirror/basicSetup.mjs +++ b/packages/codemirror/basicSetup.mjs @@ -2,23 +2,13 @@ import { keymap, highlightSpecialChars, drawSelection, - highlightActiveLine, dropCursor, rectangularSelection, crosshairCursor, - lineNumbers, - highlightActiveLineGutter, } from '@codemirror/view'; -import { - defaultHighlightStyle, - syntaxHighlighting, - bracketMatching, - foldGutter, - foldKeymap, -} from '@codemirror/language'; +import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language'; import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'; -import { searchKeymap, highlightSelectionMatches } from '@codemirror/search'; -import { completionKeymap, closeBracketsKeymap } from '@codemirror/autocomplete'; +import { closeBracketsKeymap } from '@codemirror/autocomplete'; // Taken + slightly modified from https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index d6f6e5986..15642880b 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2354,8 +2354,8 @@ export const ply = register('ply', function (factor, pat) { * @example * s("bd hh sd hh").fast(2) // s("[bd hh sd hh]*2") */ -export const { fast, density } = register( - ['fast', 'density'], +export const { fast } = register( + 'fast', function (factor, pat) { if (factor === 0) { return silence; diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index b4ae59828..073dc4901 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -28,7 +28,7 @@ async function loadFont(name) { const url = `${soundfontUrl}/${name}.js`; const preset = await fetch(url).then((res) => res.text()); let [_, data] = preset.split('={'); - return eval('{' + data); + return new Function('return {' + data)().zones; }; loadCache[name] = load(); return loadCache[name]; diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs index 83458c54e..925a9bd5e 100644 --- a/packages/webaudio/supradough.mjs +++ b/packages/webaudio/supradough.mjs @@ -61,7 +61,7 @@ function githubPath(base, subpath = '') { } return `https://raw.githubusercontent.com/${path}/${subpath}`; } -export async function fetchSampleMap(url) { +export async function _fetchSampleMap(url) { if (url.startsWith('github:')) { url = githubPath(url, 'strudel.json'); } @@ -116,7 +116,7 @@ async function fetchSample(url) { export async function supradoughsamples(sampleMap, baseUrl) { if (typeof sampleMap === 'string') { - const [json, base] = await fetchSampleMap(sampleMap); + const [json, base] = await _fetchSampleMap(sampleMap); // console.log('json', json, 'base', base); return supradoughsamples(json, base); } diff --git a/packages/xen/xen.mjs b/packages/xen/xen.mjs index 0dcc4c9d6..a3d11ac7d 100644 --- a/packages/xen/xen.mjs +++ b/packages/xen/xen.mjs @@ -106,7 +106,7 @@ export const xen = register( let k = parray(input); return new Pattern((state) => { - let res = k.withValue((x) => x.map((y) => eval(y.join('/')))).query(state); + let res = k.withValue((x) => x.map((y) => y.reduce((acc, v) => acc / v))).query(state); return res; }); }; diff --git a/website/src/my_patterns.js b/website/src/my_patterns.js index 5779cc427..bb10e2a86 100644 --- a/website/src/my_patterns.js +++ b/website/src/my_patterns.js @@ -1,7 +1,7 @@ import { getMetadata } from './metadata_parser'; export function getMyPatterns() { - const my = import.meta.glob('../../my-patterns/**', { as: 'raw', eager: true }); + const my = import.meta.glob('../../my-patterns/**', { query: '?raw', eager: true }); return Object.fromEntries( Object.entries(my) .filter(([name]) => name.endsWith('.txt')) diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index bd1a85f24..4561f06e9 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -1,12 +1,15 @@ -import { code2hash, errorLogger, evalScope, hash2code, logger } from '@strudel/core'; +import * as strudelCore from '@strudel/core'; +import * as strudelWebaudio from '@strudel/webaudio'; import { settingPatterns } from '../settings.mjs'; -import { setVersionDefaults } from '@strudel/webaudio'; -import { getMetadata } from '../metadata_parser'; +import { getMetadata } from '../metadata_parser.js'; import { isTauri } from '../tauri.mjs'; import './Repl.css'; import { createClient } from '@supabase/supabase-js'; import { writeText } from '@tauri-apps/plugin-clipboard-manager'; -import { $featuredPatterns /* , loadDBPatterns */ } from '@src/user_pattern_utils.mjs'; +import { $featuredPatterns } from '@src/user_pattern_utils.mjs'; + +const { setVersionDefaults } = strudelWebaudio; +const { code2hash, errorLogger, evalScope, hash2code, logger } = strudelCore; // Create a single supabase client for interacting with your database export const supabase = createClient( @@ -68,13 +71,13 @@ export async function getRandomTune() { export function loadModules() { let modules = [ - import('@strudel/core'), + strudelCore, import('@strudel/draw'), import('@strudel/edo'), import('@strudel/tonal'), import('@strudel/mini'), import('@strudel/xen'), - import('@strudel/webaudio'), + strudelWebaudio, import('@strudel/codemirror'), import('@strudel/hydra'), import('@strudel/serial'), From 0d4816804e33cd1ed376608c3d883748dec8b241 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 12 Oct 2026 18:29:51 +0200 Subject: [PATCH 05/24] dont cache csound --- website/astro.config.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/astro.config.mjs b/website/astro.config.mjs index de08502dd..23b49bc93 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -73,6 +73,7 @@ export default defineConfig({ workbox: { maximumFileSizeToCacheInBytes: 4194304, // 4MB globPatterns: ['**/*.{js,css,html,ico,png,svg,json,wav,mp3,ogg,ttf,woff2,TTF,otf}'], + globIgnores: ['img/**', '_astro/csound*.js'], runtimeCaching: [ { urlPattern: ({ url }) => @@ -88,7 +89,7 @@ export default defineConfig({ cacheName: 'external-samples', expiration: { maxEntries: 5000, - maxAgeSeconds: 60 * 60 * 24 * 30, // <== 14 days + maxAgeSeconds: 60 * 60 * 24 * 30, // <== 30 days }, cacheableResponse: { statuses: [0, 200], From 88fdb424c4618117da475c2cacd3097cc5e4e8db Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 12 Oct 2026 18:30:06 +0200 Subject: [PATCH 06/24] make linter less violent --- website/src/env.d.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/website/src/env.d.ts b/website/src/env.d.ts index 4138c53f3..12c817559 100644 --- a/website/src/env.d.ts +++ b/website/src/env.d.ts @@ -2,3 +2,6 @@ /// declare module 'date-fns'; +declare module '@strudel/*'; +declare module 'dough-synth'; +declare module 'paho-mqtt'; From cb06042d3b5b78a7ab55e253d67930f774d85865 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 12 Oct 2026 19:38:22 +0200 Subject: [PATCH 07/24] doc fixes --- packages/core/controls.mjs | 4 +- test/__snapshots__/examples.test.mjs.snap | 132 +++++++++++----------- website/src/pages/learn/effects.mdx | 4 +- website/src/pages/learn/factories.mdx | 5 - website/src/pages/learn/samples.mdx | 2 +- website/src/pages/learn/xen.mdx | 2 +- 6 files changed, 72 insertions(+), 77 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index e6d5c89e5..a59fe6c33 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -519,10 +519,10 @@ export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerM * each other with matrix commands like `fm13`, which would send `fm1` back into * `fm3` * - * @name fmi + * @name fm * @tags fm, superdough, supradough * @param {number | Pattern} brightness modulation index - * @synonyms fm + * @synonyms fmi * @example * note("c e g b g e") * .fm("<0 1 2 8 32>") diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index dc4c98c45..4bd44ce7b 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -4777,6 +4777,72 @@ exports[`runs examples > example "floor" example index 0 1`] = ` ] `; +exports[`runs examples > example "fm" example index 0 1`] = ` +[ + "[ 0/1 → 1/6 | note:c fmi:0 ]", + "[ 1/6 → 1/3 | note:e fmi:0 ]", + "[ 1/3 → 1/2 | note:g fmi:0 ]", + "[ 1/2 → 2/3 | note:b fmi:0 ]", + "[ 2/3 → 5/6 | note:g fmi:0 ]", + "[ 5/6 → 1/1 | note:e fmi:0 ]", + "[ 1/1 → 7/6 | note:c fmi:1 ]", + "[ 7/6 → 4/3 | note:e fmi:1 ]", + "[ 4/3 → 3/2 | note:g fmi:1 ]", + "[ 3/2 → 5/3 | note:b fmi:1 ]", + "[ 5/3 → 11/6 | note:g fmi:1 ]", + "[ 11/6 → 2/1 | note:e fmi:1 ]", + "[ 2/1 → 13/6 | note:c fmi:2 ]", + "[ 13/6 → 7/3 | note:e fmi:2 ]", + "[ 7/3 → 5/2 | note:g fmi:2 ]", + "[ 5/2 → 8/3 | note:b fmi:2 ]", + "[ 8/3 → 17/6 | note:g fmi:2 ]", + "[ 17/6 → 3/1 | note:e fmi:2 ]", + "[ 3/1 → 19/6 | note:c fmi:8 ]", + "[ 19/6 → 10/3 | note:e fmi:8 ]", + "[ 10/3 → 7/2 | note:g fmi:8 ]", + "[ 7/2 → 11/3 | note:b fmi:8 ]", + "[ 11/3 → 23/6 | note:g fmi:8 ]", + "[ 23/6 → 4/1 | note:e fmi:8 ]", +] +`; + +exports[`runs examples > example "fm" example index 1 1`] = ` +[ + "[ 0/1 → 1/8 | s:sine note:F1 fmi:4 fmi2:0 fmi3:0 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 1/8 → 1/4 | s:sine note:F1 fmi:4 fmi2:2.7408622801303864 fmi3:0.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 1/4 → 3/8 | s:sine note:F1 fmi:4 fmi2:1.479038767516613 fmi3:0.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 3/8 → 1/2 | s:sine note:F1 fmi:4 fmi2:1.605570062994957 fmi3:0.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 1/2 → 5/8 | s:sine note:F1 fmi:4 fmi2:1.041922464966774 fmi3:0.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 5/8 → 3/4 | s:sine note:F1 fmi:4 fmi2:0.5425434708595276 fmi3:0.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 3/4 → 7/8 | s:sine note:F1 fmi:4 fmi2:0.7833059206604958 fmi3:0.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 7/8 → 1/1 | s:sine note:F1 fmi:4 fmi2:1.590524323284626 fmi3:0.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 1/1 → 9/8 | s:sine note:F1 fmi:4 fmi2:2.078168660402298 fmi3:1 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 9/8 → 5/4 | s:sine note:F1 fmi:4 fmi2:2.689958058297634 fmi3:1.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 5/4 → 11/8 | s:sine note:F1 fmi:4 fmi2:2.914912812411785 fmi3:1.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 11/8 → 3/2 | s:sine note:F1 fmi:4 fmi2:0.7312150076031685 fmi3:1.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 3/2 → 13/8 | s:sine note:F1 fmi:4 fmi2:2.4336322993040085 fmi3:1.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 13/8 → 7/4 | s:sine note:F1 fmi:4 fmi2:1.987018845975399 fmi3:1.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 7/4 → 15/8 | s:sine note:F1 fmi:4 fmi2:0.8189515843987465 fmi3:1.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 15/8 → 2/1 | s:sine note:F1 fmi:4 fmi2:2.3782287165522575 fmi3:1.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 2/1 → 17/8 | s:sine note:F1 fmi:4 fmi2:3.838108479976654 fmi3:2 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 17/8 → 9/4 | s:sine note:F1 fmi:4 fmi2:3.613561175763607 fmi3:2.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 9/4 → 19/8 | s:sine note:F1 fmi:4 fmi2:1.3819300457835197 fmi3:2.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 19/8 → 5/2 | s:sine note:F1 fmi:4 fmi2:3.346026562154293 fmi3:2.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 5/2 → 21/8 | s:sine note:F1 fmi:4 fmi2:1.8344642966985703 fmi3:2.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 21/8 → 11/4 | s:sine note:F1 fmi:4 fmi2:0.6407739371061325 fmi3:2.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 11/4 → 23/8 | s:sine note:F1 fmi:4 fmi2:2.5329310819506645 fmi3:2.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 23/8 → 3/1 | s:sine note:F1 fmi:4 fmi2:0.06501668691635132 fmi3:2.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 3/1 → 25/8 | s:sine note:F1 fmi:4 fmi2:0.8691564425826073 fmi3:3 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 25/8 → 13/4 | s:sine note:F1 fmi:4 fmi2:1.3729755133390427 fmi3:3.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 13/4 → 27/8 | s:sine note:F1 fmi:4 fmi2:3.9694602862000465 fmi3:3.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 27/8 → 7/2 | s:sine note:F1 fmi:4 fmi2:1.6347672045230865 fmi3:3.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 7/2 → 29/8 | s:sine note:F1 fmi:4 fmi2:1.639795258641243 fmi3:3.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 29/8 → 15/4 | s:sine note:F1 fmi:4 fmi2:3.7565943151712418 fmi3:3.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 15/4 → 31/8 | s:sine note:F1 fmi:4 fmi2:3.2486692890524864 fmi3:3.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", + "[ 31/8 → 4/1 | s:sine note:F1 fmi:4 fmi2:2.9446661546826363 fmi3:3.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", +] +`; + exports[`runs examples > example "fmattack" example index 0 1`] = ` [ "[ 0/1 → 1/6 | note:c fmi:4 fmattack:0 ]", @@ -4893,72 +4959,6 @@ exports[`runs examples > example "fmh" example index 0 1`] = ` ] `; -exports[`runs examples > example "fmi" example index 0 1`] = ` -[ - "[ 0/1 → 1/6 | note:c fmi:0 ]", - "[ 1/6 → 1/3 | note:e fmi:0 ]", - "[ 1/3 → 1/2 | note:g fmi:0 ]", - "[ 1/2 → 2/3 | note:b fmi:0 ]", - "[ 2/3 → 5/6 | note:g fmi:0 ]", - "[ 5/6 → 1/1 | note:e fmi:0 ]", - "[ 1/1 → 7/6 | note:c fmi:1 ]", - "[ 7/6 → 4/3 | note:e fmi:1 ]", - "[ 4/3 → 3/2 | note:g fmi:1 ]", - "[ 3/2 → 5/3 | note:b fmi:1 ]", - "[ 5/3 → 11/6 | note:g fmi:1 ]", - "[ 11/6 → 2/1 | note:e fmi:1 ]", - "[ 2/1 → 13/6 | note:c fmi:2 ]", - "[ 13/6 → 7/3 | note:e fmi:2 ]", - "[ 7/3 → 5/2 | note:g fmi:2 ]", - "[ 5/2 → 8/3 | note:b fmi:2 ]", - "[ 8/3 → 17/6 | note:g fmi:2 ]", - "[ 17/6 → 3/1 | note:e fmi:2 ]", - "[ 3/1 → 19/6 | note:c fmi:8 ]", - "[ 19/6 → 10/3 | note:e fmi:8 ]", - "[ 10/3 → 7/2 | note:g fmi:8 ]", - "[ 7/2 → 11/3 | note:b fmi:8 ]", - "[ 11/3 → 23/6 | note:g fmi:8 ]", - "[ 23/6 → 4/1 | note:e fmi:8 ]", -] -`; - -exports[`runs examples > example "fmi" example index 1 1`] = ` -[ - "[ 0/1 → 1/8 | s:sine note:F1 fmi:4 fmi2:0 fmi3:0 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 1/8 → 1/4 | s:sine note:F1 fmi:4 fmi2:2.7408622801303864 fmi3:0.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 1/4 → 3/8 | s:sine note:F1 fmi:4 fmi2:1.479038767516613 fmi3:0.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 3/8 → 1/2 | s:sine note:F1 fmi:4 fmi2:1.605570062994957 fmi3:0.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 1/2 → 5/8 | s:sine note:F1 fmi:4 fmi2:1.041922464966774 fmi3:0.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 5/8 → 3/4 | s:sine note:F1 fmi:4 fmi2:0.5425434708595276 fmi3:0.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 3/4 → 7/8 | s:sine note:F1 fmi:4 fmi2:0.7833059206604958 fmi3:0.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 7/8 → 1/1 | s:sine note:F1 fmi:4 fmi2:1.590524323284626 fmi3:0.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 1/1 → 9/8 | s:sine note:F1 fmi:4 fmi2:2.078168660402298 fmi3:1 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 9/8 → 5/4 | s:sine note:F1 fmi:4 fmi2:2.689958058297634 fmi3:1.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 5/4 → 11/8 | s:sine note:F1 fmi:4 fmi2:2.914912812411785 fmi3:1.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 11/8 → 3/2 | s:sine note:F1 fmi:4 fmi2:0.7312150076031685 fmi3:1.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 3/2 → 13/8 | s:sine note:F1 fmi:4 fmi2:2.4336322993040085 fmi3:1.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 13/8 → 7/4 | s:sine note:F1 fmi:4 fmi2:1.987018845975399 fmi3:1.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 7/4 → 15/8 | s:sine note:F1 fmi:4 fmi2:0.8189515843987465 fmi3:1.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 15/8 → 2/1 | s:sine note:F1 fmi:4 fmi2:2.3782287165522575 fmi3:1.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 2/1 → 17/8 | s:sine note:F1 fmi:4 fmi2:3.838108479976654 fmi3:2 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 17/8 → 9/4 | s:sine note:F1 fmi:4 fmi2:3.613561175763607 fmi3:2.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 9/4 → 19/8 | s:sine note:F1 fmi:4 fmi2:1.3819300457835197 fmi3:2.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 19/8 → 5/2 | s:sine note:F1 fmi:4 fmi2:3.346026562154293 fmi3:2.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 5/2 → 21/8 | s:sine note:F1 fmi:4 fmi2:1.8344642966985703 fmi3:2.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 21/8 → 11/4 | s:sine note:F1 fmi:4 fmi2:0.6407739371061325 fmi3:2.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 11/4 → 23/8 | s:sine note:F1 fmi:4 fmi2:2.5329310819506645 fmi3:2.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 23/8 → 3/1 | s:sine note:F1 fmi:4 fmi2:0.06501668691635132 fmi3:2.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 3/1 → 25/8 | s:sine note:F1 fmi:4 fmi2:0.8691564425826073 fmi3:3 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 25/8 → 13/4 | s:sine note:F1 fmi:4 fmi2:1.3729755133390427 fmi3:3.125 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 13/4 → 27/8 | s:sine note:F1 fmi:4 fmi2:3.9694602862000465 fmi3:3.25 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 27/8 → 7/2 | s:sine note:F1 fmi:4 fmi2:1.6347672045230865 fmi3:3.375 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 7/2 → 29/8 | s:sine note:F1 fmi:4 fmi2:1.639795258641243 fmi3:3.5 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 29/8 → 15/4 | s:sine note:F1 fmi:4 fmi2:3.7565943151712418 fmi3:3.625 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 15/4 → 31/8 | s:sine note:F1 fmi:4 fmi2:3.2486692890524864 fmi3:3.75 fmh:1.06 fmh2:10 fmh3:0.1 ]", - "[ 31/8 → 4/1 | s:sine note:F1 fmi:4 fmi2:2.9446661546826363 fmi3:3.875 fmh:1.06 fmh2:10 fmh3:0.1 ]", -] -`; - exports[`runs examples > example "fmsustain" example index 0 1`] = ` [ "[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.1 fmsustain:1 ]", diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx index 9f8a3370c..381482c96 100644 --- a/website/src/pages/learn/effects.mdx +++ b/website/src/pages/learn/effects.mdx @@ -184,9 +184,9 @@ Each filter has 2 parameters: Amplitude modulation changes the amplitude (gain) periodically over time. -## am +## tremolo - + ## tremolosync diff --git a/website/src/pages/learn/factories.mdx b/website/src/pages/learn/factories.mdx index ab77c1028..94e634459 100644 --- a/website/src/pages/learn/factories.mdx +++ b/website/src/pages/learn/factories.mdx @@ -18,7 +18,6 @@ These are the equivalents used by the Mini Notation: | `stack(x, y)` | `"x,y"` | | `stepcat([3,x],[2,y])` | `"x@3 y@2"` | | `polymeter([a, b, c], [x, y])` | `"{a b c, x y}"` | -| `polymeterSteps(2, x, y, z)` | `"{x y z}%2"` | | `silence` | `"~"` | ## cat @@ -45,10 +44,6 @@ These are the equivalents used by the Mini Notation: -## polymeterSteps - - - ## silence diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index 6a2f0cb95..afdcce2e1 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -329,7 +329,7 @@ Sampler effects are functions that can be used to change the behaviour of sample ### begin - + ### end diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx index 3e6a5b12f..eba817157 100644 --- a/website/src/pages/learn/xen.mdx +++ b/website/src/pages/learn/xen.mdx @@ -22,7 +22,7 @@ These functions allow the use of scales other than your typical chromatic 12 bas {/* TODO (maybe): combine jsdoc things in tune.mjs with here */} - +{/* */} Here's an example of how to configure a basic hexany scale: From e66723230b4661e006f1fdaff4d8afe2e85b292c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 12 Oct 2026 19:39:05 +0200 Subject: [PATCH 08/24] make sure pnpm version is the same locally and in ci --- .forgejo/workflows/deploy-hot.yml | 2 -- .forgejo/workflows/deploy-warm-beta.yml | 2 -- .forgejo/workflows/deploy.yml | 2 -- .forgejo/workflows/test.yml | 2 -- package.json | 1 + pnpm-workspace.yaml | 25 +++++++++++++------------ 6 files changed, 14 insertions(+), 20 deletions(-) diff --git a/.forgejo/workflows/deploy-hot.yml b/.forgejo/workflows/deploy-hot.yml index a19917dfa..2414a382f 100644 --- a/.forgejo/workflows/deploy-hot.yml +++ b/.forgejo/workflows/deploy-hot.yml @@ -19,8 +19,6 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - uses: pnpm/action-setup@v4 - with: - version: 9.12.2 - uses: actions/setup-node@v4 with: node-version: 20 diff --git a/.forgejo/workflows/deploy-warm-beta.yml b/.forgejo/workflows/deploy-warm-beta.yml index 264f716d0..5f8410699 100644 --- a/.forgejo/workflows/deploy-warm-beta.yml +++ b/.forgejo/workflows/deploy-warm-beta.yml @@ -19,8 +19,6 @@ jobs: steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: 9.12.2 - uses: actions/setup-node@v4 with: node-version: 20 diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index 2b47e5e17..6b686ce7e 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -16,8 +16,6 @@ jobs: steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: 9.12.2 - uses: actions/setup-node@v4 with: node-version: 20 diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index c1db1c5f9..7f002d4e1 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -15,8 +15,6 @@ jobs: - name: apt install ztd run: apt update && apt install -y zstd - uses: pnpm/action-setup@v4 - with: - version: 9.12.2 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} diff --git a/package.json b/package.json index 26a20aec3..59e378329 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.5.0", "private": true, "description": "Port of tidalcycles to javascript", + "packageManager": "pnpm@10.15.0", "scripts": { "setup": "pnpm i", "pretest": "npm run jsdoc-json", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c8be1ac16..936f0cd29 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,18 +4,19 @@ packages: - tools/dbpatch - website/ -onlyBuiltDependencies: +catalog: + '@codemirror/autocomplete': ^6.18.4 + '@codemirror/commands': ^6.8.0 + '@codemirror/lang-javascript': ^6.2.2 + '@codemirror/language': ^6.10.8 + '@codemirror/search': ^6.5.8 + '@codemirror/state': ^6.5.1 + '@codemirror/view': ^6.36.2 + +ignoredBuiltDependencies: + - '@serialport/bindings-cpp' + - tree-sitter-haskell + - tree-sitter - esbuild - nx - sharp - - tree-sitter - - tree-sitter-haskell - -catalog: - "@codemirror/commands": "^6.8.0" - "@codemirror/language": "^6.10.8" - "@codemirror/lang-javascript": "^6.2.2" - "@codemirror/state": "^6.5.1" - "@codemirror/view": "^6.36.2" - "@codemirror/autocomplete": "^6.18.4" - "@codemirror/search": "^6.5.8" From 262a6d85dbc9497dcf1f26eb1415f1e042a52ab0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 12 Oct 2026 19:39:23 +0200 Subject: [PATCH 09/24] fix fast --- 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 15642880b..98aaee643 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2354,7 +2354,7 @@ export const ply = register('ply', function (factor, pat) { * @example * s("bd hh sd hh").fast(2) // s("[bd hh sd hh]*2") */ -export const { fast } = register( +export const fast = register( 'fast', function (factor, pat) { if (factor === 0) { From 63e28103754afe5ed85baed5acadfbf3172678f3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 12 Oct 2026 21:08:13 +0200 Subject: [PATCH 10/24] throw out setup-node to maybe speed up ci + remove failed cache: pnpm --- .forgejo/workflows/deploy-hot.yml | 3 --- .forgejo/workflows/deploy-warm-beta.yml | 5 +---- .forgejo/workflows/deploy.yml | 5 +---- .forgejo/workflows/test.yml | 9 --------- 4 files changed, 2 insertions(+), 20 deletions(-) diff --git a/.forgejo/workflows/deploy-hot.yml b/.forgejo/workflows/deploy-hot.yml index 2414a382f..b6556c936 100644 --- a/.forgejo/workflows/deploy-hot.yml +++ b/.forgejo/workflows/deploy-hot.yml @@ -19,9 +19,6 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - name: Install Dependencies run: pnpm install diff --git a/.forgejo/workflows/deploy-warm-beta.yml b/.forgejo/workflows/deploy-warm-beta.yml index 5f8410699..67b9d5b39 100644 --- a/.forgejo/workflows/deploy-warm-beta.yml +++ b/.forgejo/workflows/deploy-warm-beta.yml @@ -19,10 +19,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - # cache: "pnpm" + - name: Install Dependencies run: pnpm install diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index 6b686ce7e..e9f035ba8 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -16,10 +16,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - # cache: "pnpm" + - name: Install Dependencies run: pnpm install diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index 7f002d4e1..bae636c94 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -6,19 +6,10 @@ jobs: build: runs-on: docker container: node:22-bookworm - strategy: - matrix: - node-version: [20] steps: - uses: actions/checkout@v4 - - name: apt install ztd - run: apt update && apt install -y zstd - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: "pnpm" - run: pnpm install - run: pnpm run format-check - run: pnpm run lint From 1f2202e6a1029549702d3c055cb700db710977f0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 12 Oct 2026 23:02:04 +0200 Subject: [PATCH 11/24] fix clock timing problems after system sleep deprivation --- packages/osc/osc.mjs | 11 ++++++++++- packages/superdough/clockbridge.mjs | 12 ++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/osc/osc.mjs b/packages/osc/osc.mjs index 7c3be572a..03a8b5747 100644 --- a/packages/osc/osc.mjs +++ b/packages/osc/osc.mjs @@ -55,6 +55,7 @@ export function parseControlsFromHap(hap, cps) { return controls; } +let timeOrigin = performance.timeOrigin; export async function oscOutput(hap, targetTime, cps = 1) { const ws = await connect(); @@ -63,7 +64,15 @@ export async function oscOutput(hap, targetTime, cps = 1) { logger('[osc] clockbridge not ready'); return; } - const ts = performance.timeOrigin + timeMs; + // when the system sleeps while the browser is open, performance.now() doesnt tick + // .. which means performance.timeOrigin + performance.now() doesnt land in the present + const deprivation = timeOrigin + performance.now() - Date.now(); + if (Math.abs(deprivation) > 20) { + console.log(`[osc] snap with ${(deprivation / 1000).toFixed(2)}s deprivation`); + // shift time after microsleep + timeOrigin -= deprivation; + } + const ts = timeOrigin + timeMs; const controls = parseControlsFromHap(hap, cps); const keyvals = Object.entries(controls).flat(); diff --git a/packages/superdough/clockbridge.mjs b/packages/superdough/clockbridge.mjs index b2bbedb62..8ed6e125e 100644 --- a/packages/superdough/clockbridge.mjs +++ b/packages/superdough/clockbridge.mjs @@ -25,8 +25,16 @@ export class ClockBridge { } const offset = performanceTime - contextTime * 1000; // clock offset in ms const dt = performanceTime - (this.lastTime ?? performanceTime); // delta time since last hap - const decay = 1 / 10000; // how fast offset changes have an effect - this.p = expDecay(this.p ?? offset, offset, decay, dt); // smooth clock offset + const deprivation = offset - this.p; + // when the system sleeps, the audio and system clock get jumbled + // so we dont smooth if the jump is too high, to snap the offset + if (Math.abs(deprivation) > 20) { + console.log(`[ClockBridge] snap with ${(deprivation / 1000).toFixed(2)}s deprivation`); + this.p = offset; + } else { + const decay = 1 / 10000; // how fast offset changes have an effect + this.p = expDecay(this.p ?? offset, offset, decay, dt); // smooth clock offset + } this.lastTime = performanceTime; return this.p; } From 2cc9bf25681f811e2e9b77a1c6aec7f17e538af5 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 13 Oct 2026 18:37:29 +0200 Subject: [PATCH 12/24] update dough to 0.4.1 --- packages/dough/package.json | 2 +- pnpm-lock.yaml | 29 ++++++++++++++++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/dough/package.json b/packages/dough/package.json index 4fb496701..7dc73a962 100644 --- a/packages/dough/package.json +++ b/packages/dough/package.json @@ -31,7 +31,7 @@ "dependencies": { "@strudel/core": "workspace:*", "@strudel/webaudio": "workspace:*", - "dough-synth": "0.3.0" + "dough-synth": "0.4.1" }, "devDependencies": { "vite": "^6.0.11" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58eba5679..c4c045c9f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -318,8 +318,8 @@ importers: specifier: workspace:* version: link:../webaudio dough-synth: - specifier: 0.3.0 - version: 0.3.0 + specifier: 0.4.1 + version: 0.4.1 devDependencies: vite: specifier: ^6.0.11 @@ -4078,8 +4078,8 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} - dough-synth@0.3.0: - resolution: {integrity: sha512-23mwuF0QZOGngsiB+H1EW9ZOV2kciqHBJpi2kjZoWYOATi2v43trpai2UoY6vtWUR6raeAKYmazm4+Z5Sh8eUA==} + dough-synth@0.4.1: + resolution: {integrity: sha512-hxBsfOBwTIlJMhSB/Pk2xNNc7YTAVTgexn5Y7GRv/Kn2qBkMxwLxHJNvhsUhklQhVvzny/aCihxb0EOq4tv5Bw==} hasBin: true dset@3.1.4: @@ -8030,6 +8030,18 @@ packages: utf-8-validate: optional: true + ws@8.22.0: + resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==} + 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==} @@ -11800,7 +11812,12 @@ snapshots: dotenv@16.4.7: {} - dough-synth@0.3.0: {} + dough-synth@0.4.1: + dependencies: + ws: 8.22.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate dset@3.1.4: {} @@ -16698,6 +16715,8 @@ snapshots: ws@8.18.3: {} + ws@8.22.0: {} + xmlcreate@2.0.4: {} xtend@4.0.2: {} From c398620439dfc678119191f0d56b907728e36ad7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 13 Oct 2026 18:37:38 +0200 Subject: [PATCH 13/24] add groupmod + gmod --- packages/dough/dough.mjs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/dough/dough.mjs b/packages/dough/dough.mjs index 942e8fbe0..78ac4dc1b 100644 --- a/packages/dough/dough.mjs +++ b/packages/dough/dough.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { register, noteToMidi, registerControl } from '@strudel/core'; +import { register, noteToMidi, registerControl, stack, Pattern } from '@strudel/core'; import { Dough, doughsamples } from 'dough-synth'; import { getAudioContext, ensureMinimalOutput } from '@strudel/webaudio'; import wasm from 'dough-synth/dough.wasm?url'; @@ -77,3 +77,12 @@ export const dough = register('dough', (pat) => { ensureMinimalOutput(); return pat.onSchedule(doughOutput); }); + +export const groupmod = register('groupmod', function (modulator, pat) { + const modpat = modulator + .withValue((vm) => pat.withValue(({ group }) => ({ ...vm, group }))) + .outerJoin() + .set({ dough: 'update_group' }); + return stack(pat.set(modulator), modpat); +}); +Pattern.prototype.gmod = Pattern.prototype.groupmod; From ff80bae275a89a8f2bd7385ac32db41d0dd80cd4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 13 Oct 2026 20:52:05 +0200 Subject: [PATCH 14/24] move common dough things to lib.mjs --- packages/dough/dough.mjs | 29 +++++------------------------ packages/dough/lib.mjs | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 24 deletions(-) create mode 100644 packages/dough/lib.mjs diff --git a/packages/dough/dough.mjs b/packages/dough/dough.mjs index 78ac4dc1b..4701ce205 100644 --- a/packages/dough/dough.mjs +++ b/packages/dough/dough.mjs @@ -4,15 +4,13 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { register, noteToMidi, registerControl, stack, Pattern } from '@strudel/core'; +import { register } from '@strudel/core'; import { Dough, doughsamples } from 'dough-synth'; import { getAudioContext, ensureMinimalOutput } from '@strudel/webaudio'; import wasm from 'dough-synth/dough.wasm?url'; import workletCode from 'dough-synth/dough.js?raw'; - -export const { group } = registerControl('group'); -export const { voices } = registerControl('voices'); -export const { glide } = registerControl('glide'); +import { hap2dough } from './lib.mjs'; +export * from './lib.mjs'; Object.assign(globalThis, { doughsamples }); @@ -46,16 +44,8 @@ export async function doughOutput(hap, targetTime, cps = 1) { if (!offset) { return; // not ready } - hap.ensureObjectValue(); - const event = { - dough: 'play', - ...hap.value, - time: targetTime - offset, - duration: hap.duration / cps, - }; - if (typeof event.note === 'string') { - event.note = noteToMidi(event.note); - } + const t = targetTime - offset; + const event = hap2dough(hap, t, cps); D.evaluate(event); } @@ -77,12 +67,3 @@ export const dough = register('dough', (pat) => { ensureMinimalOutput(); return pat.onSchedule(doughOutput); }); - -export const groupmod = register('groupmod', function (modulator, pat) { - const modpat = modulator - .withValue((vm) => pat.withValue(({ group }) => ({ ...vm, group }))) - .outerJoin() - .set({ dough: 'update_group' }); - return stack(pat.set(modulator), modpat); -}); -Pattern.prototype.gmod = Pattern.prototype.groupmod; diff --git a/packages/dough/lib.mjs b/packages/dough/lib.mjs new file mode 100644 index 000000000..4ceb05441 --- /dev/null +++ b/packages/dough/lib.mjs @@ -0,0 +1,28 @@ +import { register, registerControl, stack, Pattern, noteToMidi } from '@strudel/core'; + +export const { group } = registerControl('group'); +export const { voices } = registerControl('voices'); +export const { glide } = registerControl('glide'); + +export const groupmod = register('groupmod', function (modulator, pat) { + const modpat = modulator + .withValue((vm) => pat.withValue(({ group }) => ({ ...vm, group }))) + .outerJoin() + .set({ dough: 'update_group' }); + return stack(pat.set(modulator), modpat); +}); +Pattern.prototype.gmod = Pattern.prototype.groupmod; + +export function hap2dough(hap, time, cps) { + hap.ensureObjectValue(); + const event = { + dough: 'play', + ...hap.value, + time, + duration: hap.duration / cps, + }; + if (typeof event.note === 'string') { + event.note = noteToMidi(event.note); + } + return event; +} From 2ebf9f710348846e7c2ea5ff9c773674fa882e8b Mon Sep 17 00:00:00 2001 From: hirnsalat Date: Wed, 14 Oct 2026 19:02:33 +0200 Subject: [PATCH 15/24] Revert "Merge pull request 'block based eval' (#1714) from Dsm0/strudel:block-based-eval-squashed into main" This reverts commit 70be560a7a21003ffb6b9c8d6e126480dd6902ef, reversing changes made to 98d613fbdfa769e8dcd42c804aaec8fbf41ea5b7. --- packages/codemirror/block_utilities.mjs | 51 --- packages/codemirror/codemirror.mjs | 86 +---- packages/codemirror/flash.mjs | 8 +- packages/codemirror/highlight.mjs | 221 ++++-------- packages/codemirror/slider.mjs | 123 +------ packages/codemirror/widget.mjs | 238 +++---------- packages/core/evaluate.mjs | 28 -- packages/core/repl.mjs | 333 ++---------------- packages/draw/draw.mjs | 10 - packages/transpiler/transpiler.mjs | 276 ++------------- test/examples.test.mjs | 1 - website/astro.config.mjs | 7 +- .../src/repl/components/panel/SettingsTab.jsx | 6 - website/src/repl/useReplContext.jsx | 19 +- website/src/settings.mjs | 2 - 15 files changed, 215 insertions(+), 1194 deletions(-) delete mode 100644 packages/codemirror/block_utilities.mjs diff --git a/packages/codemirror/block_utilities.mjs b/packages/codemirror/block_utilities.mjs deleted file mode 100644 index fd3dbedcd..000000000 --- a/packages/codemirror/block_utilities.mjs +++ /dev/null @@ -1,51 +0,0 @@ -// Block-based evaluation utilities - -export function getBlockRegions(code) { - const chars = code.split(''); - let i = 0, - blanks = [], - blockStart = 0, - regions = []; - while (i < chars.length) { - const isBlank = chars[i] === '\n'; - if (isBlank) { - blanks.push(i); - } else if (chars[i].trim() !== '') { - if (blanks.length > 1) { - regions.push([blockStart, blanks[0]]); - blockStart = i; - } - blanks = []; - } - i++; - } - regions.push([blockStart, blanks.length ? blanks[0] : i]); - return regions; -} - -export function getBlockAt(code, cursor) { - const regions = getBlockRegions(code); - for (const [start, end] of regions) { - if (cursor >= start && cursor <= end) { - return [start, end]; - } - } - return null; -} - -export const evalBlock = (strudelMirror) => { - const { state } = strudelMirror.editor; - const code = state.doc.toString(); - const cursor = state.selection.main.head; - const range = getBlockAt(code, cursor); - if (range) { - const [a, b] = range; - const block = code.slice(a, b); - if (block) { - // Flash the block being evaluated - strudelMirror.flash(200, { from: a, to: b }); - strudelMirror.repl.evaluateBlock(block, true, { range }); - } - } - return true; -}; diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 6014b9b0a..8784d3844 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -13,18 +13,18 @@ import { } from '@codemirror/view'; import { persistentAtom } from '@nanostores/persistent'; import { logger, registerControl, repl } from '@strudel/core'; -import { cleanupDraw, cleanupDrawContext, Drawer } from '@strudel/draw'; +import { cleanupDraw, Drawer } from '@strudel/draw'; + import { isAutoCompletionEnabled } from './autocomplete.mjs'; import { basicSetup } from './basicSetup.mjs'; -import { evalBlock } from './block_utilities.mjs'; import { flash, isFlashEnabled } from './flash.mjs'; import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs'; import { keybindings } from './keybindings.mjs'; -import { jumpToCharacter } from './labelJump.mjs'; -import { getSliderWidgets, sliderPlugin, updateSliderWidgets } from './slider.mjs'; +import { sliderPlugin, updateSliderWidgets } from './slider.mjs'; import { activateTheme, initTheme, theme } from './themes.mjs'; import { isTooltipEnabled } from './tooltip.mjs'; -import { getActiveWidgets, updateWidgets, widgetPlugin } from './widget.mjs'; +import { updateWidgets, widgetPlugin } from './widget.mjs'; +import { jumpToCharacter } from './labelJump.mjs'; export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands'; @@ -64,7 +64,6 @@ export const defaultSettings = { isLineWrappingEnabled: false, isTabIndentationEnabled: false, isMultiCursorEnabled: false, - isBlockBasedEvalEnabled: false, theme: 'strudelTheme', fontFamily: 'monospace', fontSize: 18, @@ -76,7 +75,7 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS }); // https://codemirror.net/docs/guide/ -export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo, strudelMirror }) { +export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) { const settings = codemirrorSettings.get(); const initialSettings = Object.keys(compartments).map((key) => compartments[key].of(extensions[key](parseBooleans(settings[key]))), @@ -106,26 +105,11 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo keymap.of([ { key: 'Ctrl-Enter', - run: () => { - // issue with referencing settings, this works more reliably - if (strudelMirror?.isBlockBasedEvalEnabled) { - evalBlock(strudelMirror); - return true; - } else { - return onEvaluate?.(); - } - }, + run: () => onEvaluate?.(), }, { key: 'Alt-Enter', - run: () => { - if (strudelMirror?.isBlockBasedEvalEnabled) { - evalBlock(strudelMirror); - return true; - } else { - return onEvaluate?.(); - } - }, + run: () => onEvaluate?.(), }, { key: 'Ctrl-.', @@ -187,7 +171,6 @@ export class StrudelMirror { this.onDraw = onDraw || this.draw; this.id = id || s4(); this.solo = solo; - this.isBlockBasedEvalEnabled = false; // Will be updated via updateSettings() this.drawer = new Drawer((haps, time, _, painters) => { const currentFrame = haps.filter((hap) => hap.isActive(time)); @@ -218,28 +201,20 @@ export class StrudelMirror { cleanupDraw(true, id); } }, - beforeEval: async ({ blockBased } = {}) => { - // Only clean up all drawings for full evaluation - // Block-based eval should preserve animations (like .scope()) from other blocks - if (!blockBased) { - cleanupDraw(true, id); - } + beforeEval: async () => { + cleanupDraw(true, id); await this.prebaked; await replOptions?.beforeEval?.(); }, afterEval: (options) => { // remember for when highlighting is toggled on - this.miniLocations = options.meta?.miniLocations || []; + this.miniLocations = options.meta?.miniLocations; this.widgets = options.meta?.widgets; - const sliders = this.widgets.filter((w) => w.type === 'slider'); + updateSliderWidgets(this.editor, sliders); const widgets = this.widgets.filter((w) => w.type !== 'slider'); - // range-aware update for block-based evaluation - const range = options.range && options.range.length >= 2 ? options.range : null; - - updateSliderWidgets(this.editor, sliders, range); - updateWidgets(this.editor, widgets, range); - updateMiniLocations(this.editor, this.miniLocations, range); + updateWidgets(this.editor, widgets); + updateMiniLocations(this.editor, this.miniLocations); replOptions?.afterEval?.(options); // if no painters are set (.onPaint was not called), then we only need // the present moment (for highlighting) @@ -247,14 +222,8 @@ export class StrudelMirror { this.drawer.setDrawTime(drawTime); // invalidate drawer after we've set the appropriate drawTime this.drawer.invalidate(this.repl.scheduler); - - // Clean up draw context if a non-inline widget was removed - if (options.widgetRemoved) { - cleanupDrawContext(id); - } }, }); - this.cleanupDrawContext = () => cleanupDrawContext(id); this.editor = initEditor({ root, initialCode, @@ -267,9 +236,7 @@ export class StrudelMirror { onEvaluate: () => this.evaluate(), onStop: () => this.stop(), mondo: replOptions.mondo, - strudelMirror: this, }); - const cmEditor = this.root.querySelector('.cm-editor'); if (cmEditor) { this.root.style.display = 'block'; @@ -339,9 +306,8 @@ export class StrudelMirror { this.flash(); await this.repl.evaluate(this.code, autostart); } - async stop() { - this.repl.stop(); + this.repl.scheduler.stop(); } // Listen for global stop requests (e.g., from Vim :q) @@ -360,8 +326,8 @@ export class StrudelMirror { this.evaluate(); } } - flash(ms, range) { - flash(this.editor, ms, range); + flash(ms) { + flash(this.editor, ms); } highlight(haps, time) { highlightMiniLocations(this.editor, time, haps); @@ -393,10 +359,6 @@ export class StrudelMirror { setLineWrappingEnabled(enabled) { this.reconfigureExtension('isLineWrappingEnabled', enabled); } - - setBlockBasedEvalEnabled(enabled) { - this.reconfigureExtension('isBlockBasedEvalEnabled', enabled); - } setBracketMatchingEnabled(enabled) { this.reconfigureExtension('isBracketMatchingEnabled', enabled); } @@ -418,10 +380,6 @@ export class StrudelMirror { for (let key in extensions) { this.reconfigureExtension(key, settings[key]); } - // Update block-based eval setting on the instance - if (settings.isBlockBasedEvalEnabled !== undefined) { - this.isBlockBasedEvalEnabled = parseBooleans(settings.isBlockBasedEvalEnabled); - } const updated = { ...codemirrorSettings.get(), ...settings }; codemirrorSettings.set(updated); } @@ -443,16 +401,6 @@ export class StrudelMirror { }; this.editor.dispatch({ changes }); } - // used for debugging but could serve other purposes - getActiveWidgets() { - return getActiveWidgets(this.editor); - } - getSliderWidgets() { - return getSliderWidgets(this.editor); - } - getMiniLocations() { - return this.miniLocations; - } clear() { this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl); this.onEvaluateRequest && document.removeEventListener('repl-evaluate', this.onEvaluateRequest); diff --git a/packages/codemirror/flash.mjs b/packages/codemirror/flash.mjs index 7b2473506..243500bff 100644 --- a/packages/codemirror/flash.mjs +++ b/packages/codemirror/flash.mjs @@ -14,8 +14,7 @@ export const flashField = StateField.define({ const mark = Decoration.mark({ attributes: { style: `background-color: rgba(255,255,255, .4); filter: invert(10%)` }, }); - const range = e.value.range || { from: 0, to: tr.newDoc.length }; - flash = Decoration.set([mark.range(range.from, range.to)]); + flash = Decoration.set([mark.range(0, tr.newDoc.length)]); } else { flash = Decoration.set([]); } @@ -30,9 +29,8 @@ export const flashField = StateField.define({ provide: (f) => EditorView.decorations.from(f), }); -export const flash = (view, ms = 200, range) => { - const flashData = range ? { range } : true; - view.dispatch({ effects: setFlash.of(flashData) }); +export const flash = (view, ms = 200) => { + view.dispatch({ effects: setFlash.of(true) }); setTimeout(() => { view.dispatch({ effects: setFlash.of(false) }); }, ms); diff --git a/packages/codemirror/highlight.mjs b/packages/codemirror/highlight.mjs index b37390ef7..f9f977c6b 100644 --- a/packages/codemirror/highlight.mjs +++ b/packages/codemirror/highlight.mjs @@ -3,9 +3,8 @@ import { Decoration, EditorView } from '@codemirror/view'; export const setMiniLocations = StateEffect.define(); export const showMiniLocations = StateEffect.define(); -export const displayMiniLocations = StateEffect.define(); -export const updateMiniLocations = (view, locations, range = null) => { - view.dispatch({ effects: setMiniLocations.of({ locations, range }) }); +export const updateMiniLocations = (view, locations) => { + view.dispatch({ effects: setMiniLocations.of(locations) }); }; export const highlightMiniLocations = (view, atTime, haps) => { view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) }); @@ -22,54 +21,23 @@ const miniLocations = StateField.define({ for (let e of tr.effects) { if (e.is(setMiniLocations)) { - //block-based eval case - if (e.value.range) { - const stateMiniLocations = getMiniLocationsFromDecorations(locations); - - const normalized = e.value.locations - .filter(([from]) => from < tr.newDoc.length) - .map(([from, to]) => [from, Math.min(to, tr.newDoc.length)]); - - const newIds = new Set(normalized.map((r) => r.join(':'))); - - const marks = normalized.map((range) => { - const id = range.join(':'); - return Decoration.mark({ - id, - // this green is only to verify that the decoration moves when the document is edited - // it will be removed later, so the mark is not visible by default - attributes: { style: `background-color: #00CA2880` }, - }).range(...range); // -> Decoration - }); - - const previousMarks = stateMiniLocations - .filter(({ id }) => !newIds.has(id)) - .map(({ from, to, id }) => + // this is called on eval, with the mini locations obtained from the transpiler + // codemirror will automatically remap the marks when the document is edited + // create a mark for each mini location, adding the range to the spec to find it later + const marks = e.value + .filter(([from]) => from < tr.newDoc.length) + .map(([from, to]) => [from, Math.min(to, tr.newDoc.length)]) + .map( + (range) => Decoration.mark({ - id, + id: range.join(':'), + // this green is only to verify that the decoration moves when the document is edited + // it will be removed later, so the mark is not visible by default attributes: { style: `background-color: #00CA2880` }, - }).range(from, to), - ); + }).range(...range), // -> Decoration + ); - locations = Decoration.set(previousMarks.concat(marks), true); // -> DecorationSet === RangeSet - } else { - // this is called on eval, with the mini locations obtained from the transpiler - // codemirror will automatically remap the marks when the document is edited - // create a mark for each mini location, adding the range to the spec to find it later - const marks = e.value.locations - .filter(([from]) => from < tr.newDoc.length) - .map(([from, to]) => [from, Math.min(to, tr.newDoc.length)]) - .map( - (range) => - Decoration.mark({ - id: range.join(':'), - // this green is only to verify that the decoration moves when the document is edited - // it will be removed later, so the mark is not visible by default - attributes: { style: `background-color: #00CA2880` }, - }).range(...range), // -> Decoration - ); - locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet - } + locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet } } @@ -107,83 +75,12 @@ const visibleMiniLocations = StateField.define({ }, }); -const displayMiniLocationsState = StateField.define({ - create() { - return true; // default to showing miniLocations - }, - update(display, tr) { - for (let e of tr.effects) { - if (e.is(displayMiniLocations)) { - display = e.value; - } - } - return display; - }, -}); - // // Derive the set of decorations from the miniLocations and visibleLocations -const miniLocationHighlights = EditorView.decorations.compute( - [miniLocations, visibleMiniLocations, displayMiniLocationsState], - (state) => { - // Check if miniLocations display is disabled - const shouldDisplay = state.field(displayMiniLocationsState); - if (!shouldDisplay) { - return Decoration.none; // Return empty decorations if display is disabled - } +const miniLocationHighlights = EditorView.decorations.compute([miniLocations, visibleMiniLocations], (state) => { + const iterator = state.field(miniLocations).iter(); + const { haps } = state.field(visibleMiniLocations); + const builder = new RangeSetBuilder(); - const iterator = state.field(miniLocations).iter(); - const { haps } = state.field(visibleMiniLocations); - const builder = new RangeSetBuilder(); - - while (iterator.value) { - const { - from, - to, - value: { - spec: { id }, - }, - } = iterator; - - if (haps.has(id)) { - const hap = haps.get(id); - const color = hap.value?.color ?? 'var(--foreground)'; - const style = hap.value?.markcss || `outline: solid 2px ${color}`; - // Get explicit channels for color values - /* - const swatch = document.createElement('div'); - swatch.style.color = color; - document.body.appendChild(swatch); - let channels = getComputedStyle(swatch) - .color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/) - .slice(1) - .map((c) => parseFloat(c || 1)); - document.body.removeChild(swatch); - - // Get percentage of event - const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration; - channels[3] *= percent; - */ - - builder.add( - from, - to, - Decoration.mark({ - // attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` }, - attributes: { style }, - }), - ); - } - - iterator.next(); - } - - return builder.finish(); - }, -); - -const getMiniLocationsFromDecorations = (decorations) => { - const iterator = decorations.iter(); - const miniLocationsArray = []; while (iterator.value) { const { from, @@ -192,48 +89,50 @@ const getMiniLocationsFromDecorations = (decorations) => { spec: { id }, }, } = iterator; - miniLocationsArray.push({ - from, - to, - id, - }); + + if (haps.has(id)) { + const hap = haps.get(id); + const color = hap.value?.color ?? 'var(--foreground)'; + const style = hap.value?.markcss || `outline: solid 2px ${color}`; + // Get explicit channels for color values + /* + const swatch = document.createElement('div'); + swatch.style.color = color; + document.body.appendChild(swatch); + let channels = getComputedStyle(swatch) + .color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/) + .slice(1) + .map((c) => parseFloat(c || 1)); + document.body.removeChild(swatch); + + // Get percentage of event + const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration; + channels[3] *= percent; + */ + + builder.add( + from, + to, + Decoration.mark({ + // attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` }, + attributes: { style }, + }), + ); + } + iterator.next(); } - return miniLocationsArray; -}; -export const getMiniLocations = (state) => { - const decorations = state.field(miniLocations); - return getMiniLocationsFromDecorations(decorations); -}; + return builder.finish(); +}); -export const getActiveMiniLocations = (state) => { - const miniLocations = getMiniLocations(state); - const { haps } = state.field(visibleMiniLocations); - - const activeMiniLocations = miniLocations.filter((location) => haps.has(location.id)); - return activeMiniLocations; -}; - -export const highlightExtension = [ - miniLocations, - visibleMiniLocations, - displayMiniLocationsState, - miniLocationHighlights, -]; +export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights]; export const isPatternHighlightingEnabled = (on, config) => { - // NOTE: - // Modified this function to always return the highlightExtension, and instead just toggle whether or not the miniLocations are displayed. - // This is because block based evaluation only updates regions of miniLocations, and those updates need to be kept track of constantly. - // The setTimeout was also removed because it conflicted with the range-specific updates required by - // block based evaluation. - // Not sure if this is the best approach, but for block based eval I can't think of a better way to do it. - - if (config) { - // Toggle the display state for miniLocations - config.editor.dispatch({ effects: displayMiniLocations.of(on) }); - } - - return Prec.highest(highlightExtension); + on && + config && + setTimeout(() => { + updateMiniLocations(config.editor, config.miniLocations); + }, 100); + return on ? Prec.highest(highlightExtension) : []; }; diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index be0bad975..cc224a6cc 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -2,11 +2,11 @@ import { ref, pure } from '@strudel/core'; import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view'; import { StateEffect } from '@codemirror/state'; -// Global state storage for all widget types export let sliderValues = {}; +const getSliderID = (from) => `slider_${from}`; export class SliderWidget extends WidgetType { - constructor(value, min, max, from, to, step, view, id) { + constructor(value, min, max, from, to, step, view) { super(); this.value = value; this.min = min; @@ -16,21 +16,10 @@ export class SliderWidget extends WidgetType { this.to = to; this.step = step; this.view = view; - this.id = id || `${from}:${to}`; // Range-based ID for stability } - eq(other) { - if (!(other instanceof SliderWidget)) { - return false; - } - return ( - this.id === other.id && - this.from === other.from && - this.to === other.to && - this.value === other.value && - this.min === other.min && - this.max === other.max - ); + eq() { + return false; } toDOM() { @@ -49,7 +38,6 @@ export class SliderWidget extends WidgetType { slider.from = this.from; slider.originalFrom = this.originalFrom; slider.to = this.to; - slider.id = this.id; // Store range-based ID in DOM element slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)'; this.slider = slider; slider.addEventListener('input', (e) => { @@ -61,7 +49,7 @@ export class SliderWidget extends WidgetType { slider.originalValue = insert; slider.value = insert; this.view.dispatch({ changes: change }); - const id = slider.id; // Use range-based ID + const id = getSliderID(slider.originalFrom); // matches id generated in transpiler window.postMessage({ type: 'cm-slider', value: Number(next), id }); }); return wrap; @@ -74,60 +62,19 @@ export class SliderWidget extends WidgetType { export const setSliderWidgets = StateEffect.define(); -export const setSliderWidgetsInRange = StateEffect.define(); - -export const updateSliderWidgets = (view, widgets, range = null) => { - if (range) { - // range argument passed for block-based evaluation - view.dispatch({ effects: setSliderWidgetsInRange.of({ widgets, range }) }); - } else { - view.dispatch({ effects: setSliderWidgets.of(widgets) }); - } +export const updateSliderWidgets = (view, widgets) => { + view.dispatch({ effects: setSliderWidgets.of(widgets) }); }; function getSliders(widgetConfigs, view) { - return ( - widgetConfigs - .filter((w) => w.type === 'slider') - // Deduplicate sliders that might appear multiple times (e.g., during paste operations) - .filter((slider, index, self) => index === self.findIndex((s) => s.from === slider.from && s.to === slider.to)) - .sort((a, b) => a.from - b.from) - .map(({ from, to, value, min, max, step, id }) => { - return Decoration.widget({ - widget: new SliderWidget(value, min, max, from, to, step, view, id), - side: 0, - }).range(from /* , to */); - }) - ); -} - -export function getSliderWidgets(view) { - if (!view || !view.state) { - return []; - } - - const sliderPluginInstance = view.plugin(sliderPlugin); - if (!sliderPluginInstance || !sliderPluginInstance.decorations) { - return []; - } - - const sliderWidgets = []; - - sliderPluginInstance.decorations.between(0, view.state.doc.length, (from, to, decoration) => { - if (decoration.widget instanceof SliderWidget) { - sliderWidgets.push({ - type: 'slider', - from: decoration.widget.from, - to: decoration.widget.to, - value: decoration.widget.value, - min: decoration.widget.min, - max: decoration.widget.max, - step: decoration.widget.step, - }); - } - }); - - return sliderWidgets; + return widgetConfigs + .filter((w) => w.type === 'slider') + .map(({ from, to, value, min, max, step }) => { + return Decoration.widget({ + widget: new SliderWidget(value, min, max, from, to, step, view), + side: 0, + }).range(from /* , to */); + }); } export const sliderPlugin = ViewPlugin.fromClass( @@ -154,42 +101,7 @@ export const sliderPlugin = ViewPlugin.fromClass( } } for (let e of tr.effects) { - if (e.is(setSliderWidgetsInRange)) { - // Block-aware slider update logic - const { widgets, range } = e.value; - const [rangeStart, rangeEnd] = range; - - // Get existing slider widgets that should be preserved - const existingSliders = []; - this.decorations.between(0, update.view.state.doc.length, (from, to, decoration) => { - if (decoration.widget instanceof SliderWidget) { - // Preserve sliders outside the evaluation range - // Use strict > for rangeEnd because when code is deleted, slider positions - // map to the deletion boundary (rangeEnd), and those should be removed, not preserved - if (from < rangeStart || from > rangeEnd) { - existingSliders.push({ - from, - to, - value: decoration.widget.value, - min: decoration.widget.min, - max: decoration.widget.max, - step: decoration.widget.step, - id: decoration.widget.id || `${from}:${to}`, - type: 'slider', - }); - } - } - }); - - // Merge preserved sliders with new widgets - const mergedWidgets = [...existingSliders, ...widgets] - .filter( - (slider, index, self) => index === self.findIndex((s) => s.type === 'slider' && s.id === slider.id), - ) - .sort((a, b) => a.from - b.from); - - this.decorations = Decoration.set(getSliders(mergedWidgets, update.view)); - } else if (e.is(setSliderWidgets)) { + if (e.is(setSliderWidgets)) { this.decorations = Decoration.set(getSliders(e.value, update.view)); } } @@ -220,7 +132,6 @@ export let sliderWithID = (id, value, min, max) => { sliderValues[id] = value; // sync state at eval time (code -> state) return ref(() => sliderValues[id]); // use state at query time }; - // update state when sliders are moved if (typeof window !== 'undefined') { window.addEventListener('message', (e) => { @@ -229,7 +140,7 @@ if (typeof window !== 'undefined') { // update state when slider is moved sliderValues[e.data.id] = e.data.value; } else { - console.error(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`); + console.warn(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`); } } }); diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs index 6c40d6733..42d3b1512 100644 --- a/packages/codemirror/widget.mjs +++ b/packages/codemirror/widget.mjs @@ -1,111 +1,55 @@ import { StateEffect, StateField } from '@codemirror/state'; -import { Decoration, EditorView, WidgetType, ViewPlugin } from '@codemirror/view'; +import { Decoration, EditorView, WidgetType } from '@codemirror/view'; import { getWidgetID, registerWidgetType } from '@strudel/transpiler'; import { Pattern } from '@strudel/core'; -export const setWidgets = StateEffect.define(); +export const addWidget = StateEffect.define({ + map: ({ from, to }, change) => { + return { from: change.mapPos(from), to: change.mapPos(to) }; + }, +}); -export const setWidgetsInRange = StateEffect.define(); - -export const updateWidgets = (view, widgets, range = null) => { - if (range) { - // range argument passed for block-based evaluation - view.dispatch({ effects: setWidgetsInRange.of({ widgets, range }) }); - } else { - view.dispatch({ effects: setWidgets.of(widgets) }); - } +export const updateWidgets = (view, widgets) => { + view.dispatch({ effects: addWidget.of(widgets) }); }; -function getWidgets(widgetConfigs, view) { - const filtered = widgetConfigs - // Filter to widget configs only (exclude sliders) - .filter((w) => w && w.type && w.type !== 'slider') - // Deduplicate widgets by ID, matching slider behavior for stable widget identity - .filter((widget, index, self) => index === self.findIndex((w) => w.type === widget.type && w.id === widget.id)); - - // Filter out widgets whose range is encompassed by another widget - // const nonEncompassed = filterEncompassedWidgets(filtered); - - return filtered - .sort((a, b) => (a.to || 0) - (b.to || 0)) - .map((widgetConfig) => { - try { +function getWidgets(widgetConfigs) { + return ( + widgetConfigs + // codemirror throws an error if we don't sort + .sort((a, b) => a.to - b.to) + .map((widgetConfig) => { return Decoration.widget({ - widget: new BlockWidget(widgetConfig, view), + widget: new BlockWidget(widgetConfig), side: 0, - }).range(widgetConfig.to || widgetConfig.from || 0); - } catch (error) { - console.error('error creating widget', error); - return null; - } - }) - .filter(Boolean); // Remove any null results from failed creations + block: true, + }).range(widgetConfig.to); + }) + ); } -export const widgetPlugin = ViewPlugin.fromClass( - class { - decorations; //: DecorationSet - - constructor(view /* : EditorView */) { - this.decorations = Decoration.set([]); - } - - update(update /* : ViewUpdate */) { - update.transactions.forEach((tr) => { - if (tr.docChanged) { - this.decorations = this.decorations.map(tr.changes); - const iterator = this.decorations.iter(); - // Apply changes to iterator.from and iterator.to if docChanged - while (iterator.value) { - // when the widgets are moved, we need to tell the dom node the current position - // this is important because the widget functions have to work with the dom node - if (iterator.value?.widget instanceof BlockWidget) { - iterator.value.widget.from = iterator.from; - iterator.value.widget.to = iterator.to; - } - iterator.next(); - } - } - for (let e of tr.effects) { - if (e.is(setWidgetsInRange)) { - // Block-aware widget update logic - const { widgets, range } = e.value; - const [rangeStart, rangeEnd] = range; - - // Get existing widget widgets that should be preserved - const existingWidgets = []; - this.decorations.between(0, update.view.state.doc.length, (from, to, decoration) => { - if (decoration.widget instanceof BlockWidget) { - // Preserve widgets outside the evaluation range - // Use strict > for rangeEnd because when code is deleted, widget positions - // map to the deletion boundary (rangeEnd), and those should be removed, not preserved - if (from < rangeStart || from > rangeEnd) { - existingWidgets.push({ - from: decoration.widget.from, - to: decoration.widget.to, - type: decoration.widget.type, - index: decoration.widget.index, - id: decoration.widget.id, - }); - } - } +const widgetField = StateField.define( + /* */ { + create() { + return Decoration.none; + }, + update(widgets, tr) { + widgets = widgets.map(tr.changes); + for (let e of tr.effects) { + if (e.is(addWidget)) { + try { + widgets = widgets.update({ + filter: () => false, + add: getWidgets(e.value), }); - - // Merge preserved widgets with new widgets, deduplicating by ID - const mergedWidgets = [...existingWidgets, ...widgets].filter( - (widget, index, self) => index === self.findIndex((w) => w.type === widget.type && w.id === widget.id), - ); - - this.decorations = Decoration.set(getWidgets(mergedWidgets, update.view)); - } else if (e.is(setWidgets)) { - this.decorations = Decoration.set(getWidgets(e.value, update.view)); + } catch (error) { + console.log('err', error); } } - }); - } - }, - { - decorations: (v) => v.decorations, + } + return widgets; + }, + provide: (f) => EditorView.decorations.from(f), }, ); @@ -116,116 +60,24 @@ export function setWidget(id, el) { } export class BlockWidget extends WidgetType { - constructor(widgetConfig, view) { + constructor(widgetConfig) { super(); - - // Graceful handling of invalid configs like sliders - if (!widgetConfig || typeof widgetConfig !== 'object') { - widgetConfig = { type: 'unknown', from: 0, to: 0 }; - } - - this.from = widgetConfig.from || 0; - this.originalFrom = widgetConfig.from || 0; - this.to = widgetConfig.to || this.from; - this.originalTo = widgetConfig.to || this.from; - this.type = widgetConfig.type || 'unknown'; - this.index = widgetConfig.index || 0; - this.view = view; - - // Use range-based ID for stability, similar to sliders - this.id = widgetConfig.id || getWidgetID?.(widgetConfig); this.widgetConfig = widgetConfig; } - - eq(other) { - if (!(other instanceof BlockWidget)) { - return false; - } - return ( - this.id === other.id && - this.from === other.from && - this.to === other.to && - this.type === other.type && - this.index === other.index - ); + eq() { + return true; } - toDOM() { - let wrap = document.createElement('span'); - wrap.setAttribute('aria-hidden', 'true'); - wrap.className = 'cm-widget-container'; - - let el = widgetElements[this.id]; - if (el) { - // Ensure the element has the correct ID - el.id = this.id; - wrap.appendChild(el); - } else { - // Create a placeholder element if the widget element doesn't exist - // This prevents CodeMirror errors when widget is missing - const placeholder = document.createElement('span'); - placeholder.setAttribute('aria-hidden', 'true'); - placeholder.className = 'cm-widget-placeholder'; - placeholder.style.cssText = 'display: none;'; // Hide placeholder - placeholder.id = this.id; - wrap.appendChild(placeholder); - } - - return wrap; + const id = getWidgetID(this.widgetConfig); + const el = widgetElements[id]; + return el; } - ignoreEvent(e) { return true; } } -export function getActiveWidgets(view) { - if (!view || !view.state) { - return []; - } - - const widgetPluginInstance = view.plugin(widgetPlugin); - if (!widgetPluginInstance || !widgetPluginInstance.decorations) { - return []; - } - - const widgets = []; - - widgetPluginInstance.decorations.between(0, view.state.doc.length, (from, to, decoration) => { - if (decoration.widget instanceof BlockWidget) { - widgets.push({ - type: decoration.widget.type, - from: decoration.widget.from, - to: decoration.widget.to, - index: decoration.widget.index, - id: decoration.widget.id, - }); - } - }); - - return widgets; -} - -export function getAllWidgetIds(view) { - if (!view || !view.state) { - return []; - } - - const widgetPluginInstance = view.plugin(widgetPlugin); - if (!widgetPluginInstance || !widgetPluginInstance.decorations) { - return []; - } - - const widgetIds = []; - - widgetPluginInstance.decorations.between(0, view.state.doc.length, (from, to, decoration) => { - if (decoration.widget instanceof BlockWidget) { - widgetIds.push(decoration.widget.id); - } - }); - - return widgetIds; -} +export const widgetPlugin = [widgetField]; // widget implementer API to create a new widget type export function registerWidget(type, fn) { diff --git a/packages/core/evaluate.mjs b/packages/core/evaluate.mjs index bb7b46648..0599d8662 100644 --- a/packages/core/evaluate.mjs +++ b/packages/core/evaluate.mjs @@ -5,34 +5,6 @@ This program is free software: you can redistribute it and/or modify it under th */ export const strudelScope = {}; -// Make strudelScope available globally so transpiled code can access it -globalThis.strudelScope = strudelScope; - -// Track user-defined keys (from block-based eval) so we can clear them without removing strudel functions -export const userDefinedKeys = new Set(); -globalThis.userDefinedKeys = userDefinedKeys; - -/** - * Clears all user-defined variables and functions from the scope. - * This removes variables created during block-based evaluation. - * @name clearScope - * @example - * // After defining variables in blocks: - * // let myVar = 5 - * // function myFunc() { return 10; } - * clearScope() // removes myVar and myFunc from scope - */ -export const clearScope = () => { - for (const key of userDefinedKeys) { - delete strudelScope[key]; - delete globalThis[key]; - } - userDefinedKeys.clear(); - // Return silence if available (for use in pattern expressions), otherwise undefined - return globalThis.silence; -}; -// Make clearScope available globally -globalThis.clearScope = clearScope; export const evalScope = async (...args) => { const results = await Promise.allSettled(args); diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index c1b539819..bf86ba494 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -38,7 +38,6 @@ export function repl({ pattern: undefined, miniLocations: [], widgets: [], - sliders: [], pending: false, started: false, }; @@ -80,132 +79,11 @@ export function repl({ let allTransform; let eachTransform; - // Block-based evaluation state - let codeBlocks = {}; - let lastActiveVisualizerLabel = null; - // Track which patterns belong to which blocks: { blockRange: [patternKeys] } - let blockPatterns = new Map(); - - // Helper function to collect properties from all code blocks (handles both labeled and anonymous blocks) - function collectFromBlocks(property) { - return Object.entries(codeBlocks).flatMap(([key, block]) => { - if (key === '$') { - // Anonymous blocks are stored as an array of block objects - return Array.isArray(block) ? block.flatMap((b) => b[property] || []) : []; - } - // Labeled blocks are stored as single block objects - return block[property] || []; - }); - } - - // Helper function to process a single labeled block - function processLabeledBlock(labels, i, code, options, meta) { - const label = labels[i]; - const nextLabel = labels[i + 1] || { index: code.length, end: code.length }; - - const labelCode = code.slice(label.index, nextLabel.index); - const labelRange = [label.index + options.range[0], label.end + options.range[0]]; - - // Calculate the full block range (from label start to next label start) - const blockStart = label.index + options.range[0]; - const blockEnd = nextLabel.index + options.range[0]; - - const blockWidgets = (meta?.widgets || []).filter((widget) => { - const widgetPos = widget.from ?? widget.index ?? 0; - return widgetPos >= blockStart && widgetPos < blockEnd; - }); - - const blockSliders = (meta?.sliders || []).filter((slider) => { - const sliderPos = slider.from ?? slider.index ?? 0; - return sliderPos >= blockStart && sliderPos < blockEnd; - }); - - const blockMiniLocations = (meta?.miniLocations || []).filter((loc) => { - // const locStart = loc.start ?? loc.from ?? 0; - // mini locations can be either [start, end] arrays or objects with start/from - const locStart = Array.isArray(loc) ? loc[0] : (loc.start ?? loc.from ?? 0); - return locStart >= blockStart && locStart < blockEnd; - }); - - handleSingleLabelBlock( - label, - labelCode, - { ...options, range: labelRange }, - { widgets: blockWidgets, sliders: blockSliders, miniLocations: blockMiniLocations }, - ); - } - - // helper - function cleanupConflictingRanges(codeBlocks, currentKey, newRange) { - for (const [existingKey, existingBlock] of Object.entries(codeBlocks)) { - if (existingKey === currentKey) continue; - if (!existingBlock.range) continue; - - const [existingStart, existingEnd] = existingBlock.range; - const [newStart, newEnd] = newRange; - - // If ranges overlap (not just touch), remove the stale block - if (!(newEnd <= existingStart || newStart >= existingEnd)) { - delete codeBlocks[existingKey]; - } - } - } - - // helper - function handleSingleLabelBlock(label, code, options, meta) { - // Detect if this block contains a non-inline widget - // The activeVisualizer is now provided by the transpiler for all labels - const activeVisualizer = label.activeVisualizer || null; - - if (activeVisualizer !== null) { - lastActiveVisualizerLabel = label.name; - } - - // Store the entire code block under the label name - codeBlocks[label.name] = { - code: code, - range: options.range, - labels: [label.name], - miniLocations: meta?.miniLocations || [], - widgets: meta?.widgets || [], - sliders: meta?.sliders || [], - activeVisualizer: activeVisualizer, // Store the widget type if present, null otherwise - }; - - // Clean up any blocks with conflicting ranges (including declaration blocks) - cleanupConflictingRanges(codeBlocks, label.name, options.range); - } - - // helper - // These blocks return silence but may contain mini notation strings that need highlighting - function handleDeclarationBlock(code, options, meta) { - const range = options.range || []; - if (range.length < 2) return; - - const blockKey = `_decl:${range[0]}:${range[1]}`; - - codeBlocks[blockKey] = { - code: code, - range: range, - labels: [], - miniLocations: meta?.miniLocations || [], - widgets: meta?.widgets || [], - sliders: meta?.sliders || [], - activeVisualizer: null, - }; - - // Clean up any overlapping declaration blocks - cleanupConflictingRanges(codeBlocks, blockKey, range); - } - const hush = function () { pPatterns = {}; anonymousIndex = 0; allTransform = undefined; eachTransform = undefined; - codeBlocks = {}; - blockPatterns.clear(); - lastActiveVisualizerLabel = null; // Reset 'all' visualizer tracking return silence; }; @@ -225,60 +103,7 @@ export function repl({ }; setTime(() => scheduler.now()); // TODO: refactor? - // Helper function to apply pattern transformations (solo, each, all) - // this should be abstracted more - function applyPatternTransforms(pattern) { - const allPatterns = Object.values(pPatterns); - - if (allPatterns.length) { - let patterns = []; - let soloActive = false; - for (const [key, value] of Object.entries(pPatterns)) { - // handle soloed patterns ex: S$: s("bd!4") - const isSolod = key.length > 1 && key.startsWith('S'); - if (isSolod && soloActive === false) { - // first time we see a soloed pattern, clear existing patterns - patterns = []; - soloActive = true; - } - if (!soloActive || (soloActive && isSolod)) { - const valWithState = value.withState((state) => state.setControls({ id: key })); - patterns.push(valWithState); - } - } - if (eachTransform) { - // Explicit lambda so only element (not index and array) are passed - patterns = patterns.map((x) => eachTransform(x)); - } - pattern = stack(...patterns); - } else if (eachTransform) { - pattern = eachTransform(pattern); - } - if (allTransforms.length) { - for (const transform of allTransforms) { - pattern = transform(pattern); - } - } - - if (!isPattern(pattern)) { - pattern = silence; - } - - return pattern; - } - - const stop = () => { - codeBlocks = {}; - blockPatterns.clear(); - pPatterns = {}; - lastActiveVisualizerLabel = null; // Reset 'all' visualizer tracking - updateState({ - miniLocations: [], - widgets: [], - sliders: [], - }); - scheduler.stop(); - }; + const stop = () => scheduler.stop(); const start = () => scheduler.start(); const pause = () => scheduler.pause(); const toggle = () => scheduler.toggle(); @@ -392,7 +217,7 @@ export function repl({ }); }; - const evaluate = async (code, autostart = true) => { + const evaluate = async (code, autostart = true, shouldHush = true) => { if (!code) { throw new Error('no code to evaluate'); } @@ -400,146 +225,59 @@ export function repl({ updateState({ code, pending: true }); await injectPatternMethods(); setTime(() => scheduler.now()); // TODO: refactor? - await beforeEval?.({ code, blockBased: false }); + await beforeEval?.({ code }); allTransforms = []; // reset all transforms - - codeBlocks = {}; - hush(); + shouldHush && hush(); if (mondo) { code = `mondolang\`${code}\``; } - let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); - - pattern = applyPatternTransforms(pattern); - - logger(`[eval] code updated`); - pattern = await setPattern(pattern, autostart); - updateState({ - miniLocations: meta?.miniLocations || [], - widgets: meta?.widgets || [], - sliders: meta?.sliders || [], - activeCode: code, - pattern, - evalError: undefined, - schedulerError: undefined, - pending: false, - }); - - afterEval?.({ code, pattern, meta, range: undefined, widgetRemoved: false }); - return pattern; - } catch (err) { - logger(`[eval] error: ${err.message}`, 'error'); - console.error(err); - updateState({ evalError: err, pending: false }); - onEvalError?.(err); - } - }; - - const evaluateBlock = async (code, autostart = true, options = {}) => { - if (!code) { - throw new Error('no code to evaluate'); - } - try { - updateState({ code, pending: true }); - await injectPatternMethods(); - setTime(() => scheduler.now()); // TODO: refactor? - await beforeEval?.({ code, blockBased: true }); - allTransforms = []; // reset all transforms - - const transpilerOptionsWithBlock = { - ...transpilerOptions, - blockBased: true, - range: options.range || [], - }; - - if (mondo) { - code = `mondolang\`${code}\``; - } - - let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptionsWithBlock); - - // Track activeVisualizer cleanup: check if any block's visualizer was removed - let widgetRemoved = false; - - const labels = meta.labels || []; - - // Check for anonymous labels (labels starting with '$') - const hasAnonymousLabel = labels.some((label) => label.name.startsWith('$')); - - // Store code blocks in dictionary using labels as keys - if (hasAnonymousLabel) { - // variable/function declarations that don't return patterns are allowed, - // but anonymous pattern blocks pose an issue for block-based evaluation - // if an anonymous pattern is evaluated multiple times it will just stack and get louder and louder - - // it's very common for users to write code prefixed with '$' - // but to modify and override existing patterns, the patterns must be labeled, - // otherwise we'll have no idea of which pattern is being overridden - - // (we probably need to update the docs on this) - // we could easily enable it, but it would confuse a lot of people - - throw new Error( - 'anonymous labels disabled for block based evaluation (see https://strudel.cc/blog/#label-notation)', - ); - } else if (labels.length > 0) { - for (let i = 0; i < labels.length; i++) { - // processing transpiler output instead of code is simply to avoid - // extra regex in detecting whether or not an inline widget has been commented out - processLabeledBlock(labels, i, meta.output, options, meta); - } - } else { - // Declaration block (variable/function that returns silence) - // Store it so its miniLocations are preserved for highlighting patterns stored in variables - handleDeclarationBlock(code, options, meta); - } - - meta.miniLocations = collectFromBlocks('miniLocations'); - meta.widgets = collectFromBlocks('widgets'); - meta.sliders = collectFromBlocks('sliders'); - - // Track activeVisualizer cleanup: check if any block's visualizer was removed - const blocksToUpdate = labels.map((label) => label.name); - - // this is the hackiest bit - for (const [key, block] of Object.entries(codeBlocks)) { - if (blocksToUpdate.includes(key)) { - // This block was just updated - if (block.activeVisualizer !== null) { - // Block now has a visualizer, update tracking - lastActiveVisualizerLabel = key; - } else if (lastActiveVisualizerLabel === key) { - // This block lost its visualizer, trigger cleanup - widgetRemoved = true; - lastActiveVisualizerLabel = null; + if (Object.keys(pPatterns).length) { + let patterns = []; + let soloActive = false; + for (const [key, value] of Object.entries(pPatterns)) { + // handle soloed patterns ex: S$: s("bd!4") + const isSolod = key.length > 1 && key.startsWith('S'); + if (isSolod && soloActive === false) { + // first time we see a soloed pattern, clear existing patterns + patterns = []; + soloActive = true; + } + if (!soloActive || (soloActive && isSolod)) { + const valWithState = value.withState((state) => state.setControls({ id: key })); + patterns.push(valWithState); } } + if (eachTransform) { + // Explicit lambda so only element (not index and array) are passed + patterns = patterns.map((x) => eachTransform(x)); + } + pattern = stack(...patterns); + } else if (eachTransform) { + pattern = eachTransform(pattern); + } + if (allTransforms.length) { + for (const transform of allTransforms) { + pattern = transform(pattern); + } } - pPatterns = Object.fromEntries( - Object.entries(pPatterns).filter(([key]) => { - return Object.keys(codeBlocks).includes(key); - }), - ); - - pattern = applyPatternTransforms(pattern); - + if (!isPattern(pattern)) { + pattern = silence; + } logger(`[eval] code updated`); pattern = await setPattern(pattern, autostart); updateState({ miniLocations: meta?.miniLocations || [], widgets: meta?.widgets || [], - sliders: meta?.sliders || [], activeCode: code, pattern, evalError: undefined, schedulerError: undefined, pending: false, }); - - afterEval?.({ code, pattern, meta, range: options.range, widgetRemoved }); + afterEval?.({ code, pattern, meta }); return pattern; } catch (err) { logger(`[eval] error: ${err.message}`, 'error'); @@ -548,9 +286,8 @@ export function repl({ onEvalError?.(err); } }; - const setCode = (code) => updateState({ code }); - return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state }; + return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state }; } export const getTrigger = diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index f915e9116..95f3aed50 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -78,16 +78,6 @@ export const cleanupDraw = (clearScreen = true, id) => { stopAllAnimations(id); }; -export const cleanupDrawContext = (replID) => { - const ctx = getDrawContext(); - ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - - // clear the big canvas context, ignore inline widgets - Object.keys(animationFrames).forEach( - (id) => (!replID || id.startsWith(replID)) && !id.startsWith('_') && stopAnimationFrame(id), - ); -}; - Pattern.prototype.onPaint = function (painter) { return this.withState((state) => { if (!state.controls.painters) { diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 210d40852..45f9dabd4 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -19,14 +19,7 @@ export function registerLanguage(type, config) { } export function transpiler(input, options = {}) { - const { - wrapAsync = false, - addReturn = true, - emitMiniLocations = true, - emitWidgets = true, - blockBased = false, - range = [], - } = options; + const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options; const comments = []; let ast = parse(input, { @@ -38,13 +31,6 @@ export function transpiler(input, options = {}) { const miniDisableRanges = findMiniDisableRanges(comments, input.length); let miniLocations = []; - - // Position offset for block-based evaluation - let nodeOffset = range && range.length > 0 ? range[0] : 0; - - // Track declarations to add to strudelScope for block-based eval - let scopeDeclarations = []; - const collectMiniLocations = (value, node) => { const minilang = languages.get('minilang'); if (minilang) { @@ -57,28 +43,9 @@ export function transpiler(input, options = {}) { } }; let widgets = []; - let sliders = []; - let labels = []; walk(ast, { enter(node, parent /* , prop, index */) { - // Apply position offset for block-based evaluation - if (blockBased && node.start !== undefined) { - node.start = node.start + nodeOffset; - node.end = node.end + nodeOffset; - } - // Collect variable and function declarations for strudelScope (block-based eval) - if (blockBased && parent?.type === 'Program') { - if (node.type === 'VariableDeclaration') { - for (const declarator of node.declarations) { - if (declarator.id?.name) { - scopeDeclarations.push(declarator.id.name); - } - } - } else if (node.type === 'FunctionDeclaration' && node.id?.name) { - scopeDeclarations.push(node.id.name); - } - } if (isLanguageLiteral(node)) { const { name } = node.tag; const language = languages.get(name); @@ -121,29 +88,22 @@ export function transpiler(input, options = {}) { return this.replace(miniWithLocation(value, node)); } if (isSliderFunction(node)) { - const from = node.arguments[0].start + nodeOffset; - const to = node.arguments[0].end + nodeOffset; - const id = `${from}:${to}`; // Range-based ID for stability - - const sliderConfig = { - from, - to, - id, - value: node.arguments[0].raw, // don't use value! - min: node.arguments[1]?.value ?? 0, - max: node.arguments[2]?.value ?? 1, - step: node.arguments[3]?.value, - type: 'slider', - }; - emitWidgets && widgets.push(sliderConfig); - sliders.push(sliderConfig); - return this.replace(sliderWithLocation(node, nodeOffset)); + emitWidgets && + widgets.push({ + from: node.arguments[0].start, + to: node.arguments[0].end, + value: node.arguments[0].raw, // don't use value! + min: node.arguments[1]?.value ?? 0, + max: node.arguments[2]?.value ?? 1, + step: node.arguments[3]?.value, + type: 'slider', + }); + return this.replace(sliderWithLocation(node)); } if (isWidgetMethod(node)) { const type = node.callee.property.name; const index = widgets.filter((w) => w.type === type).length; const widgetConfig = { - from: node.start, to: node.end, index, type, @@ -156,30 +116,8 @@ export function transpiler(input, options = {}) { return this.replace(withAwait(node)); } if (isLabelStatement(node)) { - // Collect label info for block-based evaluation - // Store positions WITHOUT offset so repl can slice the transpiler output correctly - if (blockBased) { - labels.push({ - name: node.label.name, - index: node.start - nodeOffset, - end: node.label.end - nodeOffset, - fullMatch: input.slice(node.start - nodeOffset, node.label.end - nodeOffset), - activeVisualizer: findVisualizerInSubtree(node.body), - }); - } return this.replace(labelToP(node)); } - // Detect all() calls as special labels for block management - // Store positions WITHOUT offset so repl can slice the transpiler output correctly - if (blockBased && isAllCall(node)) { - labels.push({ - name: 'all', - index: node.start - nodeOffset, - end: node.end - nodeOffset, - fullMatch: input.slice(node.start - nodeOffset, node.end - nodeOffset), - activeVisualizer: node.arguments[0] ? findVisualizerInSubtree(node.arguments[0]) : null, - }); - } }, leave(node, parent, prop, index) { @@ -243,33 +181,17 @@ export function transpiler(input, options = {}) { let { body } = ast; - const silenceExpression = { - type: 'ExpressionStatement', - expression: { - type: 'Identifier', - name: 'silence', - }, - }; - if (!body.length) { console.warn('empty body -> fallback to silence'); - body.push(silenceExpression); + body.push({ + type: 'ExpressionStatement', + expression: { + type: 'Identifier', + name: 'silence', + }, + }); } else if (!body?.[body.length - 1]?.expression) { - // Last statement is not an expression (e.g., VariableDeclaration, FunctionDeclaration) - if (blockBased) { - // For block-based eval, add silence as the return value when block ends with declaration - body.push(silenceExpression); - } else { - throw new Error('unexpected ast format without body expression'); - } - } - - // For block-based eval, add scope assignments before the return statement - // This allows variables/functions defined in one block to be used in other blocks - if (blockBased && scopeDeclarations.length > 0) { - const scopeAssignments = scopeDeclarations.flatMap((name) => createScopeAssignment(name)); - // Insert scope assignments before the last statement (which will become the return) - body.splice(body.length - 1, 0, ...scopeAssignments); + throw new Error('unexpected ast format without body expression'); } // add return to last statement @@ -287,7 +209,7 @@ export function transpiler(input, options = {}) { if (!emitMiniLocations) { return { output }; } - return { output, miniLocations, widgets, sliders, labels }; + return { output, miniLocations, widgets }; } function isKabelCall(node) { @@ -494,14 +416,8 @@ function isWidgetMethod(node) { return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name); } -function sliderWithLocation(node, nodeOffset = 0) { - // Apply nodeOffset for block-based evaluation to generate correct range - const from = node.arguments[0].start + nodeOffset; - const to = node.arguments[0].end + nodeOffset; - - // Use range-based ID for stability during block evaluation - const id = `${from}:${to}`; - +function sliderWithLocation(node) { + const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id // add loc as identifier to first argument // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) node.arguments.unshift({ @@ -516,26 +432,14 @@ function sliderWithLocation(node, nodeOffset = 0) { export function getWidgetID(widgetConfig) { // the widget id is used as id for the dom element + as key for eventual resources // for example, for each scope widget, a new analyser + buffer (large) is created - // Update: use range-based ID generation for better stability during block evaluation - // When we have both from and to, use them together for stability - // Otherwise fall back to position-based ID for backward compatibility - let uniqueIdentifier; - if (widgetConfig.from !== undefined && widgetConfig.to !== undefined) { - // Use range for more stable identification - uniqueIdentifier = `${widgetConfig.from}-${widgetConfig.to}`; - } else { - // Fallback to single position (for backward compatibility) - uniqueIdentifier = widgetConfig.to || widgetConfig.from || 0; - } - const baseId = `${widgetConfig.id || ''}_widget_${widgetConfig.type}`; - return `${baseId}_${widgetConfig.index}_${uniqueIdentifier}`; + // that means, if we use the index index of line position as id, less garbage is generated + // return `widget_${widgetConfig.to}`; // more gargabe + //return `widget_${widgetConfig.index}_${widgetConfig.to}`; // also more garbage + return `${widgetConfig.id || ''}_widget_${widgetConfig.type}_${widgetConfig.index}`; // less garbage } function widgetWithLocation(node, widgetConfig) { const id = getWidgetID(widgetConfig); - // Store the unique ID back into the config so it's available for widget management - // This is critical for block-based evaluation to match existing widgets with new ones - widgetConfig.id = id; // add loc as identifier to first argument // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) node.arguments.unshift({ @@ -550,10 +454,6 @@ function isBareSamplesCall(node, parent) { return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression'; } -function isAllCall(node) { - return node.type === 'CallExpression' && node.callee.name === 'all'; -} - function withAwait(node) { return { type: 'AwaitExpression', @@ -654,128 +554,6 @@ function languageWithLocation(name, value, offset) { }; } -// List of non-inline widgets that need cleanup -// These are Pattern.prototype methods that create persistent visualizations -// (should be repalced by a function call producing an actual list of registered widgets) -const nonInlineWidgets = ['punchcard', 'spiral', 'scope', 'pitchwheel', 'spectrum', 'pianoroll', 'wordfall']; - -function isVisualizerCall(node) { - if ( - node.type === 'CallExpression' && - node.callee.type === 'MemberExpression' && - nonInlineWidgets.includes(node.callee.property?.name) - ) { - return node.callee.property.name; - } - return null; -} - -function findVisualizerInSubtree(node) { - if (!node || typeof node !== 'object') return null; - - // Check if this node is a visualizer call - const viz = isVisualizerCall(node); - if (viz) return viz; - - // Recursively search children - for (const key of Object.keys(node)) { - if (key === 'parent') continue; // Skip parent references to avoid cycles - const child = node[key]; - if (Array.isArray(child)) { - for (const item of child) { - const found = findVisualizerInSubtree(item); - if (found) return found; - } - } else if (child && typeof child === 'object' && child.type) { - const found = findVisualizerInSubtree(child); - if (found) return found; - } - } - return null; -} - -// Creates AST nodes for: userDefinedKeys.add('name'); strudelScope.name = name; globalThis.name = name; -// Used in block-based evaluation to persist variables/functions across blocks -// We add to both strudelScope (for internal lookups) and globalThis (for direct access) -// We also track the key in userDefinedKeys so clearScope() can remove it later -function createScopeAssignment(name) { - return [ - // userDefinedKeys.add('name'); - { - type: 'ExpressionStatement', - expression: { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: { - type: 'Identifier', - name: 'userDefinedKeys', - }, - property: { - type: 'Identifier', - name: 'add', - }, - computed: false, - }, - arguments: [ - { - type: 'Literal', - value: name, - }, - ], - }, - }, - // strudelScope.name = name; - { - type: 'ExpressionStatement', - expression: { - type: 'AssignmentExpression', - operator: '=', - left: { - type: 'MemberExpression', - object: { - type: 'Identifier', - name: 'strudelScope', - }, - property: { - type: 'Identifier', - name: name, - }, - computed: false, - }, - right: { - type: 'Identifier', - name: name, - }, - }, - }, - // globalThis.name = name; - { - type: 'ExpressionStatement', - expression: { - type: 'AssignmentExpression', - operator: '=', - left: { - type: 'MemberExpression', - object: { - type: 'Identifier', - name: 'globalThis', - }, - property: { - type: 'Identifier', - name: name, - }, - computed: false, - }, - right: { - type: 'Identifier', - name: name, - }, - }, - }, - ]; -} - function findMiniDisableRanges(comments, codeEnd) { const ranges = []; const stack = []; // used to track on/off pairs diff --git a/test/examples.test.mjs b/test/examples.test.mjs index 5ee1348a3..ebec2252e 100644 --- a/test/examples.test.mjs +++ b/test/examples.test.mjs @@ -20,7 +20,6 @@ const skippedExamples = [ 'accelerationX', 'defaultmidimap', 'midimaps', - 'clearScope', 'bmod', ]; diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 867635883..37904cb82 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -10,11 +10,8 @@ import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; import tailwind from '@astrojs/tailwind'; import AstroPWA from '@vite-pwa/astro'; -import process from 'node:process'; - -const site = process.env.SITE_URL || `https://strudel.cc/`; // root url without a path -const base = process.env.BASE_PATH || ''; // base path of the strudel site - +const site = process.env.STRUDEL_SITE || `https://strudel.cc/`; // root url without a path +const base = process.env.STRUDEL_BASE || '/'; // base path of the strudel site const baseNoTrailing = base.endsWith('/') ? base.slice(0, -1) : base; // this rehype plugin fixes relative links diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index 62d310108..d29534468 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -150,7 +150,6 @@ export function SettingsTab({ started }) { isMultiCursorEnabled, patternAutoStart, includePrebakeScriptInShare, - isBlockBasedEvalEnabled, } = useSettings(); const shouldAlwaysSync = isUdels(); const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; @@ -307,11 +306,6 @@ export function SettingsTab({ started }) { onChange={(cbEvent) => settingsMap.setKey('isMultiCursorEnabled', cbEvent.target.checked)} value={isMultiCursorEnabled} /> - settingsMap.setKey('isBlockBasedEvalEnabled', cbEvent.target.checked)} - value={isBlockBasedEvalEnabled} - /> settingsMap.setKey('isFlashEnabled', cbEvent.target.checked)} diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 0d44eedfe..0fbd89e38 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -106,19 +106,17 @@ export function useReplContext() { //post to iframe parent (like Udels) if it exists... window.parent?.postMessage(code); - // Get the full buffer content from the editor instead of just the evaluated block - const fullBufferCode = editorRef.current?.code || code; - setLatestCode(fullBufferCode); - window.location.hash = '#' + code2hash(fullBufferCode); - setDocumentTitle(fullBufferCode); + setLatestCode(code); + window.location.hash = '#' + code2hash(code); + setDocumentTitle(code); const viewingPatternData = getViewingPatternData(); - setVersionDefaultsFrom(fullBufferCode); - const data = { ...viewingPatternData, code: fullBufferCode }; + setVersionDefaultsFrom(code); + const data = { ...viewingPatternData, code }; let id = data.id; const isExamplePattern = viewingPatternData.collection !== userPattern.collection; if (isExamplePattern) { - const codeHasChanged = fullBufferCode !== viewingPatternData.code; + const codeHasChanged = code !== viewingPatternData.code; if (codeHasChanged) { // fork example const newPattern = userPattern.duplicate(data); @@ -170,8 +168,9 @@ export function useReplContext() { useEffect(() => { let editorSettings = {}; Object.keys(defaultSettings).forEach((key) => { - // Don't use hasOwnProperty - nanostore uses proxies so values may not be own properties - editorSettings[key] = _settings[key]; + if (Object.prototype.hasOwnProperty.call(_settings, key)) { + editorSettings[key] = _settings[key]; + } }); editorRef.current?.updateSettings(editorSettings); }, [_settings]); diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 6acf8e2e2..7c0ec25ff 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -33,7 +33,6 @@ export const defaultSettings = { isPatternHighlightingEnabled: true, isTabIndentationEnabled: false, isMultiCursorEnabled: false, - isBlockBasedEvalEnabled: false, theme: 'strudelTheme', fontFamily: 'monospace', fontSize: 18, @@ -90,7 +89,6 @@ export const $settings = computed(settingsMap, (state) => { isSyncEnabled: isUdels() ? true : parseBoolean(state.isSyncEnabled), isTabIndentationEnabled: parseBoolean(state.isTabIndentationEnabled), isMultiCursorEnabled: parseBoolean(state.isMultiCursorEnabled), - isBlockBasedEvalEnabled: parseBoolean(state.isBlockBasedEvalEnabled), fontSize: Number(state.fontSize), panelPosition: state.activeFooter !== '' && !isUdels() ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is! isPanelPinned: parseBoolean(state.isPanelPinned), From 196115a9da839b0a80a4b580b4e6e075bf44b079 Mon Sep 17 00:00:00 2001 From: hirnsalat Date: Wed, 14 Oct 2026 19:32:54 +0200 Subject: [PATCH 16/24] fix slider --- packages/transpiler/plugin-widgets.mjs | 38 +++++++------------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/packages/transpiler/plugin-widgets.mjs b/packages/transpiler/plugin-widgets.mjs index a6a46eb3b..bebe63ed9 100644 --- a/packages/transpiler/plugin-widgets.mjs +++ b/packages/transpiler/plugin-widgets.mjs @@ -39,15 +39,11 @@ const sliderTranspilerPlugin = { if (!isSliderFunction(node)) return; context.widgets ??= []; context.sliders ??= []; - const { options, widgets, sliders, nodeOffset } = context; + const { options, widgets, sliders } = context; const { emitWidgets } = options; - const from = node.arguments[0].start + nodeOffset; - const to = node.arguments[0].end + nodeOffset; - const id = `${from}:${to}`; // Range-based ID for stability const sliderConfig = { - from, - to, - id, + from: node.arguments[0].start, + to: node.arguments[0].end, value: node.arguments[0].raw, // don't use value! min: node.arguments[1]?.value ?? 0, max: node.arguments[2]?.value ?? 1, @@ -56,7 +52,7 @@ const sliderTranspilerPlugin = { }; emitWidgets && widgets.push(sliderConfig); sliders.push(sliderConfig); - return this.replace(sliderWithLocation(node, nodeOffset)); + return this.replace(sliderWithLocation(node)); }, }), }; @@ -73,13 +69,8 @@ function isWidgetMethod(node) { return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name); } -function sliderWithLocation(node, nodeOffset = 0) { - // Apply nodeOffset for block-based evaluation to generate correct range - const from = node.arguments[0].start + nodeOffset; - const to = node.arguments[0].end + nodeOffset; - - // Use range-based ID for stability during block evaluation - const id = `${from}:${to}`; +function sliderWithLocation(node) { + const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id // add loc as identifier to first argument // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) @@ -95,19 +86,10 @@ function sliderWithLocation(node, nodeOffset = 0) { export function getWidgetID(widgetConfig) { // the widget id is used as id for the dom element + as key for eventual resources // for example, for each scope widget, a new analyser + buffer (large) is created - // Update: use range-based ID generation for better stability during block evaluation - // When we have both from and to, use them together for stability - // Otherwise fall back to position-based ID for backward compatibility - let uniqueIdentifier; - if (widgetConfig.from !== undefined && widgetConfig.to !== undefined) { - // Use range for more stable identification - uniqueIdentifier = `${widgetConfig.from}-${widgetConfig.to}`; - } else { - // Fallback to single position (for backward compatibility) - uniqueIdentifier = widgetConfig.to || widgetConfig.from || 0; - } - const baseId = `${widgetConfig.id || ''}_widget_${widgetConfig.type}`; - return `${baseId}_${widgetConfig.index}_${uniqueIdentifier}`; + // that means, if we use the index index of line position as id, less garbage is generated + // return `widget_${widgetConfig.to}`; // more gargabe + //return `widget_${widgetConfig.index}_${widgetConfig.to}`; // also more garbage + return `${widgetConfig.id || ''}_widget_${widgetConfig.type}_${widgetConfig.index}`; // less garbage } function widgetWithLocation(node, widgetConfig) { From 1a52eb45156993c2c176eff88fbaaaffc127ceaa Mon Sep 17 00:00:00 2001 From: hirnsalat Date: Wed, 14 Oct 2026 19:42:17 +0200 Subject: [PATCH 17/24] revert applyPatternTransformations extraction --- packages/core/repl.mjs | 75 ++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 43 deletions(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 4c81fd5a6..116ad4cc2 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -111,48 +111,6 @@ export function repl({ setTime(() => scheduler.now()); // TODO: refactor? setClockTime(() => scheduler.getTime()); // TODO: refactor? - // Helper function to apply pattern transformations (solo, each, all) - // this should be abstracted more - function applyPatternTransforms(pattern) { - const allPatterns = Object.values(pPatterns); - - if (allPatterns.length) { - let patterns = []; - let soloActive = false; - for (const [key, value] of Object.entries(pPatterns)) { - // handle soloed patterns ex: S$: s("bd!4") - const isSolod = key.length > 1 && key.startsWith('S'); - if (isSolod && soloActive === false) { - // first time we see a soloed pattern, clear existing patterns - patterns = []; - soloActive = true; - } - if (!soloActive || (soloActive && isSolod)) { - const valWithState = value.withState((state) => state.setControls({ id: key })); - patterns.push(valWithState); - } - } - if (eachTransform) { - // Explicit lambda so only element (not index and array) are passed - patterns = patterns.map((x) => eachTransform(x)); - } - pattern = stack(...patterns); - } else if (eachTransform) { - pattern = eachTransform(pattern); - } - if (allTransforms.length) { - for (const transform of allTransforms) { - pattern = transform(pattern); - } - } - - if (!isPattern(pattern)) { - pattern = silence; - } - - return pattern; - } - const stop = () => scheduler.stop(); const start = () => scheduler.start(); const pause = () => scheduler.pause(); @@ -291,8 +249,39 @@ export function repl({ code = `mondolang\`${code}\``; } let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); + if (Object.keys(pPatterns).length) { + let patterns = []; + let soloActive = false; + for (const [key, value] of Object.entries(pPatterns)) { + // handle soloed patterns ex: S$: s("bd!4") + const isSolod = key.length > 1 && key.startsWith('S'); + if (isSolod && soloActive === false) { + // first time we see a soloed pattern, clear existing patterns + patterns = []; + soloActive = true; + } + if (!soloActive || (soloActive && isSolod)) { + const valWithState = value.withState((state) => state.setControls({ id: key })); + patterns.push(valWithState); + } + } + if (eachTransform) { + // Explicit lambda so only element (not index and array) are passed + patterns = patterns.map((x) => eachTransform(x)); + } + pattern = stack(...patterns); + } else if (eachTransform) { + pattern = eachTransform(pattern); + } + if (allTransforms.length) { + for (const transform of allTransforms) { + pattern = transform(pattern); + } + } - pattern = applyPatternTransforms(pattern); + if (!isPattern(pattern)) { + pattern = silence; + } logger(`[eval] code updated`); pattern = await setPattern(pattern, autostart); From dd8d04dc5c4d6e7c6dfc96c52295c06741faaa46 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 14 Oct 2026 21:16:52 +0200 Subject: [PATCH 18/24] fix merge error --- website/src/repl/useReplContext.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index f7e672036..6648c5320 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -108,7 +108,6 @@ export function useReplContext() { window.parent?.postMessage(code); setLatestCode(code); - window.location.hash = '#' + code2hash(code); try { window.location.hash = '#' + code2hash(code); } catch (e) { From d7b4c6e4986d021d4f522419756458b3aed85099 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 14 Oct 2026 21:22:35 +0200 Subject: [PATCH 19/24] revert line --- packages/core/repl.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 116ad4cc2..b88c254f0 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -296,7 +296,7 @@ export function repl({ pending: false, }); - afterEval?.({ code, pattern, meta, range: undefined, widgetRemoved: false }); + afterEval?.({ code, pattern, meta }); return pattern; } catch (err) { logger(`[eval] error: ${err.message}`, 'error'); From 32cccda215ab11465e3a0ccdeb800746ce17e5c4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 14 Oct 2026 22:00:06 +0200 Subject: [PATCH 20/24] revert #1821 helix keybindings pr --- packages/codemirror/keybindings.mjs | 162 ++++++------------ packages/codemirror/package.json | 1 - pnpm-lock.yaml | 20 --- website/src/pages/technical-manual/helix.mdx | 85 --------- .../src/repl/components/panel/SettingsTab.jsx | 2 +- 5 files changed, 56 insertions(+), 214 deletions(-) delete mode 100644 website/src/pages/technical-manual/helix.mdx diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs index 16c7ec99e..9b217cfed 100644 --- a/packages/codemirror/keybindings.mjs +++ b/packages/codemirror/keybindings.mjs @@ -6,7 +6,6 @@ import { emacs } from '@replit/codemirror-emacs'; import { vim, Vim } from '@replit/codemirror-vim'; // import { vim } from './vim_test.mjs'; import { vscodeKeymap } from '@replit/codemirror-vscode-keymap'; -import { helix, commands } from 'codemirror-helix'; import { logger } from '@strudel/core'; const vscodePlugin = ViewPlugin.fromClass( @@ -21,70 +20,6 @@ const vscodePlugin = ViewPlugin.fromClass( ); const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []); -function replEval(view) { - try { - // Dispatch a dedicated evaluate event first - let handled = false; - try { - const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true }); - handled = document.dispatchEvent(ev) === false; // false means preventDefault was called - } catch (e) { - console.error('Error dispatching repl-evaluate event', e); - } - if (handled) { - return; - } - // Try Ctrl+Enter first if not handled by custom event - const ctrlEnter = new KeyboardEvent('keydown', { - key: 'Enter', - code: 'Enter', - ctrlKey: true, - bubbles: true, - cancelable: true, - }); - view?.dom?.dispatchEvent?.(ctrlEnter); - // If not handled (no handler called preventDefault), try Alt+Enter as - // fallback - if (!ctrlEnter.defaultPrevented) { - const altEnter = new KeyboardEvent('keydown', { - key: 'Enter', - code: 'Enter', - altKey: true, - bubbles: true, - cancelable: true, - }); - view?.dom?.dispatchEvent?.(altEnter); - } - } catch (e) { - console.error('Error dispatching repl evaluation event', e); - } -} - -function replStop(view) { - try { - // First try dispatching our custom stop event, then fallback to Alt+. - let handled = false; - try { - const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true }); - handled = document.dispatchEvent(ev) === false; - } catch (e) { - console.error('Error dispatching repl-stop event', e); - } - if (!handled) { - const altDot = new KeyboardEvent('keydown', { - key: '.', - code: 'Period', - altKey: true, - bubbles: true, - cancelable: true, - }); - view?.dom?.dispatchEvent?.(altDot); - } - } catch (e) { - console.error('Error dispatching repl stop event', e); - } -} - // Map Vim :w to trigger the same action as evaluation. We dispatch a custom // event 'repl-evaluate' that the editor listens for, and also simulate // Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it @@ -112,9 +47,29 @@ try { // :q to pause/stop Vim.defineEx('quit', 'q', (cm) => { - const view = cm.cm6; - logger('[vim] :q — stopping repl'); - replStop(view); + try { + const view = cm?.view || cm; + // First try dispatching our custom stop event, then fallback to Alt+. + let handled = false; + try { + const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true }); + handled = document.dispatchEvent(ev) === false; + } catch (e) { + console.error('Error dispatching repl-stop event', e); + } + if (!handled) { + const altDot = new KeyboardEvent('keydown', { + key: '.', + code: 'Period', + altKey: true, + bubbles: true, + cancelable: true, + }); + view?.dom?.dispatchEvent?.(altDot); + } + } catch (e) { + console.error('Error dispatching :q stop event', e); + } }); // :w to evaluate @@ -128,7 +83,38 @@ try { } catch (e) { console.error('Error logging Vim :w evaluation', e); } - replEval(view); + // Dispatch a dedicated evaluate event first + let handled = false; + try { + const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true }); + handled = document.dispatchEvent(ev) === false; // false means preventDefault was called + } catch (e) { + console.error('Error dispatching repl-evaluate event', e); + } + if (handled) { + return; + } + // Try Ctrl+Enter first if not handled by custom event + const ctrlEnter = new KeyboardEvent('keydown', { + key: 'Enter', + code: 'Enter', + ctrlKey: true, + bubbles: true, + cancelable: true, + }); + view?.dom?.dispatchEvent?.(ctrlEnter); + // If not handled (no handler called preventDefault), try Alt+Enter as + // fallback + if (!ctrlEnter.defaultPrevented) { + const altEnter = new KeyboardEvent('keydown', { + key: 'Enter', + code: 'Enter', + altKey: true, + bubbles: true, + cancelable: true, + }); + view?.dom?.dispatchEvent?.(altEnter); + } } catch (e) { console.error('Error dispatching :w evaluation event', e); } @@ -138,49 +124,11 @@ try { console.error('Vim ex command setup failed (defineEx missing or Vim unavailable)', e); } -// Map Helix :w to trigger the same action as evaluation. We dispatch a custom -// event 'repl-evaluate' that the editor listens for, and also simulate -// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it -// appears in the Console panel. -const helixCommands = commands.of([ - { - // :w to evaluate - name: 'write', - aliases: ['w'], - help: 'Repl-eval', - handler(view, args) { - try { - view?.focus?.(); // Let the app know this came from Helix :w - logger('[helix] :w — evaluating code'); - replEval(view); - } catch (e) { - console.error('Error dispatching helix :w evaluation event', e); - } - }, - }, - { - // :q to pause/stop - name: 'quit', - aliases: ['q'], - help: 'Repl-stop', - handler(view, args) { - try { - view?.focus?.(); // Let the app know this came from Helix :q - logger('[helix] :q — stopping repl'); - replStop(view); - } catch (e) { - console.error('Error dispatching helix :q stop event', e); - } - }, - }, -]); - const keymaps = { vim, emacs, codemirror: () => keymap.of(defaultKeymap), vscode: vscodeExtension, - helix: () => [helix(), helixCommands], }; export { Vim } from '@replit/codemirror-vim'; diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index d2432ef83..a18019e37 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -44,7 +44,6 @@ "@replit/codemirror-emacs": "^6.1.0", "@replit/codemirror-vim": "^6.3.0", "@replit/codemirror-vscode-keymap": "^6.0.2", - "codemirror-helix": "^0.5.0", "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", "@strudel/tonal": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9930ef857..20c96e9d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -251,9 +251,6 @@ importers: '@tonaljs/tonal': specifier: ^4.10.0 version: 4.10.0 - codemirror-helix: - specifier: ^0.5.0 - version: 0.5.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2) nanostores: specifier: ^0.11.3 version: 0.11.3 @@ -3660,15 +3657,6 @@ packages: resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - codemirror-helix@0.5.0: - resolution: {integrity: sha512-hI56hf9VGz53H1YvL6H1GC7HtP6te8vX+MsIHaE9J7Q3PQ6KFapKtIRg6lqSH898ikHWpMCPu42r6HJN0IfVLA==} - peerDependencies: - '@codemirror/commands': ^6.0.0 - '@codemirror/language': ^6.0.0 - '@codemirror/search': ^6.0.0 - '@codemirror/state': ^6.0.0 - '@codemirror/view': ^6.0.0 - collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} @@ -11411,14 +11399,6 @@ snapshots: cmd-shim@6.0.3: {} - codemirror-helix@0.5.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2): - dependencies: - '@codemirror/commands': 6.8.0 - '@codemirror/language': 6.10.8 - '@codemirror/search': 6.5.8 - '@codemirror/state': 6.5.1 - '@codemirror/view': 6.36.2 - collapse-white-space@2.1.0: {} color-convert@2.0.1: diff --git a/website/src/pages/technical-manual/helix.mdx b/website/src/pages/technical-manual/helix.mdx deleted file mode 100644 index 36f058985..000000000 --- a/website/src/pages/technical-manual/helix.mdx +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: Helix Keybindings -layout: ../../layouts/MainLayout.astro ---- - -# Helix Keybindings in the REPL - -The Strudel REPL supports [Helix editor](https://helix-editor.com/) keybindings through the `codemirror-helix` extension. Helix is a post-modern modal text editor with a focus on selection-first editing and multiple cursors. - -## Enabling Helix Mode - -1. Open the Settings panel in the REPL (click the settings icon or use the keyboard shortcut) -2. Find the "Keybindings" section -3. Select "Helix" from the available options - -## Key Differences from Vim - -Helix uses a selection-first approach, which means: - -- **Selection → Action** (Helix) vs **Action → Motion** (Vim) -- For example, to delete a word in Helix: `w` (select word) → `d` (delete) -- In Vim, it would be: `dw` (delete word) - -## Common Helix Commands - -### Normal Mode - -- `i` — Enter insert mode before selection -- `a` — Enter insert mode after selection -- `v` — Enter visual/select mode -- `w` — Select next word -- `e` — Select to end of word -- `b` — Select previous word -- `x` — Select/extend line -- `d` — Delete selection -- `c` — Change selection (delete and enter insert mode) -- `y` — Yank (copy) selection -- `p` — Paste after selection -- `u` — Undo -- `U` — Redo -- `/` — Search -- `n` — Select next search match -- `N` — Select previous search match - -### Movement - -- `h, j, k, l` — Move left, down, up, right -- `gg` — Go to start of document -- `ge` — Go to end of document -- `{` / `}` — Move to previous/next paragraph -- `%` — Match bracket - -### Multi-cursor - -- `C` — Duplicate cursor to line below -- `Alt-C` — Duplicate cursor to line above -- `,` — Remove primary cursor -- `Alt-,` — Remove all secondary cursors - -## Strudel-Specific Features - -Currently, Helix mode in Strudel provides standard Helix keybindings. Unlike Vim mode, there are no custom Strudel-specific commands (like `:w` to evaluate) yet. - -To evaluate code while in Helix mode, use the standard shortcuts: - -- `Ctrl+Enter` or `Alt+Enter` — Evaluate code -- `Alt+.` — Stop playback - -## Resources - -- [Helix Documentation](https://docs.helix-editor.com/) -- [Helix Keymap](https://docs.helix-editor.com/keymap.html) -- [codemirror-helix on npm](https://www.npmjs.com/package/codemirror-helix) -- [codemirror-helix on GitLab](https://gitlab.com/_rvidal/codemirror-helix) - -## Notes - -- The `codemirror-helix` extension is described as an "initial version" and may not implement all Helix features -- Behavior may differ slightly from the native Helix editor -- If you encounter issues, you can switch back to other keybinding modes in Settings -- The keybinding preference is saved in your browser's local storage - -## Contributing - -If you'd like to add Strudel-specific Helix commands (similar to Vim's `:w` for evaluate), contributions are welcome! See the implementation in `/packages/codemirror/keybindings.mjs` for reference. diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index a385fd48a..f01c1669c 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -235,7 +235,7 @@ function MainSettingsContent({ started }) { settingsMap.setKey('keybindings', keybindings)} - items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs', helix: 'Helix', vscode: 'VSCode' }} + items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs', vscode: 'VSCode' }} > From 56b279d72396c8981bf18960e61c7faa6248fc2d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 14 Oct 2026 22:40:37 +0200 Subject: [PATCH 21/24] Revert "Merge pull request 'feature/autocomplete-sound-names' (#1564) from drdozer/strudel:feature/autocomplete-sound-names into main" This reverts commit 98f5a56ef6391cb25e652f021078a532f3f373e0, reversing changes made to c727534d290c0ec3742df310767741eb9e15d55c. --- .gitignore | 14 -- packages/codemirror/autocomplete.mjs | 356 +-------------------------- packages/codemirror/package.json | 5 +- packages/tonal/index.mjs | 4 +- pnpm-lock.yaml | 9 - 5 files changed, 15 insertions(+), 373 deletions(-) diff --git a/.gitignore b/.gitignore index 7532adaf0..2be3ee698 100644 --- a/.gitignore +++ b/.gitignore @@ -45,20 +45,6 @@ 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 diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 20c128188..69aa3bc59 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -1,11 +1,6 @@ import jsdoc from '../../doc.json'; import { autocompletion } from '@codemirror/autocomplete'; import { h } from './html'; -//TODO: fix tonal scale import -// import { Scale } from '@tonaljs/tonal'; -// import { soundMap } from '@strudel/webaudio'; -let soundMap = undefined; -import { complex } from '@strudel/tonal'; const escapeHtml = (str) => { const div = document.createElement('div'); @@ -80,53 +75,6 @@ const isValidDoc = (doc) => { const hasExcludedTags = (doc) => ['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)); -export function bankCompletions() { - // TODO: FIX IMPORT - 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' })); -} - -// Attempt to get all scale names from Tonal TODO: FIX IMPORT -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); -// } - -// Valid mode values for voicing -const modeCompletions = [ - { label: 'below', type: 'mode' }, - { label: 'above', type: 'mode' }, - { label: 'duck', type: 'mode' }, - { 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); @@ -165,299 +113,19 @@ 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', -]; - -// 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_NO_QUOTES_REGEX); - if (scaleNoQuotesContext) { - return { - from: scaleNoQuotesContext.to, - options: [], - }; - } - - // 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(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; - const options = filtered.map((p) => ({ label: p, type: 'pitch' })); - return { from, options }; - } else { - return { from: scalePreColonContext.to, options: [] }; - } - } - } - 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(SOUND_NO_QUOTES_REGEX); - if (soundNoQuotesContext) { - return { - from: soundNoQuotesContext.to, - options: [], - }; - } - - // Then check for sound context with quotes - provide completions - 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(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)); - let options = filteredSounds.map((name) => ({ label: name, type: 'sound' })); - const from = soundContext.to - fragment.length; - return { - from, - options, - }; -} - -// 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_NO_QUOTES_REGEX); - if (bankNoQuotesContext) { - return { - from: bankNoQuotesContext.to, - options: [], - }; - } - - // Then check for bank context with quotes - provide completions - let bankMatch = context.matchBefore(BANK_WITH_QUOTES_REGEX); - 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, - }; -} - -// 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_NO_QUOTES_REGEX); - if (modeNoQuotesContext) { - return { - from: modeNoQuotesContext.to, - options: [], - }; - } - - // 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(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; - return { - from, - options: filteredModes, - }; -} - -// 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_NO_QUOTES_REGEX); - if (chordNoQuotesContext) { - return { - from: chordNoQuotesContext.to, - options: [], - }; - } - - // Then check for chord context with quotes - provide completions - let chordContext = context.matchBefore(CHORD_WITH_QUOTES_REGEX); - 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(CHORD_FRAGMENT_MATCH_REGEX); - 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 }; - } -} - -// Cached regex patterns for fallbackHandler -const FALLBACK_WORD_REGEX = /\w*/; - -function fallbackHandler(context) { - const word = context.matchBefore(FALLBACK_WORD_REGEX); - if (word && word.from === word.to && !context.explicit) return null; - if (word) { - return { - from: word.from, - options: jsdocCompletions, - }; - } - return null; -} - -const handlers = [ - soundHandler, - bankHandler, - chordHandler, - scaleHandler, - modeHandler, - // 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; + const word = context.matchBefore(/\w*/); + if (word.from === word.to && !context.explicit) return null; + + return { + from: word.from, + options: jsdocCompletions, + /* options: [ + { label: 'match', type: 'keyword' }, + { label: 'hello', type: 'variable', info: '(World)' }, + { label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' }, + ], */ + }; }; export const isAutoCompletionEnabled = (enabled) => diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index d2432ef83..016184052 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -47,11 +47,8 @@ "codemirror-helix": "^0.5.0", "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", - "@strudel/tonal": "workspace:*", "@strudel/transpiler": "workspace:*", - "@tonaljs/tonal": "^4.10.0", - "nanostores": "^0.11.3", - "superdough": "workspace:*" + "nanostores": "^0.11.3" }, "devDependencies": { "vite": "^6.0.11" diff --git a/packages/tonal/index.mjs b/packages/tonal/index.mjs index ebbcf0851..a3c0cba39 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'; -export * from './ireal.mjs'; + +import './ireal.mjs'; export const packageName = '@strudel/tonal'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4c045c9f..e0a698a6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -242,24 +242,15 @@ importers: '@strudel/draw': specifier: workspace:* version: link:../draw - '@strudel/tonal': - specifier: workspace:* - version: link:../tonal '@strudel/transpiler': specifier: workspace:* version: link:../transpiler - '@tonaljs/tonal': - specifier: ^4.10.0 - version: 4.10.0 codemirror-helix: specifier: ^0.5.0 version: 0.5.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2) nanostores: specifier: ^0.11.3 version: 0.11.3 - superdough: - specifier: workspace:* - version: link:../superdough devDependencies: vite: specifier: ^6.0.11 From a2af0cd5cc15b224e731a863c1e75a3b186d6243 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 14 Oct 2026 23:10:29 +0200 Subject: [PATCH 22/24] Revert "Merge pull request 'Repurpose vim shortcuts for usability' (#1624) from dtricks/strudel:feat/vim-shortcuts into main" This reverts commit 12204c9053c1355cdd06a6007b28e21116e88534, reversing changes made to f2009a33aadde8ef4140442afc5f8addee5fdf56. --- packages/codemirror/codemirror.mjs | 52 +----- packages/codemirror/keybindings.mjs | 161 +----------------- website/src/pages/technical-manual/repl.mdx | 4 - website/src/pages/technical-manual/vim.mdx | 58 ------- .../src/repl/components/panel/SettingsTab.jsx | 7 +- website/src/repl/prebakeCodeMirror.mjs | 33 ---- 6 files changed, 6 insertions(+), 309 deletions(-) delete mode 100644 website/src/pages/technical-manual/vim.mdx diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 71e5c49ab..874fee4a4 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -1,5 +1,5 @@ import { closeBrackets } from '@codemirror/autocomplete'; -import { indentWithTab, toggleLineComment } from '@codemirror/commands'; +import { indentWithTab } from '@codemirror/commands'; import { javascript, javascriptLanguage } from '@codemirror/lang-javascript'; import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language'; import { Compartment, EditorState, Prec } from '@codemirror/state'; @@ -12,7 +12,7 @@ import { lineNumbers, } from '@codemirror/view'; import { persistentAtom } from '@nanostores/persistent'; -import { logger, registerControl, repl } from '@strudel/core'; +import { registerControl, repl } from '@strudel/core'; import { cleanupDraw, cleanupDrawContext, Drawer } from '@strudel/draw'; import { isAutoCompletionEnabled } from './autocomplete.mjs'; import { basicSetup } from './basicSetup.mjs'; @@ -289,39 +289,6 @@ export class StrudelMirror { } }; document.addEventListener('start-repl', this.onStartRepl); - - // Handle global evaluation requests (e.g., from Vim :w) - this.onEvaluateRequest = (e) => { - try { - if (e.detail.view !== this.editor) { - return; // ignore events from other editors - } - // Evaluate current editor on repl-evaluate - logger('[repl] evaluate via event'); - this.evaluate(); - e?.cancelable && e.preventDefault?.(); - } catch (err) { - console.error('Error handling repl-evaluate event', err); - } - }; - document.addEventListener('repl-evaluate', this.onEvaluateRequest); - document.addEventListener('repl-stop', this.onStopRequest); - - // Toggle comments requested from Vim (gc) - this.onToggleComment = (e) => { - try { - if (e.detail.view !== this.editor) { - return; // ignore events from other editors - } - // Honor selections; toggleLineComment handles both selections and - // single line - toggleLineComment(this.editor); - e?.cancelable && e.preventDefault?.(); - } catch (err) { - console.error('Error handling repl-toggle-comment event', err); - } - }; - document.addEventListener('repl-toggle-comment', this.onToggleComment); } draw(haps, time, painters) { painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime)); @@ -350,18 +317,6 @@ export class StrudelMirror { this.repl.stop(); } - // Listen for global stop requests (e.g., from Vim :q) - onStopRequest = (e) => { - try { - if (e.detail.view !== this.editor) { - return; // ignore events from other editors - } - this.stop(); - e?.cancelable && e.preventDefault?.(); - } catch (err) { - console.error('Error handling repl-stop event', err); - } - }; async toggle() { if (this.repl.scheduler.started) { this.repl.stop(); @@ -466,9 +421,6 @@ export class StrudelMirror { } clear() { this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl); - this.onEvaluateRequest && document.removeEventListener('repl-evaluate', this.onEvaluateRequest); - this.onStopRequest && document.removeEventListener('repl-stop', this.onStopRequest); - this.onToggleComment && document.removeEventListener('repl-toggle-comment', this.onToggleComment); } getCursorLocation() { return this.editor.state.selection.main.head; diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs index 16c7ec99e..5c5360a3e 100644 --- a/packages/codemirror/keybindings.mjs +++ b/packages/codemirror/keybindings.mjs @@ -3,11 +3,10 @@ import { Prec, EditorState } from '@codemirror/state'; import { keymap, ViewPlugin } from '@codemirror/view'; // import { searchKeymap } from '@codemirror/search'; import { emacs } from '@replit/codemirror-emacs'; -import { vim, Vim } from '@replit/codemirror-vim'; +import { vim } from '@replit/codemirror-vim'; // import { vim } from './vim_test.mjs'; import { vscodeKeymap } from '@replit/codemirror-vscode-keymap'; -import { helix, commands } from 'codemirror-helix'; -import { logger } from '@strudel/core'; +import { helix } from 'codemirror-helix'; const vscodePlugin = ViewPlugin.fromClass( class { @@ -21,166 +20,12 @@ const vscodePlugin = ViewPlugin.fromClass( ); const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []); -function replEval(view) { - try { - // Dispatch a dedicated evaluate event first - let handled = false; - try { - const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true }); - handled = document.dispatchEvent(ev) === false; // false means preventDefault was called - } catch (e) { - console.error('Error dispatching repl-evaluate event', e); - } - if (handled) { - return; - } - // Try Ctrl+Enter first if not handled by custom event - const ctrlEnter = new KeyboardEvent('keydown', { - key: 'Enter', - code: 'Enter', - ctrlKey: true, - bubbles: true, - cancelable: true, - }); - view?.dom?.dispatchEvent?.(ctrlEnter); - // If not handled (no handler called preventDefault), try Alt+Enter as - // fallback - if (!ctrlEnter.defaultPrevented) { - const altEnter = new KeyboardEvent('keydown', { - key: 'Enter', - code: 'Enter', - altKey: true, - bubbles: true, - cancelable: true, - }); - view?.dom?.dispatchEvent?.(altEnter); - } - } catch (e) { - console.error('Error dispatching repl evaluation event', e); - } -} - -function replStop(view) { - try { - // First try dispatching our custom stop event, then fallback to Alt+. - let handled = false; - try { - const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true }); - handled = document.dispatchEvent(ev) === false; - } catch (e) { - console.error('Error dispatching repl-stop event', e); - } - if (!handled) { - const altDot = new KeyboardEvent('keydown', { - key: '.', - code: 'Period', - altKey: true, - bubbles: true, - cancelable: true, - }); - view?.dom?.dispatchEvent?.(altDot); - } - } catch (e) { - console.error('Error dispatching repl stop event', e); - } -} - -// Map Vim :w to trigger the same action as evaluation. We dispatch a custom -// event 'repl-evaluate' that the editor listens for, and also simulate -// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it -// appears in the Console panel. -try { - if (Vim && typeof Vim.defineEx === 'function') { - // Map gc to toggle line comments by dispatching a custom event that our - // CodeMirror integration listens to. This avoids depending on Vim's - // internal actions and works with current selections/visual mode. - try { - Vim.defineAction('strudelToggleComment', (cm) => { - const view = cm.cm6; - try { - const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true }); - document.dispatchEvent(ev); - } catch (e) { - console.error('strudelToggleComment dispatch failed', e); - } - }); - Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'normal' }); - Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'visual' }); - } catch (e) { - console.error('Vim gc mapping failed', e); - } - - // :q to pause/stop - Vim.defineEx('quit', 'q', (cm) => { - const view = cm.cm6; - logger('[vim] :q — stopping repl'); - replStop(view); - }); - - // :w to evaluate - Vim.defineEx('write', 'w', (cm) => { - const view = cm.cm6; - try { - view.focus?.(); - // Let the app know this came from Vim :w - try { - logger('[vim] :w — evaluating code'); - } catch (e) { - console.error('Error logging Vim :w evaluation', e); - } - replEval(view); - } catch (e) { - console.error('Error dispatching :w evaluation event', e); - } - }); - } -} catch (e) { - console.error('Vim ex command setup failed (defineEx missing or Vim unavailable)', e); -} - -// Map Helix :w to trigger the same action as evaluation. We dispatch a custom -// event 'repl-evaluate' that the editor listens for, and also simulate -// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it -// appears in the Console panel. -const helixCommands = commands.of([ - { - // :w to evaluate - name: 'write', - aliases: ['w'], - help: 'Repl-eval', - handler(view, args) { - try { - view?.focus?.(); // Let the app know this came from Helix :w - logger('[helix] :w — evaluating code'); - replEval(view); - } catch (e) { - console.error('Error dispatching helix :w evaluation event', e); - } - }, - }, - { - // :q to pause/stop - name: 'quit', - aliases: ['q'], - help: 'Repl-stop', - handler(view, args) { - try { - view?.focus?.(); // Let the app know this came from Helix :q - logger('[helix] :q — stopping repl'); - replStop(view); - } catch (e) { - console.error('Error dispatching helix :q stop event', e); - } - }, - }, -]); - const keymaps = { vim, emacs, codemirror: () => keymap.of(defaultKeymap), vscode: vscodeExtension, - helix: () => [helix(), helixCommands], + helix: () => [helix()], }; export { Vim } from '@replit/codemirror-vim'; diff --git a/website/src/pages/technical-manual/repl.mdx b/website/src/pages/technical-manual/repl.mdx index 4135848ed..166b73f27 100644 --- a/website/src/pages/technical-manual/repl.mdx +++ b/website/src/pages/technical-manual/repl.mdx @@ -117,10 +117,6 @@ Here's an example AST for `c3 [e3 g3]` which translates to `seq(c3, seq(e3, g3))` -## Vim Keybindings - -See the separate page on Vim shortcuts for a quick reference: [/technical-manual/vim](/technical-manual/vim) - ## Scheduling Events After an instance of `Pattern` is obtained from the user code, diff --git a/website/src/pages/technical-manual/vim.mdx b/website/src/pages/technical-manual/vim.mdx deleted file mode 100644 index 6ec41d4a2..000000000 --- a/website/src/pages/technical-manual/vim.mdx +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: Vim Shortcuts -layout: ../../layouts/MainLayout.astro ---- - -# Vim Shortcuts in the REPL - -When the REPL editor (CodeMirror) is configured to use Vim keybindings, the following commands are available: - -- :w — Evaluate the current code - - - Triggers the same evaluation as Ctrl+Enter / Alt+Enter - - You'll see messages in the Console panel such as: - - [vim] :w — evaluating code - - [repl] evaluate via event - - [eval] code updated - -- :q — Stop/pause playback - - - Triggers the same stop action as Alt+. - - Useful to quickly stop scheduling without leaving Vim mode - -- gc — Toggle line comments for the current selection(s) - - - Works in normal and visual mode - - If there's a selection, all selected lines are toggled - -Notes - -- Behavior respects the current language mode in the editor for comment syntax. -- If multiple REPL editors are open, commands target the active editor. The implementation dispatches custom events handled by the editor. -- If you don't see the Console panel, open the right panel in the REPL UI. - -Troubleshooting - -- If :w logs but evaluation doesn't apply, ensure Vim keybindings are active and try again. You can also use Ctrl+Enter as a fallback. -- For :q / gc, ensure focus is inside the editor. If an error occurs, reload the page to reset editor state and try again. - -## Adding custom keybindings - -To add custom keybindings the `Vim` object can be used (either from within a pattern or in a prebake script) - -Example: - -```javascript -// Map 'jk' to Escape in normal mode -Vim.map('jk', '', 'insert'); -// Map 'U' to :w (Evaluate the current code) -Vim.map('U', ':w', 'normal'); -// Map 'Q' to :q — Stop/pause playback -Vim.map('Q', ':q', 'normal'); -// Map 'J' to find next '$' (jump to next label) -Vim.map('J', '/\\$', 'normal'); -// Map 'K' to find previous '$' (jump to previous label) -Vim.map('K', '?\\$', 'normal'); -``` - -For more information on how to use the `Vim` object see [CodeMirror Vim](https://github.com/replit/codemirror-vim) diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index a385fd48a..8cdba09e0 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -9,7 +9,7 @@ import { confirmDialog } from '../../util.mjs'; import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio'; import { ActionButton } from '../button/action-button.jsx'; import { exportScript, ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx'; -import { useEffect, useRef } from 'react'; +import { useRef } from 'react'; import cx from '@src/cx.mjs'; const inputClass = @@ -356,11 +356,6 @@ function MainSettingsContent({ started }) { function PrebakeSettingsContent() { const { fontFamily, includePrebakeScriptInShare, prebakeScript } = useSettings(); const editorRef = useRef(); - useEffect(() => { - return () => { - editorRef.current?.cleanup(); - }; - }); return (
diff --git a/website/src/repl/prebakeCodeMirror.mjs b/website/src/repl/prebakeCodeMirror.mjs index 42b913ae6..972da0062 100644 --- a/website/src/repl/prebakeCodeMirror.mjs +++ b/website/src/repl/prebakeCodeMirror.mjs @@ -1,4 +1,3 @@ -import { toggleLineComment } from '@codemirror/commands'; import { javascript, javascriptLanguage } from '@codemirror/lang-javascript'; import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language'; import { Compartment, EditorState, Prec } from '@codemirror/state'; @@ -63,28 +62,6 @@ export class PrebakeCodeMirror { state, parent: container, }); - - const handleSaveEvent = async (e) => { - if (e.detail.view !== this.view) { - return; // ignore events from other editors - } - await this.savePrebake(); - e?.cancelable && e.preventDefault?.(); - }; - const handleToggleComment = (e) => { - if (e.detail.view !== this.view) { - return; // ignore events from other editors - } - this.toggleComment(); - e?.cancelable && e.preventDefault?.(); - }; - - document.addEventListener('repl-evaluate', handleSaveEvent); - document.addEventListener('repl-toggle-comment', handleToggleComment); - this.cleanup = () => { - document.removeEventListener('prebake-evaluate', handleSaveEvent); - document.removeEventListener('prebake-toggle-comment', handleToggleComment); - }; } async savePrebake() { @@ -94,16 +71,6 @@ export class PrebakeCodeMirror { logger('[prebake] prebake saved'); } - toggleComment() { - try { - // Honor selections; toggleLineComment handles both selections and - // single line - toggleLineComment(this.view); - } catch (err) { - console.error('Error handling repl-toggle-comment event', err); - } - } - setCode(code) { const changes = { from: 0, From f440d7869c671731cdf9508752ba5b6ac086c2f9 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 15 Oct 2026 14:57:59 +0200 Subject: [PATCH 23/24] remove supradough --- eslint.config.mjs | 1 - packages/core/controls.mjs | 117 +- packages/supradough/.gitignore | 1 - packages/supradough/README.md | 3 - packages/supradough/dough-export.mjs | 123 -- packages/supradough/dough-worklet.mjs | 39 - packages/supradough/dough.mjs | 1119 ----------------- packages/supradough/index.mjs | 5 - packages/supradough/package.json | 39 - packages/supradough/vite.config.js | 16 - packages/webaudio/index.mjs | 1 - packages/webaudio/package.json | 3 +- packages/webaudio/supradough.mjs | 130 -- packages/webaudio/webaudio.mjs | 3 - pnpm-lock.yaml | 20 - .../src/repl/components/panel/Reference.jsx | 5 +- 16 files changed, 55 insertions(+), 1570 deletions(-) delete mode 100644 packages/supradough/.gitignore delete mode 100644 packages/supradough/README.md delete mode 100644 packages/supradough/dough-export.mjs delete mode 100644 packages/supradough/dough-worklet.mjs delete mode 100644 packages/supradough/dough.mjs delete mode 100644 packages/supradough/index.mjs delete mode 100644 packages/supradough/package.json delete mode 100644 packages/supradough/vite.config.js delete mode 100644 packages/webaudio/supradough.mjs diff --git a/eslint.config.mjs b/eslint.config.mjs index c9ff40ca1..e30b8e8a6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -42,7 +42,6 @@ export default [ '**/hydra.mjs', '**/jsdoc-synonyms.js', 'packages/hs2js/src/hs2js.mjs', - 'packages/supradough/dough-export.mjs', '**/samples', ], }, diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index a59fe6c33..cbd499e1b 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -446,7 +446,7 @@ export const { accelerate } = registerControl('accelerate'); * Sets the velocity from 0 to 1. Is multiplied together with gain. * * @name velocity - * @tags amplitude, superdough, supradough + * @tags amplitude, superdough * @synonyms vel * @example * s("hh*8") @@ -458,7 +458,7 @@ export const { velocity, vel } = registerControl('velocity', 'vel'); * Controls the gain by an exponential amount. * * @name gain - * @tags amplitude, superdough, supradough + * @tags amplitude, superdough * @param {number | Pattern} amount gain. * @example * s("hh*8").gain(".4!2 1 .4!2 1 .4 1").fast(2) @@ -469,7 +469,7 @@ export const { gain } = registerControl('gain'); * Gain applied after all effects have been processed. * * @name postgain - * @tags amplitude, superdough, supradough + * @tags amplitude, superdough * @example * s("bd sd [~ bd] sd,hh*8") * .compressor("-20:20:10:.002:.02").postgain(1.5) @@ -499,7 +499,7 @@ export const { amp } = registerControl('amp'); * any of the 8 individual FMs (e.g. `fmh2`) * * @name fmh - * @tags fm, superdough, supradough + * @tags fm, superdough * @param {number | Pattern} harmonicity * @example * note("c e g b g e") @@ -520,7 +520,7 @@ export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerM * `fm3` * * @name fm - * @tags fm, superdough, supradough + * @tags fm, superdough * @param {number | Pattern} brightness modulation index * @synonyms fmi * @example @@ -544,7 +544,7 @@ export const { fmi, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2 * any of the 8 individual FMs (e.g. `fmenv4`) * * @name fmenv - * @tags fm, envelope, superdough, supradough + * @tags fm, envelope, superdough * @param {number | Pattern} type lin | exp * @synonyms fme * @example @@ -569,7 +569,7 @@ export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fm * any of the 8 individual FMs (e.g. `fmatt5`) * * @name fmattack - * @tags fm, envelope, superdough, supradough + * @tags fm, envelope, superdough * @synonyms fmatt * @param {number | Pattern} time attack time * @synonyms fmatt @@ -608,7 +608,7 @@ export const { * any of the 8 individual FMs (e.g. `fmwave6`) * * @name fmwave - * @tags fm, superdough, supradough + * @tags fm, superdough * @param {number | Pattern} wave waveform * @example * n("0 1 2 3".fast(4)).scale("d:minor").s("sine").fmwave("").fm(4).fmh(2.01) @@ -628,7 +628,7 @@ export const { fmwave, fmwave1, fmwave2, fmwave3, fmwave4, fmwave5, fmwave6, fmw * any of the 8 individual FMs (e.g. `fmdec6`) * * @name fmdecay - * @tags fm, envelope, superdough, supradough + * @tags fm, envelope, superdough * @synonyms fmdec * @param {number | Pattern} time decay time * @synonyms fmdec @@ -668,7 +668,7 @@ export const { * any of the 8 individual FMs (e.g. `fmsus7`) * * @name fmsustain - * @tags fm, envelope, superdough, supradough + * @tags fm, envelope, superdough * @synonyms fmsus * @param {number | Pattern} level sustain level * @synonyms fmsus @@ -708,7 +708,7 @@ export const { * any of the 8 individual FMs (e.g. `fmrel8`) * * @name fmrelease - * @tags fm, envelope, superdough, supradough + * @tags fm, envelope, superdough * @synonyms fmrel * @param {number | Pattern} time release time * @@ -778,7 +778,7 @@ export const { fft } = registerControl('fft'); * Amplitude envelope attack time: Specifies how long it takes for the sound to reach its peak value, relative to the onset. * * @name attack - * @tags amplitude, envelope, superdough, supradough + * @tags amplitude, envelope, superdough * @param {number | Pattern} attack time in seconds. * @synonyms att * @example @@ -792,7 +792,7 @@ export const { attack, att } = registerControl('attack', 'att'); * Note that the decay is only audible if the sustain value is lower than 1. * * @name decay - * @tags amplitude, envelope, superdough, supradough + * @tags amplitude, envelope, superdough * @param {number | Pattern} time decay time in seconds * @synonyms dec * @example @@ -804,7 +804,7 @@ export const { decay, dec } = registerControl('decay', 'dec'); * Amplitude envelope sustain level: The level which is reached after attack / decay, being sustained until the offset. * * @name sustain - * @tags amplitude, envelope, superdough, supradough + * @tags amplitude, envelope, superdough * @param {number | Pattern} gain sustain level between 0 and 1 * @synonyms sus * @example @@ -816,7 +816,7 @@ export const { sustain, sus } = registerControl('sustain', 'sus'); * Amplitude envelope release time: The time it takes after the offset to go from sustain level to zero. * * @name release - * @tags amplitude, envelope, superdough, supradough + * @tags amplitude, envelope, superdough * @param {number | Pattern} time release time in seconds * @synonyms rel * @example @@ -831,7 +831,7 @@ export const { hold } = registerControl('hold'); * can also optionally supply the 'bpq' parameter separated by ':'. * * @name bpf - * @tags filter, superdough, supradough + * @tags filter, superdough * @param {number | Pattern} frequency center frequency * @synonyms bandf, bp * @example @@ -844,7 +844,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], ' * Sets the **b**and-**p**ass **q**-factor (resonance). * * @name bpq - * @tags filter, superdough, supradough + * @tags filter, superdough * @param {number | Pattern} q q factor * @synonyms bandq * @example @@ -923,7 +923,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope'); * Bit crusher effect. * * @name crush - * @tags superdough, supradough + * @tags superdough * @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction). * @example * s(",hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>") @@ -935,7 +935,7 @@ export const { crush } = registerControl('crush'); * Fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers * * @name coarse - * @tags superdough, supradough + * @tags superdough * @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on. * @example * s("bd sd [~ bd] sd,hh*8").coarse("<1 4 8 16 32>") @@ -1294,7 +1294,7 @@ export const { cut } = registerControl('cut'); * When using mininotation, you can also optionally add the 'lpq' parameter, separated by ':'. * * @name lpf - * @tags filter, superdough, supradough + * @tags filter, superdough * @param {number | Pattern} frequency audible between 0 and 20000 * @synonyms cutoff, ctf, lp * @example @@ -1308,7 +1308,7 @@ export const { cutoff, ctf, lpf, lp } = registerControl(['cutoff', 'resonance', /** * Sets the lowpass filter envelope modulation depth. * @name lpenv - * @tags filter, envelope, superdough, supradough + * @tags filter, envelope, superdough * @param {number | Pattern} modulation depth of the lowpass filter envelope between 0 and _n_ * @synonyms lpe * @example @@ -1322,7 +1322,7 @@ export const { lpenv, lpe } = registerControl('lpenv', 'lpe'); /** * Sets the highpass filter envelope modulation depth. * @name hpenv - * @tags filter, envelope, superdough, supradough + * @tags filter, envelope, superdough * @param {number | Pattern} modulation depth of the highpass filter envelope between 0 and _n_ * @synonyms hpe * @example @@ -1336,7 +1336,7 @@ export const { hpenv, hpe } = registerControl('hpenv', 'hpe'); /** * Sets the bandpass filter envelope modulation depth. * @name bpenv - * @tags filter, envelope, superdough, supradough + * @tags filter, envelope, superdough * @param {number | Pattern} modulation depth of the bandpass filter envelope between 0 and _n_ * @synonyms bpe * @example @@ -1350,7 +1350,7 @@ export const { bpenv, bpe } = registerControl('bpenv', 'bpe'); /** * Sets the attack duration for the lowpass filter envelope. * @name lpattack - * @tags filter, envelope, superdough, supradough + * @tags filter, envelope, superdough * @param {number | Pattern} attack time of the filter envelope * @synonyms lpa * @example @@ -1364,7 +1364,7 @@ export const { lpattack, lpa } = registerControl('lpattack', 'lpa'); /** * Sets the attack duration for the highpass filter envelope. * @name hpattack - * @tags filter, envelope, superdough, supradough + * @tags filter, envelope, superdough * @param {number | Pattern} attack time of the highpass filter envelope * @synonyms hpa * @example @@ -1378,7 +1378,7 @@ export const { hpattack, hpa } = registerControl('hpattack', 'hpa'); /** * Sets the attack duration for the bandpass filter envelope. * @name bpattack - * @tags filter, envelope, superdough, supradough + * @tags filter, envelope, superdough * @param {number | Pattern} attack time of the bandpass filter envelope * @synonyms bpa * @example @@ -1392,7 +1392,7 @@ export const { bpattack, bpa } = registerControl('bpattack', 'bpa'); /** * Sets the decay duration for the lowpass filter envelope. * @name lpdecay - * @tags filter, envelope, superdough, supradough + * @tags filter, envelope, superdough * @param {number | Pattern} decay time of the filter envelope * @synonyms lpd * @example @@ -1406,7 +1406,7 @@ export const { lpdecay, lpd } = registerControl('lpdecay', 'lpd'); /** * Sets the decay duration for the highpass filter envelope. * @name hpdecay - * @tags filter, envelope, superdough, supradough + * @tags filter, envelope, superdough * @param {number | Pattern} decay time of the highpass filter envelope * @synonyms hpd * @example @@ -1421,7 +1421,7 @@ export const { hpdecay, hpd } = registerControl('hpdecay', 'hpd'); /** * Sets the decay duration for the bandpass filter envelope. * @name bpdecay - * @tags filter, envelope, superdough, supradough + * @tags filter, envelope, superdough * @param {number | Pattern} decay time of the bandpass filter envelope * @synonyms bpd * @example @@ -1436,7 +1436,7 @@ export const { bpdecay, bpd } = registerControl('bpdecay', 'bpd'); /** * Sets the sustain amplitude for the lowpass filter envelope. * @name lpsustain - * @tags filter, envelope, superdough, supradough + * @tags filter, envelope, superdough * @param {number | Pattern} sustain amplitude of the lowpass filter envelope * @synonyms lps * @example @@ -1451,7 +1451,7 @@ export const { lpsustain, lps } = registerControl('lpsustain', 'lps'); /** * Sets the sustain amplitude for the highpass filter envelope. * @name hpsustain - * @tags filter, envelope, superdough, supradough + * @tags filter, envelope, superdough * @param {number | Pattern} sustain amplitude of the highpass filter envelope * @synonyms hps * @example @@ -1466,7 +1466,7 @@ export const { hpsustain, hps } = registerControl('hpsustain', 'hps'); /** * Sets the sustain amplitude for the bandpass filter envelope. * @name bpsustain - * @tags filter, envelope, superdough, supradough + * @tags filter, envelope, superdough * @param {number | Pattern} sustain amplitude of the bandpass filter envelope * @synonyms bps * @example @@ -1481,7 +1481,7 @@ export const { bpsustain, bps } = registerControl('bpsustain', 'bps'); /** * Sets the release time for the lowpass filter envelope. * @name lprelease - * @tags filter, envelope, superdough, supradough + * @tags filter, envelope, superdough * @param {number | Pattern} release time of the filter envelope * @synonyms lpr * @example @@ -1497,7 +1497,7 @@ export const { lprelease, lpr } = registerControl('lprelease', 'lpr'); /** * Sets the release time for the highpass filter envelope. * @name hprelease - * @tags filter, envelope, superdough, supradough + * @tags filter, envelope, superdough * @param {number | Pattern} release time of the highpass filter envelope * @synonyms hpr * @example @@ -1513,7 +1513,7 @@ export const { hprelease, hpr } = registerControl('hprelease', 'hpr'); /** * Sets the release time for the bandpass filter envelope. * @name bprelease - * @tags filter, envelope, superdough, supradough + * @tags filter, envelope, superdough * @param {number | Pattern} release time of the bandpass filter envelope * @synonyms bpr * @example @@ -1558,7 +1558,7 @@ export const { fanchor } = registerControl('fanchor'); * When using mininotation, you can also optionally add the 'hpq' parameter, separated by ':'. * * @name hpf - * @tags filter, superdough, supradough + * @tags filter, superdough * @param {number | Pattern} frequency audible between 0 and 20000 * @synonyms hp, hcutoff * @example @@ -1781,7 +1781,7 @@ export const { hpskew } = registerControl('hpskew'); * Applies a vibrato to the frequency of the oscillator. * * @name vib - * @tags pitch, lfo, superdough, supradough + * @tags pitch, lfo, superdough * @synonyms vibrato, v * @param {number | Pattern} frequency of the vibrato in hertz * @example @@ -1799,7 +1799,7 @@ export const { vib, vibrato, v } = registerControl(['vib', 'vibmod'], 'vibrato', * Adds pink noise to the mix * * @name noise - * @tags generators, superdough, supradough + * @tags generators, superdough * @param {number | Pattern} wet wet amount * @example * sound("/2") @@ -1809,7 +1809,7 @@ export const { noise } = registerControl('noise'); * Sets the vibrato depth in semitones. Only has an effect if `vibrato` | `vib` | `v` is is also set * * @name vibmod - * @tags pitch, lfo, superdough, supradough + * @tags pitch, lfo, superdough * @synonyms vmod * @param {number | Pattern} depth of vibrato (in semitones) * @example @@ -1828,7 +1828,7 @@ export const { hcutoff, hpf, hp } = registerControl(['hcutoff', 'hresonance', 'h * Controls the **h**igh-**p**ass **q**-value. * * @name hpq - * @tags filter, superdough, supradough + * @tags filter, superdough * @param {number | Pattern} q resonance factor between 0 and 50 * @synonyms hresonance * @example @@ -1840,7 +1840,7 @@ export const { hresonance, hpq } = registerControl('hresonance', 'hpq'); * Controls the **l**ow-**p**ass **q**-value. * * @name lpq - * @tags filter, superdough, supradough + * @tags filter, superdough * @param {number | Pattern} q resonance factor between 0 and 50 * @synonyms resonance * @example @@ -1870,7 +1870,7 @@ export const { djf } = registerControl('djf'); * * * @name delay - * @tags orbit, superdough, supradough + * @tags orbit, superdough * @param {number | Pattern} level between 0 and 1 * @example * s("bd bd").delay("<0 .25 .5 1>") @@ -1884,7 +1884,7 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback'] * Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it * * @name delayfeedback - * @tags orbit, superdough, supradough + * @tags orbit, superdough * @param {number | Pattern} feedback between 0 and 1 * @synonyms delayfb, dfb * @example @@ -1893,24 +1893,11 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback'] */ export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb'); -/** - * Sets the time of the delay effect. - * - * @name delayspeed - * @tags supradough - * @param {number | Pattern} delayspeed controls the pitch of the delay feedback - * @synonyms delayt, dt - * @example - * note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>") - * - */ -export const { delayspeed } = registerControl('delayspeed'); - /** * Sets the time of the delay effect in seconds. * * @name delaytime - * @tags orbit, superdough, supradough + * @tags orbit, superdough * @param {number | Pattern} delay in seconds * @synonyms delayt, dt * @example @@ -2026,7 +2013,7 @@ export const { freq } = registerControl('freq'); * Attack time of pitch envelope. * * @name pattack - * @tags pitch, envelope, superdough, supradough + * @tags pitch, envelope, superdough * @synonyms patt * @param {number | Pattern} time time in seconds * @example @@ -2038,7 +2025,7 @@ export const { pattack, patt } = registerControl('pattack', 'patt'); * Decay time of pitch envelope. * * @name pdecay - * @tags pitch, envelope, superdough, supradough + * @tags pitch, envelope, superdough * @synonyms pdec * @param {number | Pattern} time time in seconds * @example @@ -2052,7 +2039,7 @@ export const { psustain, psus } = registerControl('psustain', 'psus'); * Release time of pitch envelope * * @name prelease - * @tags pitch, envelope, superdough, supradough + * @tags pitch, envelope, superdough * @synonyms prel * @param {number | Pattern} time time in seconds * @example @@ -2067,7 +2054,7 @@ export const { prelease, prel } = registerControl('prelease', 'prel'); * If you don't set other pitch envelope controls, `pattack:.2` will be the default. * * @name penv - * @tags pitch, envelope, superdough, supradough + * @tags pitch, envelope, superdough * @param {number | Pattern} semitones change in semitones * @example * note("c") @@ -2248,7 +2235,7 @@ export const { overshape } = registerControl('overshape'); * Sets position in stereo. * * @name pan - * @tags superdough, supradough + * @tags superdough * @param {number | Pattern} pan between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel) * @example * s("[bd hh]*2").pan("<.5 1 .5 0>") @@ -2506,7 +2493,7 @@ export const { shape } = registerControl(['shape', 'shapevol']); * Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;) * * @name distort - * @tags distortion, superdough, supradough + * @tags distortion, superdough * @synonyms dist * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion @@ -2526,7 +2513,7 @@ export const { distort, dist } = registerControl(['distort', 'distortvol', 'dist * * @name distortvol * @synonyms distortion, distvol - * @tags superdough, supradough + * @tags superdough * @param {number | Pattern} volume linear postgain of the distortion * @example * s("bd*4").bank("tr909").distort(2).distortvol(0.8) @@ -2537,7 +2524,7 @@ export const { distortvol } = registerControl('distortvol', 'distvol'); * Type of waveshaping distortion to apply. * * @name distorttype - * @tags distortion, superdough, supradough + * @tags distortion, superdough * @synonyms disttype * @param {number | string | Pattern} type type of distortion to apply * @example diff --git a/packages/supradough/.gitignore b/packages/supradough/.gitignore deleted file mode 100644 index d21cbdf3e..000000000 --- a/packages/supradough/.gitignore +++ /dev/null @@ -1 +0,0 @@ -pattern.wav diff --git a/packages/supradough/README.md b/packages/supradough/README.md deleted file mode 100644 index a8cfa84b3..000000000 --- a/packages/supradough/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# supradough - -platform agnostic synth and sampler intended for live coding. a reimplementation of superdough. \ No newline at end of file diff --git a/packages/supradough/dough-export.mjs b/packages/supradough/dough-export.mjs deleted file mode 100644 index bd4b530b3..000000000 --- a/packages/supradough/dough-export.mjs +++ /dev/null @@ -1,123 +0,0 @@ -// this is a poc of how a pattern can be rendered as a wav file using node -// run via: node dough-export.mjs -import fs from 'node:fs'; -import WavEncoder from 'wav-encoder'; -import { evalScope } from '@strudel/core'; -import { miniAllStrings } from '@strudel/mini'; -import { Dough } from './dough.mjs'; - -await evalScope( - import('@strudel/core'), - import('@strudel/mini'), - import('@strudel/tonal'), - // import('@strudel/tonal'), -); - -miniAllStrings(); // allows using single quotes for mini notation / skip transpilation - -let sampleRate = 48000, - cps = 0.4; - -/* await doughsamples('github:eddyflux/crate'); -await doughsamples('github:eddyflux/wax'); */ - -let pat = note('c,eb,g,') - .s('sine') - .press() - .add(note(24)) - .fmi(3) - .fmh(5.01) - .dec(0.4) - .delay('.6:<.12 .22>:.8') - .jux(press) - .rarely(add(note('12'))) - .lpf(400) - .lpq(0.2) - .lpd(0.4) - .lpenv(3) - .fmdecay(0.4) - .fmenv(1) - .postgain(0.6) - .stack(s('*8').dec(0.07).rarely(ply('2')).delay(0.5).hpf(sine.range(200, 2000).slow(4)).hpq(0.2)) - .stack( - s('[- white@3]*2') - .dec(0.4) - .hpf('<2000!3 <4000 8000>>*4') - .hpq(0.6) - .ply('<1 2>*4') - .postgain(0.5) - .delay(0.5) - .jux(rev) - .lpf(5000), - ) - .stack( - note('*2') - .s('square') - .lpf(sine.range(100, 300).slow(4)) - .lpe(1) - .segment(8) - .lpd(0.3) - .lpq(0.2) - .dec(0.2) - .speed('<1 2>') - .ply('<1 2>') - .postgain(1), - ) - .stack( - chord('') - .voicing() - .s('') - .clip(1) - .rel(0.4) - .vib('4:.2') - .gain(0.7) - .hpf(1200) - .fm(0.5) - .att(1) - .lpa(0.5) - .lpf(200) - .lpenv(4) - .chorus(0.8), - ) - .slow(1 / cps); - -let cycles = 30; -let seconds = cycles + 1; // 1s release tail -const haps = pat.queryArc(0, cycles); - -const dough = new Dough(sampleRate); - -console.log('spawn voices...'); -haps.forEach((hap) => { - hap.value._begin = Number(hap.whole.begin); - hap.value._duration = hap.duration /* / cps */; - dough.scheduleSpawn(hap.value); -}); -console.log(`render ${seconds}s long buffer, each dot is 1 second:`); -const buffers = [new Float32Array(seconds * sampleRate), new Float32Array(seconds * sampleRate)]; -let t = performance.now(); -while (dough.t <= buffers[0].length) { - dough.update(); - buffers[0][dough.t] = dough.out[0]; - buffers[1][dough.t] = dough.out[1]; - if (dough.t % sampleRate === 0) { - process.stdout.write('.'); - } -} -const took = (performance.now() - t) / 1000; -const load = (took / seconds) * 100; -const speed = (seconds / took).toFixed(2); -console.log(''); -console.log(`done! -rendered ${seconds}s in ${took.toFixed(2)}s -speed: ${speed}x -load: ${load.toFixed(2)}%`); - -const patternAudio = { - sampleRate, - channelData: buffers, -}; - -WavEncoder.encode(patternAudio).then((buffer) => { - fs.writeFileSync('pattern.wav', new Float32Array(buffer)); -}); diff --git a/packages/supradough/dough-worklet.mjs b/packages/supradough/dough-worklet.mjs deleted file mode 100644 index 18c316b2d..000000000 --- a/packages/supradough/dough-worklet.mjs +++ /dev/null @@ -1,39 +0,0 @@ -import { Dough } from './dough.mjs'; - -const clamp = (num, min, max) => Math.min(Math.max(num, min), max); - -class DoughProcessor extends AudioWorkletProcessor { - constructor() { - super(); - this.dough = new Dough(sampleRate, currentTime); - this.port.onmessage = (event) => { - if (event.data.spawn) { - this.dough.scheduleSpawn(event.data.spawn); - } else if (event.data.sample) { - this.dough.loadSample(event.data.sample, event.data.channels, event.data.sampleRate); - } else if (event.data.samples) { - event.data.samples.forEach(([name, channels, sampleRate]) => { - this.dough.loadSample(name, channels, sampleRate); - }); - } else { - console.log('unrecognized event type', event.data); - } - }; - } - process(inputs, outputs, params) { - if (this.disconnected) { - return false; - } - const output = outputs[0]; - for (let i = 0; i < output[0].length; i++) { - this.dough.update(); - for (let c = 0; c < output.length; c++) { - //prevent speaker blowout via clipping if threshold exceeds - output[c][i] = clamp(this.dough.out[c], -1, 1); - } - } - return true; // keep the audio processing going - } -} - -registerProcessor('dough-processor', DoughProcessor); diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs deleted file mode 100644 index f7eae6ef5..000000000 --- a/packages/supradough/dough.mjs +++ /dev/null @@ -1,1119 +0,0 @@ -// this is dough, the superdough without dependencies -// @ts-nocheck -// @ts-ignore ignore next line because sampleRate is unknown -const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000; -const PI_DIV_SR = Math.PI / SAMPLE_RATE; -const ISR = 1 / SAMPLE_RATE; - -let gainCurveFunc = (val) => Math.pow(val, 2); -const clamp = (num, min, max) => Math.min(Math.max(num, min), max); - -function applyGainCurve(val) { - return gainCurveFunc(val); -} - -/** - * Equal Power Crossfade function. - * Smoothly transitions between signals A and B, maintaining consistent perceived loudness. - * - * @param {number} a - Signal A (can be a single value or an array value in buffer processing). - * @param {number} b - Signal B (can be a single value or an array value in buffer processing). - * @param {number} m - Crossfade parameter (0.0 = all A, 1.0 = all B, 0.5 = equal mix). - * @tags internals - * @returns {number} Crossfaded output value. - */ -function crossfade(a, b, m) { - const aGain = Math.sin((1 - m) * 0.5 * Math.PI); - const bGain = Math.sin(m * 0.5 * Math.PI); - return a * aGain + b * bGain; -} - -// function setGainCurve(newGainCurveFunc) { -// gainCurveFunc = newGainCurveFunc; -// } -// https://garten.salat.dev/audio-DSP/oscillators.html -export class SineOsc { - phase = 0; - update(freq) { - const value = Math.sin(this.phase * 2 * Math.PI); - this.phase = (this.phase + freq / SAMPLE_RATE) % 1; - return value; - } -} - -export class ZawOsc { - phase = 0; - update(freq) { - this.phase += ISR * freq; - return (this.phase % 1) * 2 - 1; - } -} - -function polyBlep(t, dt) { - // 0 <= t < 1 - if (t < dt) { - t /= dt; - // 2 * (t - t^2/2 - 0.5) - return t + t - t * t - 1; - } - // -1 < t < 0 - if (t > 1 - dt) { - t = (t - 1) / dt; - // 2 * (t^2/2 + t + 0.5) - return t * t + t + t + 1; - } - // 0 otherwise - return 0; -} - -export class SawOsc { - constructor(props = {}) { - this.phase = props.phase ?? 0; - } - update(freq) { - const dt = freq / SAMPLE_RATE; - let p = polyBlep(this.phase, dt); - let s = 2 * this.phase - 1 - p; - this.phase += dt; - if (this.phase > 1) { - this.phase -= 1; - } - return s; - } -} - -function getUnisonDetune(unison, detune, voiceIndex) { - if (unison < 2) { - return 0; - } - const lerp = (a, b, n) => { - return n * (b - a) + a; - }; - return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1)); -} -function applySemitoneDetuneToFrequency(frequency, detune) { - return frequency * Math.pow(2, detune / 12); -} -export class SupersawOsc { - constructor(props = {}) { - //TODO: figure out a good way to pass in these params - this.voices = props.voices ?? 5; - this.freqspread = props.freqspread ?? 0.2; - this.panspread = props.panspread ?? 0.4; - this.phase = new Float32Array(this.voices).map(() => Math.random()); - } - update(freq) { - const gain1 = Math.sqrt(1 - this.panspread); - const gain2 = Math.sqrt(this.panspread); - let sl = 0; - let sr = 0; - for (let n = 0; n < this.voices; n++) { - const freqAdjusted = applySemitoneDetuneToFrequency(freq, getUnisonDetune(this.voices, this.freqspread, n)); - const dt = freqAdjusted / SAMPLE_RATE; - const isOdd = (n & 1) == 1; - let gainL = gain1; - let gainR = gain2; - // invert right and left gain - if (isOdd) { - gainL = gain2; - gainR = gain1; - } - let p = polyBlep(this.phase[n], dt); - let s = 2 * this.phase[n] - 1 - p; - sl = sl + s * gainL; - sr = sr + s * gainL; - - this.phase[n] += dt; - if (this.phase[n] > 1) { - this.phase[n] -= 1; - } - } - - return sl + sr; - //TODO: make stereo - // return [sl, sr]; - } -} - -export class TriOsc { - phase = 0; - update(freq) { - this.phase += ISR * freq; - let phase = this.phase % 1; - let value = phase < 0.5 ? 2 * phase : 1 - 2 * (phase - 0.5); - return value * 2 - 1; - } -} - -export class TwoPoleFilter { - s0 = 0; - s1 = 0; - update(s, cutoff, resonance = 0) { - // Out of bound values can produce NaNs - resonance = Math.max(resonance, 0); - - cutoff = Math.min(cutoff, 20000); - let c = 2 * Math.sin(cutoff * PI_DIV_SR); - c = clamp(c, 0, 1.14); // this line prevents instability TODO: test - - const r = Math.pow(0.5, (resonance + 0.125) / 0.125); - const mrc = 1 - r * c; - - this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf - this.s1 = mrc * this.s1 + c * this.s0; // lpf - return this.s1; // return lpf by default - } -} - -class PulseOsc { - constructor(phase = 0) { - this.phase = phase; - } - saw(offset, dt) { - let phase = (this.phase + offset) % 1; - let p = polyBlep(phase, dt); - return 2 * phase - 1 - p; - } - update(freq, pw = 0.5) { - const dt = freq / SAMPLE_RATE; - let pulse = this.saw(0, dt) - this.saw(pw, dt); - this.phase = (this.phase + dt) % 1; - return pulse + pw * 2 - 1; - } -} - -// non bandlimited (has aliasing) -export class PulzeOsc { - phase = 0; - update(freq, duty = 0.5) { - this.phase += ISR * freq; - let cyclePos = this.phase % 1; - return cyclePos < duty ? 1 : -1; - } -} - -export class Dust { - update = (density) => (Math.random() < density * ISR ? Math.random() : 0); -} - -export class WhiteNoise { - update() { - return Math.random() * 2 - 1; - } -} - -export class BrownNoise { - constructor() { - this.out = 0; - } - update() { - let white = Math.random() * 2 - 1; - this.out = (this.out + 0.02 * white) / 1.02; - return this.out; - } -} - -export class PinkNoise { - constructor() { - this.b0 = 0; - this.b1 = 0; - this.b2 = 0; - this.b3 = 0; - this.b4 = 0; - this.b5 = 0; - this.b6 = 0; - } - - update() { - const white = Math.random() * 2 - 1; - - this.b0 = 0.99886 * this.b0 + white * 0.0555179; - this.b1 = 0.99332 * this.b1 + white * 0.0750759; - this.b2 = 0.969 * this.b2 + white * 0.153852; - this.b3 = 0.8665 * this.b3 + white * 0.3104856; - this.b4 = 0.55 * this.b4 + white * 0.5329522; - this.b5 = -0.7616 * this.b5 - white * 0.016898; - - const pink = this.b0 + this.b1 + this.b2 + this.b3 + this.b4 + this.b5 + this.b6 + white * 0.5362; - this.b6 = white * 0.115926; - - return pink * 0.11; - } -} - -export class Impulse { - phase = 1; - update(freq) { - this.phase += ISR * freq; - let v = this.phase >= 1 ? 1 : 0; - this.phase = this.phase % 1; - return v; - } -} - -export class ClockDiv { - inSgn = true; - outSgn = true; - clockCnt = 0; - update(clock, factor) { - let curSgn = clock > 0; - if (this.inSgn != curSgn) { - this.clockCnt++; - if (this.clockCnt >= factor) { - this.clockCnt = 0; - this.outSgn = !this.outSgn; - } - } - - this.inSgn = curSgn; - return this.outSgn ? 1 : -1; - } -} - -export class Hold { - value = 0; - trigSgn = false; - update(input, trig) { - if (!this.trigSgn && trig > 0) this.value = input; - this.trigSgn = trig > 0; - return this.value; - } -} - -function lerp(x, y0, y1, exponent = 1) { - if (x <= 0) return y0; - if (x >= 1) return y1; - - let curvedX; - - if (exponent === 0) { - curvedX = x; // linear - } else if (exponent > 0) { - curvedX = Math.pow(x, exponent); // ease-in - } else { - curvedX = 1 - Math.pow(1 - x, -exponent); // ease-out - } - - return y0 + (y1 - y0) * curvedX; -} - -export class ADSR { - constructor(props = {}) { - this.state = 'off'; - this.startTime = 0; - this.startVal = 0; - this.decayCurve = props.decayCurve ?? 1; - } - - update(curTime, gate, attack, decay, susVal, release) { - switch (this.state) { - case 'off': { - if (gate > 0) { - this.state = 'attack'; - this.startTime = curTime; - this.startVal = 0; - } - return 0; - } - case 'attack': { - let time = curTime - this.startTime; - if (time > attack) { - this.state = 'decay'; - this.startTime = curTime; - return 1; - } - return lerp(time / attack, this.startVal, 1, 1); - } - case 'decay': { - let time = curTime - this.startTime; - let curVal = lerp(time / decay, 1, susVal, -this.decayCurve); - if (gate <= 0) { - this.state = 'release'; - this.startTime = curTime; - this.startVal = curVal; - return curVal; - } - if (time > decay) { - this.state = 'sustain'; - this.startTime = curTime; - return susVal; - } - return curVal; - } - case 'sustain': { - if (gate <= 0) { - this.state = 'release'; - this.startTime = curTime; - this.startVal = susVal; - } - return susVal; - } - case 'release': { - let time = curTime - this.startTime; - - if (time > release) { - this.state = 'off'; - return 0; - } - let curVal = lerp(time / release, this.startVal, 0, -this.decayCurve); - if (gate > 0) { - this.state = 'attack'; - this.startTime = curTime; - this.startVal = curVal; - } - return curVal; - } - } - throw 'invalid envelope state'; - } -} - -/* - impulse(1).ad(.1).mul(sine(200)) -.add(x=>x.delay(.1).mul(.8)) -.out()*/ -const MAX_DELAY_TIME = 10; -export class PitchDelay { - lpf = new TwoPoleFilter(); - constructor(_props = {}) { - this.buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); - this.writeIdx = 0; - this.readIdx = 0; - this.numSamples = 0; - } - write(s, delayTime) { - // Calculate how far in the past to read - this.numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1); - this.writeIdx = (this.writeIdx + 1) % this.numSamples; - this.buffer[this.writeIdx] = s; - this.readIdx = this.writeIdx - this.numSamples + 1; - - // If past the start of the buffer, wrap around (Q: is this possible?) - if (this.readIdx < 0) this.readIdx += this.numSamples; - } - update(input, delayTime, speed = 1) { - this.write(input, delayTime); - let index = this.readIdx; - if (speed < 0) { - index = this.numSamples - Math.floor(Math.abs(this.readIdx * speed) % this.numSamples); - } else { - index = Math.floor(this.readIdx * speed) % this.numSamples; - } - const s = this.lpf.update(this.buffer[index], 0.9, 0); - - return s; - } -} - -export class Delay { - writeIdx = 0; - readIdx = 0; - buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); //.fill(0) - write(s, delayTime) { - this.writeIdx = (this.writeIdx + 1) % this.buffer.length; - this.buffer[this.writeIdx] = s; - // Calculate how far in the past to read - let numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1); - this.readIdx = this.writeIdx - numSamples; - // If past the start of the buffer, wrap around - if (this.readIdx < 0) this.readIdx += this.buffer.length; - } - update(input, delayTime) { - this.write(input, delayTime); - return this.buffer[this.readIdx]; - } -} -//TODO: Figure out why clicking at the start off the buffer -export class Chorus { - delay = new Delay(); - modulator = new TriOsc(); - update(input, mix, delayTime, modulationFreq, modulationDepth) { - const m = this.modulator.update(modulationFreq) * modulationDepth; - const c = this.delay.update(input, delayTime * (1 + m)); - return crossfade(input, c, mix); - } -} - -export class Fold { - update(input = 0, rate = 0) { - if (rate < 0) rate = 0; - rate = rate + 1; - input = input * rate; - return 4 * (Math.abs(0.25 * input + 0.25 - Math.round(0.25 * input + 0.25)) - 0.25); - } -} - -export class Lag { - lagUnit = 4410; - s = 0; - update(input, rate) { - // Remap so the useful range is around [0, 1] - rate = rate * this.lagUnit; - if (rate < 1) rate = 1; - this.s += (1 / rate) * (input - this.s); - return this.s; - } -} - -export class Slew { - last = 0; - update(input, up, dn) { - const upStep = up * ISR; - const downStep = dn * ISR; - let delta = input - this.last; - if (delta > upStep) { - delta = upStep; - } else if (delta < -downStep) { - delta = -downStep; - } - this.last += delta; - return this.last; - } -} - -// overdrive style distortion (adapted from noisecraft) currently unused -export function applyDistortion(x, amount) { - amount = Math.min(Math.max(amount, 0), 1); - amount -= 0.01; - var k = (2 * amount) / (1 - amount); - var y = ((1 + k) * x) / (1 + k * Math.abs(x)); - return y; -} - -export class Sequence { - clockSgn = true; - step = 0; - first = true; - update(clock, ...ins) { - if (!this.clockSgn && clock > 0) { - this.step = (this.step + 1) % ins.length; - this.clockSgn = clock > 0; - return 0; // set first sample to zero to retrigger gates on step change... - } - this.clockSgn = clock > 0; - return ins[this.step]; - } -} - -// sample rate bit crusher -export class Coarse { - hold = 0; - t = 0; - update(input, coarse) { - if (this.t++ % coarse === 0) { - this.t = 0; - this.hold = input; - } - return this.hold; - } -} - -// amplitude bit crusher -export class Crush { - update(input, crush) { - crush = Math.max(1, crush); - const x = Math.pow(2, crush - 1); - return Math.round(input * x) / x; - } -} - -// this is the distort from superdough -export class Distort { - update(input, distort = 0, postgain = 1) { - postgain = Math.max(0.001, Math.min(1, postgain)); - const shape = Math.expm1(distort); - return (((1 + shape) * input) / (1 + shape * Math.abs(input))) * postgain; - } -} -// distortion could be expressed as a function, because it's stateless - -export class BufferPlayer { - static samples = new Map(); // string -> { channels, sampleRate } - buffer; // Float32Array - sampleRate; - pos = 0; - sampleFreq = note2freq(); - constructor(buffer, sampleRate, normalize) { - this.buffer = buffer; - this.sampleRate = sampleRate; - this.duration = this.buffer.length / this.sampleRate; - this.speed = SAMPLE_RATE / this.sampleRate; - if (normalize) { - // this will make the buffer last 1s if freq = sampleFreq - // it's useful to loop samples (e.g. fit function) - this.speed *= this.duration; - } - } - update(freq) { - if (this.pos >= this.buffer.length) { - return 0; - } - const speed = (freq / this.sampleFreq) * this.speed; - let s = this.buffer[Math.floor(this.pos)]; - this.pos = this.pos + speed; - return s; - } -} - -export function _rangex(sig, min, max) { - let logmin = Math.log(min); - let range = Math.log(max) - logmin; - const unipolar = (sig + 1) / 2; - return Math.exp(unipolar * range + logmin); -} - -// duplicate -export const getADSR = (params, curve = 'linear', defaultValues) => { - const envmin = curve === 'exponential' ? 0.001 : 0.001; - const releaseMin = 0.01; - const envmax = 1; - const [a, d, s, r] = params; - if (a == null && d == null && s == null && r == null) { - return defaultValues ?? [envmin, envmin, envmax, releaseMin]; - } - const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? envmax : envmin; - return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)]; -}; - -let shapes = { - sine: SineOsc, - saw: SawOsc, - zaw: ZawOsc, - sawtooth: SawOsc, - zawtooth: ZawOsc, - supersaw: SupersawOsc, - tri: TriOsc, - triangle: TriOsc, - pulse: PulseOsc, - square: PulseOsc, - pulze: PulzeOsc, - dust: Dust, - crackle: Dust, - impulse: Impulse, - white: WhiteNoise, - brown: BrownNoise, - pink: PinkNoise, -}; - -const defaultDefaultValues = { - chorus: 0, - note: 48, - s: 'triangle', - bank: '', - gain: 1, - postgain: 1, - velocity: 1, - density: '.03', - ftype: '12db', - fanchor: 0, - //resonance: 1, // superdough resonance is scaled differently - resonance: 0, - //hresonance: 1, // superdough resonance is scaled differently - hresonance: 0, - // bandq: 1, // superdough resonance is scaled differently - bandq: 0, - channels: [1, 2], - phaserdepth: 0.75, - shapevol: 1, - distortvol: 1, - delay: 0, - byteBeatExpression: '0', - delayfeedback: 0.5, - delayspeed: 1, - delaytime: 0.25, - orbit: 1, - i: 1, - fft: 8, - z: 'triangle', - pan: 0.5, - fmh: 1, - fmenv: 0, // differs from superdough - speed: 1, - pw: 0.5, -}; - -let getDefaultValue = (key) => defaultDefaultValues[key]; - -const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }; -const accs = { '#': 1, b: -1, s: 1, f: -1 }; -const note2midi = (note, defaultOctave = 3) => { - let [pc, acc = '', oct = ''] = - String(note) - .match(/^([a-gA-G])([#bsf]*)([0-9]*)$/) - ?.slice(1) || []; - if (!pc) { - throw new Error('not a note: "' + note + '"'); - } - const chroma = chromas[pc.toLowerCase()]; - const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0; - const octave = Number(oct || defaultOctave); - return (octave + 1) * 12 + chroma + offset; -}; -const midi2freq = (midi) => Math.pow(2, (midi - 69) / 12) * 440; -const note2freq = (note) => { - note = note || getDefaultValue('note'); - if (typeof note === 'string') { - note = note2midi(note, 3); // e.g. c3 => 48 - } - return midi2freq(note); -}; - -export class DoughVoice { - /** @type {number} */ - id = 0; - /** @type {number[]} */ - out = [0, 0]; - - /** @type {number | undefined} */ - attack; - /** @type {number | undefined} */ - decay; - /** @type {number | undefined} */ - sustain; - /** @type {number} */ - release; - /** @type {number} */ - _begin; - /** @type {number} */ - _duration; - - /** @type {any} */ - _sound; - /** @type {number} */ - _channels = 1; - /** @type {BufferPlayer[] | undefined} */ - _buffers; - /** @type {string | undefined} */ - unit; - - /** @type {ADSR | undefined} */ - _penv; - /** @type {number | undefined} */ - penv; - /** @type {number | undefined} */ - pattack; - /** @type {number | undefined} */ - pdecay; - /** @type {number | undefined} */ - psustain; - /** @type {number | undefined} */ - prelease; - - /** @type {number | undefined} */ - vib; - - _vib; - /** @type {number | undefined} */ - vibmod; - - /** @type {SineOsc | undefined} */ - _fm; - /** @type {number | undefined} */ - fmh; - /** @type {number | undefined} */ - fmi; - - /** @type {ADSR | undefined} */ - _fmenv; - /** @type {number | undefined} */ - fmattack; - /** @type {number | undefined} */ - fmdecay; - /** @type {number | undefined} */ - fmsustain; - /** @type {number | undefined} */ - fmrelease; - - /** @type {ADSR | undefined} */ - _lpenv; - lpenv; - /** @type {number | undefined} */ - lpattack; - /** @type {number | undefined} */ - lpdecay; - /** @type {number | undefined} */ - lpsustain; - /** @type {number | undefined} */ - lprelease; - - /** @type {ADSR | undefined} */ - _hpenv; - /** @type {number | undefined} */ - hpenv; - /** @type {number | undefined} */ - hpattack; - /** @type {number | undefined} */ - hpdecay; - /** @type {number | undefined} */ - hpsustain; - /** @type {number | undefined} */ - hprelease; - - /** @type {ADSR | undefined} */ - _bpenv; - /** @type {number | undefined} */ - bpenv; - /** @type {number | undefined} */ - bpattack; - /** @type {number | undefined} */ - bpdecay; - /** @type {number | undefined} */ - bpsustain; - /** @type {number | undefined} */ - bprelease; - - /** @type {number | undefined} */ - cutoff; - /** @type {number | undefined} */ - hcutoff; - /** @type {number | undefined} */ - bandf; - /** @type {number | undefined} */ - coarse; - /** @type {number | undefined} */ - crush; - /** @type {number | undefined} */ - distort; - - /** @type {number} */ - freq; - /** @type {string | undefined} */ - note; - - /** @type {TwoPoleFilter[] | null | undefined} */ - _lpf; - /** @type {TwoPoleFilter[] | null | undefined} */ - _hpf; - /** @type {TwoPoleFilter[] | null | undefined} */ - _bpf; - /** @type {Chorus[] | null | undefined} */ - _chorus; - /** @type {Coarse[] | null | undefined} */ - _coarse; - /** @type {Crush[] | null | undefined} */ - _crush; - /** @type {Distort[] | null | undefined} */ - _distort; - - /** - * @param {DoughVoice} value - */ - constructor(value) { - // mandatory controls - this.freq ??= note2freq(value.note); - this._begin = value._begin; - this._duration = value._duration; - this.release = value.release ?? 0; - // the rest.. we use $ for readability - let $ = this; - Object.assign($, value); - $.s = $.s ?? getDefaultValue('s'); - $.gain = applyGainCurve($.gain ?? getDefaultValue('gain')); - $.velocity = applyGainCurve($.velocity ?? getDefaultValue('velocity')); - $.postgain = applyGainCurve($.postgain ?? getDefaultValue('postgain')); - $.density = $.density ?? getDefaultValue('density'); - $.fanchor = $.fanchor ?? getDefaultValue('fanchor'); - $.drive = $.drive ?? 0.69; - $.phaserdepth = $.phaserdepth ?? getDefaultValue('phaserdepth'); - $.shapevol = applyGainCurve($.shapevol ?? getDefaultValue('shapevol')); - $.distortvol = applyGainCurve($.distortvol ?? getDefaultValue('distortvol')); - $.i = $.i ?? getDefaultValue('i'); - $.chorus = $.chorus ?? getDefaultValue('chorus'); - $.fft = $.fft ?? getDefaultValue('fft'); - $.pan = $.pan ?? getDefaultValue('pan'); - $.orbit = $.orbit ?? getDefaultValue('orbit'); - $.fmenv = $.fmenv ?? getDefaultValue('fmenv'); - $.resonance = $.resonance ?? getDefaultValue('resonance'); - $.hresonance = $.hresonance ?? getDefaultValue('hresonance'); - $.bandq = $.bandq ?? getDefaultValue('bandq'); - $.speed = $.speed ?? getDefaultValue('speed'); - $.pw = $.pw ?? getDefaultValue('pw'); - - [$.attack, $.decay, $.sustain, $.release] = getADSR([$.attack, $.decay, $.sustain, $.release]); - - $._holdEnd = $._begin + $._duration; // needed for gate - $._end = $._holdEnd + $.release + 0.01; // needed for despawn - - if ($.fmi && ($.s === 'saw' || $.s === 'sawtooth')) { - $.s = 'zaw'; // polyblepped saw when fm is applied - } - - if (shapes[$.s]) { - const SourceClass = shapes[$.s]; - $._sound = new SourceClass(); - $._channels = 1; - } else if (BufferPlayer.samples.has($.s)) { - const sample = BufferPlayer.samples.get($.s); - $._buffers = []; - $._channels = sample.channels.length; - for (let i = 0; i < $._channels; i++) { - $._buffers.push(new BufferPlayer(sample.channels[i], sample.sampleRate, $.unit === 'c')); // tbd unit === 'c' - } - } else { - console.warn('sound not loaded', $.s); - } - - if ($.penv) { - $._penv = new ADSR({ decayCurve: 4 }); - [$.pattack, $.pdecay, $.psustain, $.prelease] = getADSR([$.pattack, $.pdecay, $.psustain, $.prelease]); - } - - if ($.vib) { - $._vib = new SineOsc(); - $.vibmod = $.vibmod ?? getDefaultValue('vibmod'); - } - - if ($.fmi) { - $._fm = new SineOsc(); - $.fmh = $.fmh ?? getDefaultValue('fmh'); - if ($.fmenv) { - $._fmenv = new ADSR({ decayCurve: 2 }); - [$.fmattack, $.fmdecay, $.fmsustain, $.fmrelease] = getADSR([$.fmattack, $.fmdecay, $.fmsustain, $.fmrelease]); - } - } - - // gain envelope - $._adsr = new ADSR({ decayCurve: 2 }); - // delay - $.delay = applyGainCurve($.delay ?? getDefaultValue('delay')); - $.delayfeedback = $.delayfeedback ?? getDefaultValue('delayfeedback'); - $.delayspeed = $.delayspeed ?? getDefaultValue('delayspeed'); - $.delaytime = $.delaytime ?? getDefaultValue('delaytime'); - - // filter setup - if ($.lpenv) { - $._lpenv = new ADSR({ decayCurve: 4 }); - [$.lpattack, $.lpdecay, $.lpsustain, $.lprelease] = getADSR([$.lpattack, $.lpdecay, $.lpsustain, $.lprelease]); - } - if ($.hpenv) { - $._hpenv = new ADSR({ decayCurve: 4 }); - [$.hpattack, $.hpdecay, $.hpsustain, $.hprelease] = getADSR([$.hpattack, $.hpdecay, $.hpsustain, $.hprelease]); - } - if ($.bpenv) { - $._bpenv = new ADSR({ decayCurve: 4 }); - [$.bpattack, $.bpdecay, $.bpsustain, $.bprelease] = getADSR([$.bpattack, $.bpdecay, $.bpsustain, $.bprelease]); - } - - // channelwise effects setup - $._chorus = $.chorus ? [] : null; - $._lpf = $.cutoff ? [] : null; - $._hpf = $.hcutoff ? [] : null; - $._bpf = $.bandf ? [] : null; - $._coarse = $.coarse ? [] : null; - $._crush = $.crush ? [] : null; - $._distort = $.distort ? [] : null; - for (let i = 0; i < this._channels; i++) { - $._lpf?.push(new TwoPoleFilter()); - $._hpf?.push(new TwoPoleFilter()); - $._bpf?.push(new TwoPoleFilter()); - $._chorus?.push(new Chorus()); - $._coarse?.push(new Coarse()); - $._crush?.push(new Crush()); - $._distort?.push(new Distort()); - } - } - update(t) { - if (!this._sound && !this._buffers) { - return 0; - } - let gate = Number(t >= this._begin && t <= this._holdEnd); - - let freq = this.freq * this.speed; - - // frequency modulation - if (this._fm && this.fmh !== undefined && this.fmi !== undefined) { - let fmi = this.fmi; - if (this._fmenv) { - const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease); - fmi = this.fmenv * env * fmi; - } - const modfreq = freq * this.fmh; - const modgain = modfreq * fmi; - freq = freq + this._fm.update(modfreq) * modgain; - } - - // vibrato - if (this._vib && this.vibmod !== undefined) { - freq = freq * 2 ** ((this._vib.update(this.vib) * this.vibmod) / 12); - } - - // pitch envelope - if (this._penv && this.penv !== undefined) { - const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease); - freq = freq + env * this.penv; - } - - // filters - let lpf = this.cutoff; - if (lpf !== undefined && this._lpenv) { - const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease); - lpf = this.lpenv * env * lpf + lpf; - } - let hpf = this.hcutoff; - if (hpf !== undefined && this._hpenv && this.hpenv !== undefined) { - const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease); - hpf = 2 ** this.hpenv * env * hpf + hpf; - } - let bpf = this.bandf; - if (bpf !== undefined && this._bpenv && this.bpenv !== undefined) { - const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease); - bpf = 2 ** this.bpenv * env * bpf + bpf; - } - // gain envelope - const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release); - - // channelwise dsp - for (let i = 0; i < this._channels; i++) { - // sound source - if (this._sound && this.s === 'pulse') { - this.out[i] = this._sound.update(freq, this.pw); - } else if (this._sound) { - this.out[i] = this._sound.update(freq); - } else if (this._buffers) { - this.out[i] = this._buffers[i].update(freq); - } - this.out[i] = this.out[i] * this.gain * this.velocity; - if (this._chorus) { - const c = this._chorus[i].update(this.out[i], this.chorus, 0.03 + 0.05 * i, 1, 0.11); - this.out[i] = c + this.out[i]; - } - - if (this._lpf) { - this._lpf[i].update(this.out[i], lpf, this.resonance); - this.out[i] = this._lpf[i].s1; - } - if (this._hpf) { - this._hpf[i].update(this.out[i], hpf, this.hresonance); - this.out[i] = this.out[i] - this._hpf[i].s1; - } - if (this._bpf) { - this._bpf[i].update(this.out[i], bpf, this.bandq); - this.out[i] = this._bpf[i].s0; - } - if (this._coarse) { - this.out[i] = this._coarse[i].update(this.out[i], this.coarse); - } - if (this._crush) { - this.out[i] = this._crush[i].update(this.out[i], this.crush); - } - if (this._distort) { - this.out[i] = this._distort[i].update(this.out[i], this.distort, this.distortvol); - } - this.out[i] = this.out[i] * env; - this.out[i] = this.out[i] * this.postgain; - if (!this._buffers) { - this.out[i] = this.out[i] * 0.2; // turn down waveform - } - } - if (this._channels === 1) { - this.out[1] = this.out[0]; - } - if (this.pan !== 0.5) { - const panpos = (this.pan * Math.PI) / 2; - this.out[0] = this.out[0] * Math.cos(panpos); - this.out[1] = this.out[1] * Math.sin(panpos); - } - } -} - -// this class is the interface to the "outer world" -// it handles spawning and despawning of DoughVoice's -export class Dough { - voices = []; // DoughVoice[] - vid = 0; - q = []; - out = [0, 0]; - delaysend = [0, 0]; - delaytime = getDefaultValue('delaytime'); - delayfeedback = getDefaultValue('delayfeedback'); - delayspeed = getDefaultValue('delayspeed'); - t = 0; - // sampleRate: number, currentTime: number (seconds) - constructor(sampleRate = 48000, currentTime = 0) { - this.sampleRate = sampleRate; - this.t = Math.floor(currentTime * sampleRate); // samples - // console.log('init dough', this.sampleRate, this.t); - this._delayL = new Delay(); - this._delayR = new Delay(); - } - loadSample(name, channels, sampleRate) { - BufferPlayer.samples.set(name, { channels, sampleRate }); - } - scheduleSpawn(value) { - if (value._begin === undefined) { - throw new Error('[dough]: scheduleSpawn expected _begin to be set'); - } - if (value._duration === undefined) { - throw new Error('[dough]: scheduleSpawn expected _duration to be set'); - } - value.sampleRate = this.sampleRate; - // convert seconds to samples - const time = Math.floor(value._begin * this.sampleRate); // set from supradough.mjs - this.schedule({ time, type: 'spawn', arg: value }); - } - spawn(value) { - value.id = this.vid++; - const voice = new DoughVoice(value); - this.voices.push(voice); - // console.log('spawn', voice.id, 'voices:', this.voices.length); - // schedule removal - const endTime = Math.ceil(voice._end * this.sampleRate); - this.schedule({ time: endTime /* + 48000 */, type: 'despawn', arg: voice.id }); - } - despawn(vid) { - this.voices = this.voices.filter((v) => v.id !== vid); - // console.log('despawn', vid, 'voices:', this.voices.length); - } - // schedules a function call with a single argument - // msg = {time:number,type:string, arg: any} - // the Dough method "type" will be called with "arg" at "time" - schedule(msg) { - if (!this.q.length) { - // if empty, just push - this.q.push(msg); - return; - } - // not empty - // find index where msg.time fits in - let i = 0; - while (i < this.q.length && this.q[i].time < msg.time) { - i++; - } - // this ensures q stays sorted by time, so we only need to check q[0] - this.q.splice(i, 0, msg); - } - // maybe update should be called once per block instead for perf reasons? - update() { - // go over q - while (this.q.length > 0 && this.q[0].time <= this.t) { - // console.log('schedule', this.q[0]); - // trigger due messages. q is sorted, so we only need to check q[0] - this[this.q[0].type](this.q[0].arg); // type is expected to be a Dough method - this.q.shift(); - } - // add active voices - this.out[0] = 0; - this.out[1] = 0; - for (let v = 0; v < this.voices.length; v++) { - this.voices[v].update(this.t / this.sampleRate); - this.out[0] += this.voices[v].out[0]; - this.out[1] += this.voices[v].out[1]; - if (this.voices[v].delay) { - this.delaysend[0] += this.voices[v].out[0] * this.voices[v].delay; - this.delaysend[1] += this.voices[v].out[1] * this.voices[v].delay; - this.delaytime = this.voices[v].delaytime; // we trust that these are initialized in the voice - this.delayspeed = this.voices[v].delayspeed; // we trust that these are initialized in the voice - this.delayfeedback = this.voices[v].delayfeedback; - } - } - // todo: how to change delaytime / delayfeedback from a voice? - const delayL = this._delayL.update(this.delaysend[0], this.delaytime); - const delayR = this._delayR.update(this.delaysend[1], this.delaytime); - - this.delaysend[0] = delayL * this.delayfeedback; - this.delaysend[1] = delayR * this.delayfeedback; - this.out[0] += delayL; - this.out[1] += delayR; - this.t++; - } -} diff --git a/packages/supradough/index.mjs b/packages/supradough/index.mjs deleted file mode 100644 index cb132fb3f..000000000 --- a/packages/supradough/index.mjs +++ /dev/null @@ -1,5 +0,0 @@ -// import _workletUrl from './dough-worklet.mjs?url'; // only for dev (breaks for production build) -import _workletUrl from './dough-worklet.mjs?audioworklet'; // only for prod (breaks in development?!) - -export * from './dough.mjs'; -export const workletUrl = _workletUrl; diff --git a/packages/supradough/package.json b/packages/supradough/package.json deleted file mode 100644 index ee3869d08..000000000 --- a/packages/supradough/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "supradough", - "version": "1.2.4", - "description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.", - "main": "index.mjs", - "type": "module", - "publishConfig": { - "main": "dist/index.mjs" - }, - "scripts": { - "build": "vite build", - "prepublishOnly": "npm run build" - }, - "repository": { - "type": "git", - "url": "git+https://codeberg.org/uzu/strudel.git" - }, - "keywords": [ - "tidalcycles", - "strudel", - "pattern", - "livecoding", - "algorave" - ], - "author": "Felix Roos ", - "license": "AGPL-3.0-or-later", - "bugs": { - "url": "https://codeberg.org/uzu/strudel/issues" - }, - "homepage": "https://codeberg.org/uzu/strudel#readme", - "devDependencies": { - "vite": "^6.0.11", - "vite-plugin-bundle-audioworklet": "workspace:*", - "wav-encoder": "^1.3.0" - }, - "engines": { - "node": ">=18.0.0" - } -} diff --git a/packages/supradough/vite.config.js b/packages/supradough/vite.config.js deleted file mode 100644 index deb488e3e..000000000 --- a/packages/supradough/vite.config.js +++ /dev/null @@ -1,16 +0,0 @@ -import { defineConfig } from 'vite'; -import { resolve } from 'path'; -import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [bundleAudioWorkletPlugin()], - build: { - lib: { - entry: resolve(__dirname, 'index.mjs'), - formats: ['es'], - fileName: (ext) => ({ es: 'index.mjs' })[ext], - }, - target: 'esnext', - }, -}); diff --git a/packages/webaudio/index.mjs b/packages/webaudio/index.mjs index 4933b7a01..362e61c44 100644 --- a/packages/webaudio/index.mjs +++ b/packages/webaudio/index.mjs @@ -7,5 +7,4 @@ This program is free software: you can redistribute it and/or modify it under th export * from './webaudio.mjs'; export * from './scope.mjs'; export * from './spectrum.mjs'; -export * from './supradough.mjs'; export * from 'superdough'; diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index 48b40470a..95249aafa 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -35,8 +35,7 @@ "dependencies": { "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", - "superdough": "workspace:*", - "supradough": "workspace:*" + "superdough": "workspace:*" }, "devDependencies": { "vite": "^6.0.11" diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs deleted file mode 100644 index 925a9bd5e..000000000 --- a/packages/webaudio/supradough.mjs +++ /dev/null @@ -1,130 +0,0 @@ -import { Pattern } from '@strudel/core'; -import { connectToDestination, getAudioContext, getWorklet } from 'superdough'; - -let doughWorklet; - -function initDoughWorklet() { - const ac = getAudioContext(); - doughWorklet = getWorklet( - ac, - 'dough-processor', - {}, - { - outputChannelCount: [2], - }, - ); - connectToDestination(doughWorklet); // channels? -} - -const soundMap = new Map(); -const loadedSounds = new Map(); - -Pattern.prototype.supradough = function () { - return this.onSchedule((hap, begin, cps) => { - hap.value._begin = begin; - hap.value._duration = hap.duration / cps; - !doughWorklet && initDoughWorklet(); - const s = (hap.value.bank ? hap.value.bank + '_' : '') + hap.value.s; - const n = hap.value.n ?? 0; - const soundKey = `${s}:${n}`; - if (soundMap.has(s)) { - hap.value.s = soundKey; // dough.mjs is unaware of bank and n (only maps keys to buffers) - } - if (soundMap.has(s) && !loadedSounds.has(soundKey)) { - const urls = soundMap.get(s); - const url = urls[n % urls.length]; - console.log(`load ${soundKey} from ${url}`); - const loadSample = fetchSample(url); - loadedSounds.set(soundKey, loadSample); - loadSample.then(({ channels, sampleRate }) => - doughWorklet.port.postMessage({ - sample: soundKey, - channels, - sampleRate, - }), - ); - } - - doughWorklet.port.postMessage({ spawn: hap.value }); - }, 1); -}; - -function githubPath(base, subpath = '') { - if (!base.startsWith('github:')) { - throw new Error('expected "github:" at the start of pseudoUrl'); - } - let [_, path] = base.split('github:'); - path = path.endsWith('/') ? path.slice(0, -1) : path; - if (path.split('/').length === 2) { - // assume main as default branch if none set - path += '/main'; - } - return `https://raw.githubusercontent.com/${path}/${subpath}`; -} -export async function _fetchSampleMap(url) { - if (url.startsWith('github:')) { - url = githubPath(url, 'strudel.json'); - } - if (url.startsWith('local:')) { - url = `http://localhost:5432`; - } - if (url.startsWith('shabda:')) { - let [_, path] = url.split('shabda:'); - url = `https://shabda.ndre.gr/${path}.json?strudel=1`; - } - if (url.startsWith('shabda/speech')) { - let [_, path] = url.split('shabda/speech'); - path = path.startsWith('/') ? path.substring(1) : path; - let [params, words] = path.split(':'); - let gender = 'f'; - let language = 'en-GB'; - if (params) { - [language, gender] = params.split('/'); - } - url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`; - } - if (typeof fetch !== 'function') { - // not a browser - return; - } - const base = url.split('/').slice(0, -1).join('/'); - if (typeof fetch === 'undefined') { - // skip fetch when in node / testing - return; - } - const json = await fetch(url) - .then((res) => res.json()) - .catch((error) => { - console.error(error); - throw new Error(`error loading "${url}"`); - }); - return [json, json._base || base]; -} - -// for some reason, only piano and flute work.. is it because mp3?? - -async function fetchSample(url) { - const buffer = await fetch(url) - .then((res) => res.arrayBuffer()) - .then((buf) => getAudioContext().decodeAudioData(buf)); - let channels = []; - for (let i = 0; i < buffer.numberOfChannels; i++) { - channels.push(buffer.getChannelData(i)); - } - return { channels, sampleRate: buffer.sampleRate }; -} - -export async function supradoughsamples(sampleMap, baseUrl) { - if (typeof sampleMap === 'string') { - const [json, base] = await _fetchSampleMap(sampleMap); - // console.log('json', json, 'base', base); - return supradoughsamples(json, base); - } - Object.entries(sampleMap).map(async ([key, urls]) => { - if (key !== '_base') { - urls = urls.map((url) => baseUrl + url); - // console.log('set', key, urls); - soundMap.set(key, urls); - } - }); -} diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 213ac54bd..0062fed0a 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -17,10 +17,7 @@ import { resetGlobalEffects, errorLogger, } from 'superdough'; -import './supradough.mjs'; -import { workletUrl } from 'supradough'; import { SuperdoughAudioController } from 'superdough/superdoughoutput.mjs'; -registerWorklet(workletUrl); const { Pattern, logger, repl } = strudel; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7728d3ebf..bf0931007 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -606,18 +606,6 @@ importers: specifier: workspace:* version: link:../vite-plugin-bundle-audioworklet - packages/supradough: - devDependencies: - vite: - specifier: ^6.0.11 - version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) - vite-plugin-bundle-audioworklet: - specifier: workspace:* - version: link:../vite-plugin-bundle-audioworklet - wav-encoder: - specifier: ^1.3.0 - version: 1.3.0 - packages/tidal: dependencies: '@strudel/core': @@ -729,9 +717,6 @@ importers: superdough: specifier: workspace:* version: link:../superdough - supradough: - specifier: workspace:* - version: link:../supradough devDependencies: vite: specifier: ^6.0.11 @@ -7782,9 +7767,6 @@ packages: walk-up-path@3.0.1: resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==} - wav-encoder@1.3.0: - resolution: {integrity: sha512-FXJdEu2qDOI+wbVYZpu21CS1vPEg5NaxNskBr4SaULpOJMrLE6xkH8dECa7PiS+ZoeyvP7GllWUAxPN3AvFSEw==} - wav@1.0.2: resolution: {integrity: sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==} @@ -16365,8 +16347,6 @@ snapshots: walk-up-path@3.0.1: {} - wav-encoder@1.3.0: {} - wav@1.0.2: dependencies: buffer-alloc: 1.2.0 diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index 05f25eefd..a809c61b1 100644 --- a/website/src/repl/components/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -5,9 +5,8 @@ import { Textbox } from '@src/repl/components/panel/SettingsTab'; import { settingsMap, useSettings } from '@src/settings.mjs'; const isValid = ({ name, description, tags = [] }) => { - const isSupradoughOnly = tags.includes('supradough') && !tags.includes('superdough'); const isSuperdirtOnly = tags.includes('superdirt') && !tags.includes('superdough'); - return name && !name.startsWith('_') && !!description && !isSupradoughOnly && !isSuperdirtOnly; + return name && !name.startsWith('_') && !!description && !isSuperdirtOnly; }; const availableFunctions = (() => { @@ -38,7 +37,7 @@ const availableFunctions = (() => { })(); const tagCounts = {}; -const ignoredTags = ['supradough', 'superdirt']; +const ignoredTags = ['superdirt']; // const tagOptions = { all: `all (${availableFunctions.length})` }; const tagOptions = { all: `all` }; for (const doc of availableFunctions) { From 4f3ba01afcdde29dd50a2f71f5161c6f31061084 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 15 Oct 2026 15:01:10 +0200 Subject: [PATCH 24/24] fix snapshots --- packages/core/controls.mjs | 1 - test/__snapshots__/examples.test.mjs.snap | 101 +++++++--------------- 2 files changed, 32 insertions(+), 70 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index cbd499e1b..9749bb7b2 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1905,7 +1905,6 @@ export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', * .s("sawtooth") * .delay(.8) * .delaytime(1/2) - * .delayspeed("<2 .5 -1 -2>") */ export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt'); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 4bd44ce7b..d726f6251 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3138,43 +3138,6 @@ exports[`runs examples > example "delayfeedback" example index 0 1`] = ` ] `; -exports[`runs examples > example "delayspeed" example index 0 1`] = ` -[ - "[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 1/8 → 1/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 1/4 → 3/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 3/8 → 1/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 1/2 → 5/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 5/8 → 3/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 3/4 → 7/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 7/8 → 1/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 1/1 → 9/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 9/8 → 5/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 5/4 → 11/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 11/8 → 3/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 3/2 → 13/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 13/8 → 7/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 7/4 → 15/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 15/8 → 2/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 2/1 → 17/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 17/8 → 9/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 9/4 → 19/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 19/8 → 5/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 5/2 → 21/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 21/8 → 11/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 11/4 → 23/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 23/8 → 3/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 3/1 → 25/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 25/8 → 13/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 13/4 → 27/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 27/8 → 7/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 7/2 → 29/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 29/8 → 15/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 15/4 → 31/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 31/8 → 4/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", -] -`; - exports[`runs examples > example "delaysync" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:bd delay:0.25 delaysync:0.125 ]", @@ -3190,38 +3153,38 @@ exports[`runs examples > example "delaysync" example index 0 1`] = ` exports[`runs examples > example "delaytime" example index 0 1`] = ` [ - "[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 1/8 → 1/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 1/4 → 3/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 3/8 → 1/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 1/2 → 5/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 5/8 → 3/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 3/4 → 7/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 7/8 → 1/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 1/1 → 9/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 9/8 → 5/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 5/4 → 11/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 11/8 → 3/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 3/2 → 13/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 13/8 → 7/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 7/4 → 15/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 15/8 → 2/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 2/1 → 17/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 17/8 → 9/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 9/4 → 19/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 19/8 → 5/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 5/2 → 21/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 21/8 → 11/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 11/4 → 23/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 23/8 → 3/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 3/1 → 25/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 25/8 → 13/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 13/4 → 27/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 27/8 → 7/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 7/2 → 29/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 29/8 → 15/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 15/4 → 31/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 31/8 → 4/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", + "[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 1/8 → 1/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 1/4 → 3/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 3/8 → 1/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 1/2 → 5/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 5/8 → 3/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 3/4 → 7/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 7/8 → 1/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 1/1 → 9/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 9/8 → 5/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 5/4 → 11/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 11/8 → 3/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 3/2 → 13/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 13/8 → 7/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 7/4 → 15/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 15/8 → 2/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 2/1 → 17/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 17/8 → 9/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 9/4 → 19/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 19/8 → 5/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 5/2 → 21/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 21/8 → 11/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 11/4 → 23/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 23/8 → 3/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 3/1 → 25/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 25/8 → 13/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 13/4 → 27/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 27/8 → 7/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 7/2 → 29/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 29/8 → 15/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 15/4 → 31/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 ]", + "[ 31/8 → 4/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 ]", ] `;