diff --git a/.astro/settings.json b/.astro/settings.json new file mode 100644 index 00000000..bf82959b --- /dev/null +++ b/.astro/settings.json @@ -0,0 +1,5 @@ +{ + "devToolbar": { + "enabled": false + } +} \ No newline at end of file diff --git a/.forgejo/workflows/deploy-hot.yml b/.forgejo/workflows/deploy-hot.yml new file mode 100644 index 00000000..6c8205ae --- /dev/null +++ b/.forgejo/workflows/deploy-hot.yml @@ -0,0 +1,44 @@ +name: Build and Deploy hot PRs + +on: + pull_request_target: + types: [labeled] + +# Allow one concurrent deployment +concurrency: + group: "warm-pages" + cancel-in-progress: false + +jobs: + build: + if: ${{ github.event.label.name == 'serve-hot' }} + runs-on: docker + steps: + - uses: actions/checkout@v4 + 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 + + - name: Install Dependencies + run: pnpm install + + - name: Build + run: pnpm build + + - name: Deploy + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + run: | + eval $(ssh-agent -s) + echo "$SSH_PRIVATE_KEY" | ssh-add - + apt update && apt install -y rsync + mkdir -p ~/.ssh + ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts + rsync -atv --delete --delete-after --progress \ + ./website/dist/ \ + strudel@matrix.toplap.org:/home/strudel/deploy/pr-${{ github.event.pull_request.number }}.hot.strudel.cc \ No newline at end of file diff --git a/examples/codemirror-repl/main.js b/examples/codemirror-repl/main.js index 5b5714fb..5b5f2028 100644 --- a/examples/codemirror-repl/main.js +++ b/examples/codemirror-repl/main.js @@ -29,6 +29,7 @@ const editor = new StrudelMirror({ import('@strudel/core'), import('@strudel/draw'), import('@strudel/mini'), + import('@strudel/edo'), import('@strudel/tonal'), import('@strudel/webaudio'), ); diff --git a/examples/codemirror-repl/package.json b/examples/codemirror-repl/package.json index 0a65eb71..22fbde0c 100644 --- a/examples/codemirror-repl/package.json +++ b/examples/codemirror-repl/package.json @@ -15,6 +15,7 @@ "@strudel/codemirror": "workspace:*", "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", + "@strudel/edo": "workspace:*", "@strudel/mini": "workspace:*", "@strudel/soundfonts": "workspace:*", "@strudel/tonal": "workspace:*", diff --git a/index.mjs b/index.mjs index 08339bde..6576be47 100644 --- a/index.mjs +++ b/index.mjs @@ -4,6 +4,7 @@ export * from './packages/core/index.mjs'; export * from './packages/csound/index.mjs'; export * from './packages/desktopbridge/index.mjs'; export * from './packages/draw/index.mjs'; +export * from './packages/edo/index.mjs'; export * from './packages/embed/index.mjs'; export * from './packages/hydra/index.mjs'; export * from './packages/midi/index.mjs'; diff --git a/jsdoc/jsdoc-synonyms.js b/jsdoc/jsdoc-synonyms.js index d59c8dac..c5860626 100644 --- a/jsdoc/jsdoc-synonyms.js +++ b/jsdoc/jsdoc-synonyms.js @@ -12,6 +12,13 @@ function defineTags(dictionary) { doclet.synonyms = doclet.synonyms_text.split(/[ ,]+/); }, }); + + dictionary.defineTag('tags', { + mustHaveValue: true, + onTagged: function (doclet, tag) { + doclet.tags = tag.value.split(/[ ,]+/); + }, + }); } module.exports = { defineTags: defineTags }; diff --git a/lerna-debug.log b/lerna-debug.log new file mode 100644 index 00000000..3c15de50 --- /dev/null +++ b/lerna-debug.log @@ -0,0 +1,211 @@ +0 silly argv { +0 silly argv _: [ 'version' ], +0 silly argv private: false, +0 silly argv lernaVersion: '8.1.9', +0 silly argv '$0': 'node_modules/lerna/dist/cli.js' +0 silly argv } +1 notice cli v8.1.9 +2 verbose packageConfigs Explicit "packages" configuration found in lerna.json. Resolving packages using the configured glob(s): ["packages/*"] +3 verbose rootPath /Users/jaderose/Documents/Github/cstrudel/strudel +4 info versioning independent +5 silly isAnythingCommitted +6 verbose isAnythingCommitted 1 +7 silly getCurrentBranch +8 verbose currentBranch main +9 silly remoteBranchExists +10 silly isBehindUpstream +11 silly isBehindUpstream main is behind origin/main by 0 commit(s) and ahead by 4 +12 silly hasTags +13 verbose hasTags true +14 silly git-describe.sync "@strudel/codemirror@1.2.7-4-ga3ac9646" => {"lastTagName":"@strudel/codemirror@1.2.7","lastVersion":"1.2.7","refCount":"4","sha":"a3ac9646","isDirty":false} +15 info Looking for changed packages since @strudel/codemirror@1.2.7 +16 silly checking diff packages/codemirror +17 silly no diff found in @strudel/codemirror +18 silly checking diff packages/core +19 silly no diff found in @strudel/core +20 silly checking diff packages/csound +21 silly no diff found in @strudel/csound +22 silly checking diff packages/desktopbridge +23 silly no diff found in @strudel/desktopbridge +24 silly checking diff packages/draw +25 silly no diff found in @strudel/draw +26 silly checking diff packages/embed +27 silly no diff found in @strudel/embed +28 silly checking diff packages/gamepad +29 silly no diff found in @strudel/gamepad +30 silly checking diff packages/hs2js +31 silly no diff found in hs2js +32 silly checking diff packages/hydra +33 silly no diff found in @strudel/hydra +34 silly checking diff packages/midi +35 silly no diff found in @strudel/midi +36 silly checking diff packages/mini +37 silly no diff found in @strudel/mini +38 silly checking diff packages/mondo +39 silly no diff found in mondolang +40 silly checking diff packages/mondough +41 silly no diff found in @strudel/mondo +42 silly checking diff packages/motion +43 silly no diff found in @strudel/motion +44 silly checking diff packages/mqtt +45 silly no diff found in @strudel/mqtt +46 silly checking diff packages/osc +47 silly found diff in packages/osc/server.js +48 verbose filtered diff [ 'packages/osc/server.js' ] +49 silly checking diff packages/reference +50 silly no diff found in @strudel/reference +51 silly checking diff packages/repl +52 silly no diff found in @strudel/repl +53 silly checking diff packages/sampler +54 silly no diff found in @strudel/sampler +55 silly checking diff packages/serial +56 silly no diff found in @strudel/serial +57 silly checking diff packages/soundfonts +58 silly no diff found in @strudel/soundfonts +59 silly checking diff packages/superdough +60 silly found diff in packages/superdough/package.json +61 verbose filtered diff [ 'packages/superdough/package.json' ] +62 silly checking diff packages/supradough +63 silly found diff in packages/supradough/package.json +64 verbose filtered diff [ 'packages/supradough/package.json' ] +65 silly checking diff packages/tidal +66 silly no diff found in @strudel/tidal +67 silly checking diff packages/tonal +68 silly no diff found in @strudel/tonal +69 silly checking diff packages/transpiler +70 silly no diff found in @strudel/transpiler +71 silly checking diff packages/vite-plugin-bundle-audioworklet +72 silly no diff found in vite-plugin-bundle-audioworklet +73 silly checking diff packages/web +74 silly no diff found in @strudel/web +75 silly checking diff packages/webaudio +76 silly found diff in packages/webaudio/package.json +77 verbose filtered diff [ 'packages/webaudio/package.json' ] +78 silly checking diff packages/xen +79 silly no diff found in @strudel/xen +80 verbose updated @strudel/codemirror +81 verbose updated @strudel/csound +82 verbose updated @strudel/midi +83 verbose updated @strudel/osc +84 verbose updated @strudel/repl +85 verbose updated @strudel/soundfonts +86 verbose updated superdough +87 verbose updated supradough +88 verbose updated @strudel/web +89 verbose updated @strudel/webaudio +90 verbose git-describe undefined => "@strudel/codemirror@1.2.7-4-ga3ac9646" +91 silly git-describe parsed => {"lastTagName":"@strudel/codemirror@1.2.7","lastVersion":"1.2.7","refCount":"4","sha":"a3ac9646","isDirty":false} +92 info execute Skipping releases +93 silly lifecycle No script for "preversion" in "@strudel/monorepo", continuing +94 silly lifecycle No script for "preversion" in "@strudel/osc", continuing +95 silly lifecycle No script for "preversion" in "superdough", continuing +96 silly lifecycle No script for "preversion" in "supradough", continuing +97 verbose version supradough has no lockfile. Skipping lockfile update. +98 verbose version @strudel/osc has no lockfile. Skipping lockfile update. +99 verbose version superdough has no lockfile. Skipping lockfile update. +100 silly lifecycle No script for "version" in "supradough", continuing +101 silly lifecycle No script for "version" in "superdough", continuing +102 silly lifecycle No script for "preversion" in "@strudel/codemirror", continuing +103 silly lifecycle No script for "preversion" in "@strudel/webaudio", continuing +104 silly lifecycle No script for "version" in "@strudel/osc", continuing +105 verbose version @strudel/webaudio has no lockfile. Skipping lockfile update. +106 verbose version @strudel/codemirror has no lockfile. Skipping lockfile update. +107 silly lifecycle No script for "version" in "@strudel/codemirror", continuing +108 silly lifecycle No script for "version" in "@strudel/webaudio", continuing +109 silly lifecycle No script for "preversion" in "@strudel/csound", continuing +110 silly lifecycle No script for "preversion" in "@strudel/midi", continuing +111 silly lifecycle No script for "preversion" in "@strudel/soundfonts", continuing +112 silly lifecycle No script for "preversion" in "@strudel/web", continuing +113 verbose version @strudel/csound has no lockfile. Skipping lockfile update. +114 verbose version @strudel/midi has no lockfile. Skipping lockfile update. +115 verbose version @strudel/soundfonts has no lockfile. Skipping lockfile update. +116 verbose version @strudel/web has no lockfile. Skipping lockfile update. +117 silly lifecycle No script for "version" in "@strudel/midi", continuing +118 silly lifecycle No script for "version" in "@strudel/csound", continuing +119 silly lifecycle No script for "version" in "@strudel/soundfonts", continuing +120 silly lifecycle No script for "preversion" in "@strudel/repl", continuing +121 verbose version @strudel/repl has no lockfile. Skipping lockfile update. +122 silly lifecycle No script for "version" in "@strudel/web", continuing +123 silly lifecycle No script for "version" in "@strudel/repl", continuing +124 silly lifecycle No script for "version" in "@strudel/monorepo", continuing +125 verbose version Updating root pnpm-lock.yaml +126 silly version Skipped applying prettier to ignored file: packages/supradough/package.json +127 silly version Skipped applying prettier to ignored file: packages/superdough/package.json +128 silly version Skipped applying prettier to ignored file: packages/osc/package.json +129 silly version Skipped applying prettier to ignored file: packages/codemirror/package.json +130 silly version Skipped applying prettier to ignored file: packages/webaudio/package.json +131 silly version Skipped applying prettier to ignored file: packages/midi/package.json +132 silly version Skipped applying prettier to ignored file: packages/csound/package.json +133 silly version Skipped applying prettier to ignored file: packages/soundfonts/package.json +134 silly version Skipped applying prettier to ignored file: packages/web/package.json +135 silly version Skipped applying prettier to ignored file: packages/repl/package.json +136 silly version Skipped applying prettier to ignored file: pnpm-lock.yaml +137 silly gitAdd [ +137 silly gitAdd 'packages/supradough/package.json', +137 silly gitAdd 'packages/superdough/package.json', +137 silly gitAdd 'packages/osc/package.json', +137 silly gitAdd 'packages/codemirror/package.json', +137 silly gitAdd 'packages/webaudio/package.json', +137 silly gitAdd 'packages/midi/package.json', +137 silly gitAdd 'packages/csound/package.json', +137 silly gitAdd 'packages/soundfonts/package.json', +137 silly gitAdd 'packages/web/package.json', +137 silly gitAdd 'packages/repl/package.json', +137 silly gitAdd 'pnpm-lock.yaml' +137 silly gitAdd ] +138 silly gitCommit Publish +138 silly gitCommit +138 silly gitCommit - @strudel/codemirror@1.2.8 +138 silly gitCommit - @strudel/csound@1.2.8 +138 silly gitCommit - @strudel/midi@1.2.8 +138 silly gitCommit - @strudel/osc@1.3.2 +138 silly gitCommit - @strudel/repl@1.2.9 +138 silly gitCommit - @strudel/soundfonts@1.2.8 +138 silly gitCommit - superdough@1.2.5 +138 silly gitCommit - supradough@1.2.4 +138 silly gitCommit - @strudel/web@1.2.8 +138 silly gitCommit - @strudel/webaudio@1.2.9 +139 verbose git [ +139 verbose git 'commit', +139 verbose git '-F', +139 verbose git '/private/var/folders/hc/yf_zr55547sbcpz5rj7q85sc0000gn/T/3c85b5d4-56ae-40e9-8df7-db2bc1048402/lerna-commit.txt' +139 verbose git ] +140 silly gitTag @strudel/codemirror@1.2.8 git tag %s -m %s +141 verbose git [ +141 verbose git 'tag', +141 verbose git '@strudel/codemirror@1.2.8', +141 verbose git '-m', +141 verbose git '@strudel/codemirror@1.2.8' +141 verbose git ] +142 silly gitTag @strudel/csound@1.2.8 git tag %s -m %s +143 verbose git [ 'tag', '@strudel/csound@1.2.8', '-m', '@strudel/csound@1.2.8' ] +144 silly gitTag @strudel/midi@1.2.8 git tag %s -m %s +145 verbose git [ 'tag', '@strudel/midi@1.2.8', '-m', '@strudel/midi@1.2.8' ] +146 silly gitTag @strudel/osc@1.3.2 git tag %s -m %s +147 verbose git [ 'tag', '@strudel/osc@1.3.2', '-m', '@strudel/osc@1.3.2' ] +148 silly gitTag @strudel/repl@1.2.9 git tag %s -m %s +149 verbose git [ 'tag', '@strudel/repl@1.2.9', '-m', '@strudel/repl@1.2.9' ] +150 silly gitTag @strudel/soundfonts@1.2.8 git tag %s -m %s +151 verbose git [ +151 verbose git 'tag', +151 verbose git '@strudel/soundfonts@1.2.8', +151 verbose git '-m', +151 verbose git '@strudel/soundfonts@1.2.8' +151 verbose git ] +152 silly gitTag superdough@1.2.5 git tag %s -m %s +153 verbose git [ 'tag', 'superdough@1.2.5', '-m', 'superdough@1.2.5' ] +154 silly gitTag supradough@1.2.4 git tag %s -m %s +155 verbose git [ 'tag', 'supradough@1.2.4', '-m', 'supradough@1.2.4' ] +156 silly gitTag @strudel/web@1.2.8 git tag %s -m %s +157 verbose git [ 'tag', '@strudel/web@1.2.8', '-m', '@strudel/web@1.2.8' ] +158 silly gitTag @strudel/webaudio@1.2.9 git tag %s -m %s +159 verbose git [ 'tag', '@strudel/webaudio@1.2.9', '-m', '@strudel/webaudio@1.2.9' ] +160 error Error: Command failed with exit code 128: git tag superdough@1.2.5 -m superdough@1.2.5 +160 error fatal: tag 'superdough@1.2.5' already exists +160 error at makeError (/Users/jaderose/Documents/Github/cstrudel/strudel/node_modules/.pnpm/execa@5.0.0/node_modules/execa/lib/error.js:59:11) +160 error at handlePromise (/Users/jaderose/Documents/Github/cstrudel/strudel/node_modules/.pnpm/execa@5.0.0/node_modules/execa/index.js:114:26) +160 error at process.processTicksAndRejections (node:internal/process/task_queues:105:5) +160 error at async Promise.all (index 6) +160 error at async VersionCommand.gitCommitAndTagVersionForUpdates (/Users/jaderose/Documents/Github/cstrudel/strudel/node_modules/.pnpm/lerna@8.1.9_encoding@0.1.13/node_modules/lerna/dist/index.js:9957:11) +160 error at async VersionCommand.commitAndTagUpdates (/Users/jaderose/Documents/Github/cstrudel/strudel/node_modules/.pnpm/lerna@8.1.9_encoding@0.1.13/node_modules/lerna/dist/index.js:9933:18) +160 error at async Promise.all (index 0) diff --git a/package.json b/package.json index b74c8575..26a20aec 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "homepage": "https://strudel.cc", "dependencies": { "@strudel/core": "workspace:*", + "@strudel/edo": "workspace:*", "@strudel/mini": "workspace:*", "@strudel/tonal": "workspace:*", "@strudel/transpiler": "workspace:*", diff --git a/packages/codemirror/block_utilities.mjs b/packages/codemirror/block_utilities.mjs new file mode 100644 index 00000000..fd3dbedc --- /dev/null +++ b/packages/codemirror/block_utilities.mjs @@ -0,0 +1,51 @@ +// Block-based evaluation utilities + +export function getBlockRegions(code) { + const chars = code.split(''); + let i = 0, + blanks = [], + blockStart = 0, + regions = []; + while (i < chars.length) { + const isBlank = chars[i] === '\n'; + if (isBlank) { + blanks.push(i); + } else if (chars[i].trim() !== '') { + if (blanks.length > 1) { + regions.push([blockStart, blanks[0]]); + blockStart = i; + } + blanks = []; + } + i++; + } + regions.push([blockStart, blanks.length ? blanks[0] : i]); + return regions; +} + +export function getBlockAt(code, cursor) { + const regions = getBlockRegions(code); + for (const [start, end] of regions) { + if (cursor >= start && cursor <= end) { + return [start, end]; + } + } + return null; +} + +export const evalBlock = (strudelMirror) => { + const { state } = strudelMirror.editor; + const code = state.doc.toString(); + const cursor = state.selection.main.head; + const range = getBlockAt(code, cursor); + if (range) { + const [a, b] = range; + const block = code.slice(a, b); + if (block) { + // Flash the block being evaluated + strudelMirror.flash(200, { from: a, to: b }); + strudelMirror.repl.evaluateBlock(block, true, { range }); + } + } + return true; +}; diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index ec7eee85..6aac0c8a 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, Drawer } from '@strudel/draw'; - +import { cleanupDraw, cleanupDrawContext, 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 { sliderPlugin, updateSliderWidgets } from './slider.mjs'; +import { jumpToCharacter } from './labelJump.mjs'; +import { getSliderWidgets, sliderPlugin, updateSliderWidgets } from './slider.mjs'; import { activateTheme, initTheme, theme } from './themes.mjs'; import { isTooltipEnabled } from './tooltip.mjs'; -import { updateWidgets, widgetPlugin } from './widget.mjs'; -import { jumpToCharacter } from './labelJump.mjs'; +import { getActiveWidgets, updateWidgets, widgetPlugin } from './widget.mjs'; export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands'; @@ -64,6 +64,7 @@ export const defaultSettings = { isLineWrappingEnabled: false, isTabIndentationEnabled: false, isMultiCursorEnabled: false, + isBlockBasedEvalEnabled: false, theme: 'strudelTheme', fontFamily: 'monospace', fontSize: 18, @@ -75,7 +76,7 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS }); // https://codemirror.net/docs/guide/ -export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) { +export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo, strudelMirror }) { const settings = codemirrorSettings.get(); const initialSettings = Object.keys(compartments).map((key) => compartments[key].of(extensions[key](parseBooleans(settings[key]))), @@ -105,11 +106,26 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo keymap.of([ { key: 'Ctrl-Enter', - run: () => onEvaluate?.(), + run: () => { + // issue with referencing settings, this works more reliably + if (strudelMirror?.isBlockBasedEvalEnabled) { + evalBlock(strudelMirror); + return true; + } else { + return onEvaluate?.(); + } + }, }, { key: 'Alt-Enter', - run: () => onEvaluate?.(), + run: () => { + if (strudelMirror?.isBlockBasedEvalEnabled) { + evalBlock(strudelMirror); + return true; + } else { + return onEvaluate?.(); + } + }, }, { key: 'Ctrl-.', @@ -171,6 +187,7 @@ export class StrudelMirror { this.onDraw = onDraw || this.draw; this.id = id || s4(); this.solo = solo; + this.isBlockBasedEvalEnabled = false; // Will be updated via updateSettings() this.drawer = new Drawer((haps, time, _, painters) => { const currentFrame = haps.filter((hap) => hap.isActive(time)); @@ -201,20 +218,28 @@ export class StrudelMirror { cleanupDraw(true, id); } }, - beforeEval: async () => { - cleanupDraw(true, id); + beforeEval: async ({ blockBased } = {}) => { + // Only clean up all drawings for full evaluation + // Block-based eval should preserve animations (like .scope()) from other blocks + if (!blockBased) { + cleanupDraw(true, id); + } await this.prebaked; await replOptions?.beforeEval?.(); }, afterEval: (options) => { // remember for when highlighting is toggled on - this.miniLocations = options.meta?.miniLocations; - this.widgets = options.meta?.widgets; + this.miniLocations = options.meta?.miniLocations || []; + this.widgets = options.meta?.widgets || []; + const sliders = this.widgets.filter((w) => w.type === 'slider'); - updateSliderWidgets(this.editor, sliders); const widgets = this.widgets.filter((w) => w.type !== 'slider'); - updateWidgets(this.editor, widgets); - updateMiniLocations(this.editor, this.miniLocations); + // range-aware update for block-based evaluation + const range = options.range && options.range.length >= 2 ? options.range : null; + + updateSliderWidgets(this.editor, sliders, range); + updateWidgets(this.editor, widgets, range); + updateMiniLocations(this.editor, this.miniLocations, range); replOptions?.afterEval?.(options); // if no painters are set (.onPaint was not called), then we only need // the present moment (for highlighting) @@ -222,8 +247,14 @@ export class StrudelMirror { this.drawer.setDrawTime(drawTime); // invalidate drawer after we've set the appropriate drawTime this.drawer.invalidate(this.repl.scheduler); + + // Clean up draw context if a non-inline widget was removed + if (options.widgetRemoved) { + cleanupDrawContext(id); + } }, }); + this.cleanupDrawContext = () => cleanupDrawContext(id); this.editor = initEditor({ root, initialCode, @@ -236,7 +267,9 @@ export class StrudelMirror { onEvaluate: () => this.evaluate(), onStop: () => this.stop(), mondo: replOptions.mondo, + strudelMirror: this, }); + const cmEditor = this.root.querySelector('.cm-editor'); if (cmEditor) { this.root.style.display = 'block'; @@ -260,6 +293,9 @@ export class StrudelMirror { // 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(); @@ -274,6 +310,9 @@ export class StrudelMirror { // 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); @@ -306,13 +345,17 @@ export class StrudelMirror { this.flash(); await this.repl.evaluate(this.code, autostart); } + async stop() { - this.repl.scheduler.stop(); + this.repl.stop(); } // Listen for global stop requests (e.g., from Vim :q) onStopRequest = (e) => { try { + if (e.detail.view !== this.editor) { + return; // ignore events from other editors + } this.stop(); e?.cancelable && e.preventDefault?.(); } catch (err) { @@ -326,8 +369,8 @@ export class StrudelMirror { this.evaluate(); } } - flash(ms) { - flash(this.editor, ms); + flash(ms, range) { + flash(this.editor, ms, range); } highlight(haps, time) { highlightMiniLocations(this.editor, time, haps); @@ -359,6 +402,10 @@ export class StrudelMirror { setLineWrappingEnabled(enabled) { this.reconfigureExtension('isLineWrappingEnabled', enabled); } + + setBlockBasedEvalEnabled(enabled) { + this.reconfigureExtension('isBlockBasedEvalEnabled', enabled); + } setBracketMatchingEnabled(enabled) { this.reconfigureExtension('isBracketMatchingEnabled', enabled); } @@ -380,6 +427,10 @@ export class StrudelMirror { for (let key in extensions) { this.reconfigureExtension(key, settings[key]); } + // Update block-based eval setting on the instance + if (settings.isBlockBasedEvalEnabled !== undefined) { + this.isBlockBasedEvalEnabled = parseBooleans(settings.isBlockBasedEvalEnabled); + } const updated = { ...codemirrorSettings.get(), ...settings }; codemirrorSettings.set(updated); } @@ -401,6 +452,16 @@ export class StrudelMirror { }; this.editor.dispatch({ changes }); } + // used for debugging but could serve other purposes + getActiveWidgets() { + return getActiveWidgets(this.editor); + } + getSliderWidgets() { + return getSliderWidgets(this.editor); + } + getMiniLocations() { + return this.miniLocations; + } clear() { this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl); this.onEvaluateRequest && document.removeEventListener('repl-evaluate', this.onEvaluateRequest); @@ -434,6 +495,7 @@ function s4() { /** * Overrides the css of highlighted events. Make sure to use single quotes! * @name markcss + * @tag visualization * @example * note("c a f e") * .markcss('text-decoration:underline') diff --git a/packages/codemirror/flash.mjs b/packages/codemirror/flash.mjs index 243500bf..7b247350 100644 --- a/packages/codemirror/flash.mjs +++ b/packages/codemirror/flash.mjs @@ -14,7 +14,8 @@ export const flashField = StateField.define({ const mark = Decoration.mark({ attributes: { style: `background-color: rgba(255,255,255, .4); filter: invert(10%)` }, }); - flash = Decoration.set([mark.range(0, tr.newDoc.length)]); + const range = e.value.range || { from: 0, to: tr.newDoc.length }; + flash = Decoration.set([mark.range(range.from, range.to)]); } else { flash = Decoration.set([]); } @@ -29,8 +30,9 @@ export const flashField = StateField.define({ provide: (f) => EditorView.decorations.from(f), }); -export const flash = (view, ms = 200) => { - view.dispatch({ effects: setFlash.of(true) }); +export const flash = (view, ms = 200, range) => { + const flashData = range ? { range } : true; + view.dispatch({ effects: setFlash.of(flashData) }); setTimeout(() => { view.dispatch({ effects: setFlash.of(false) }); }, ms); diff --git a/packages/codemirror/highlight.mjs b/packages/codemirror/highlight.mjs index f9f977c6..b37390ef 100644 --- a/packages/codemirror/highlight.mjs +++ b/packages/codemirror/highlight.mjs @@ -3,8 +3,9 @@ import { Decoration, EditorView } from '@codemirror/view'; export const setMiniLocations = StateEffect.define(); export const showMiniLocations = StateEffect.define(); -export const updateMiniLocations = (view, locations) => { - view.dispatch({ effects: setMiniLocations.of(locations) }); +export const displayMiniLocations = StateEffect.define(); +export const updateMiniLocations = (view, locations, range = null) => { + view.dispatch({ effects: setMiniLocations.of({ locations, range }) }); }; export const highlightMiniLocations = (view, atTime, haps) => { view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) }); @@ -21,23 +22,54 @@ const miniLocations = StateField.define({ for (let e of tr.effects) { if (e.is(setMiniLocations)) { - // this is called on eval, with the mini locations obtained from the transpiler - // codemirror will automatically remap the marks when the document is edited - // create a mark for each mini location, adding the range to the spec to find it later - const marks = e.value - .filter(([from]) => from < tr.newDoc.length) - .map(([from, to]) => [from, Math.min(to, tr.newDoc.length)]) - .map( - (range) => - Decoration.mark({ - id: range.join(':'), - // this green is only to verify that the decoration moves when the document is edited - // it will be removed later, so the mark is not visible by default - attributes: { style: `background-color: #00CA2880` }, - }).range(...range), // -> Decoration - ); + //block-based eval case + if (e.value.range) { + const stateMiniLocations = getMiniLocationsFromDecorations(locations); - locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet + const normalized = e.value.locations + .filter(([from]) => from < tr.newDoc.length) + .map(([from, to]) => [from, Math.min(to, tr.newDoc.length)]); + + const newIds = new Set(normalized.map((r) => r.join(':'))); + + const marks = normalized.map((range) => { + const id = range.join(':'); + 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 }) => + Decoration.mark({ + id, + attributes: { style: `background-color: #00CA2880` }, + }).range(from, to), + ); + + locations = Decoration.set(previousMarks.concat(marks), true); // -> DecorationSet === RangeSet + } else { + // this is called on eval, with the mini locations obtained from the transpiler + // codemirror will automatically remap the marks when the document is edited + // create a mark for each mini location, adding the range to the spec to find it later + const marks = e.value.locations + .filter(([from]) => from < tr.newDoc.length) + .map(([from, to]) => [from, Math.min(to, tr.newDoc.length)]) + .map( + (range) => + Decoration.mark({ + id: range.join(':'), + // this green is only to verify that the decoration moves when the document is edited + // it will be removed later, so the mark is not visible by default + attributes: { style: `background-color: #00CA2880` }, + }).range(...range), // -> Decoration + ); + locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet + } } } @@ -75,12 +107,83 @@ const visibleMiniLocations = StateField.define({ }, }); -// // Derive the set of decorations from the miniLocations and visibleLocations -const miniLocationHighlights = EditorView.decorations.compute([miniLocations, visibleMiniLocations], (state) => { - const iterator = state.field(miniLocations).iter(); - const { haps } = state.field(visibleMiniLocations); - const builder = new RangeSetBuilder(); +const displayMiniLocationsState = StateField.define({ + create() { + return true; // default to showing miniLocations + }, + update(display, tr) { + for (let e of tr.effects) { + if (e.is(displayMiniLocations)) { + display = e.value; + } + } + return display; + }, +}); +// // Derive the set of decorations from the miniLocations and visibleLocations +const miniLocationHighlights = EditorView.decorations.compute( + [miniLocations, visibleMiniLocations, displayMiniLocationsState], + (state) => { + // Check if miniLocations display is disabled + const shouldDisplay = state.field(displayMiniLocationsState); + if (!shouldDisplay) { + return Decoration.none; // Return empty decorations if display is disabled + } + + const iterator = state.field(miniLocations).iter(); + const { haps } = state.field(visibleMiniLocations); + const builder = new RangeSetBuilder(); + + while (iterator.value) { + const { + from, + to, + value: { + spec: { id }, + }, + } = iterator; + + if (haps.has(id)) { + const hap = haps.get(id); + const color = hap.value?.color ?? 'var(--foreground)'; + const style = hap.value?.markcss || `outline: solid 2px ${color}`; + // Get explicit channels for color values + /* + const swatch = document.createElement('div'); + swatch.style.color = color; + document.body.appendChild(swatch); + let channels = getComputedStyle(swatch) + .color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/) + .slice(1) + .map((c) => parseFloat(c || 1)); + document.body.removeChild(swatch); + + // Get percentage of event + const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration; + channels[3] *= percent; + */ + + builder.add( + from, + to, + Decoration.mark({ + // attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` }, + attributes: { style }, + }), + ); + } + + iterator.next(); + } + + return builder.finish(); + }, +); + +const getMiniLocationsFromDecorations = (decorations) => { + const iterator = decorations.iter(); + const miniLocationsArray = []; while (iterator.value) { const { from, @@ -89,50 +192,48 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi spec: { id }, }, } = iterator; - - if (haps.has(id)) { - const hap = haps.get(id); - const color = hap.value?.color ?? 'var(--foreground)'; - const style = hap.value?.markcss || `outline: solid 2px ${color}`; - // Get explicit channels for color values - /* - const swatch = document.createElement('div'); - swatch.style.color = color; - document.body.appendChild(swatch); - let channels = getComputedStyle(swatch) - .color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/) - .slice(1) - .map((c) => parseFloat(c || 1)); - document.body.removeChild(swatch); - - // Get percentage of event - const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration; - channels[3] *= percent; - */ - - builder.add( - from, - to, - Decoration.mark({ - // attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` }, - attributes: { style }, - }), - ); - } - + miniLocationsArray.push({ + from, + to, + id, + }); iterator.next(); } + return miniLocationsArray; +}; - return builder.finish(); -}); +export const getMiniLocations = (state) => { + const decorations = state.field(miniLocations); + return getMiniLocationsFromDecorations(decorations); +}; -export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights]; +export const getActiveMiniLocations = (state) => { + const miniLocations = getMiniLocations(state); + const { haps } = state.field(visibleMiniLocations); + + const activeMiniLocations = miniLocations.filter((location) => haps.has(location.id)); + return activeMiniLocations; +}; + +export const highlightExtension = [ + miniLocations, + visibleMiniLocations, + displayMiniLocationsState, + miniLocationHighlights, +]; export const isPatternHighlightingEnabled = (on, config) => { - on && - config && - setTimeout(() => { - updateMiniLocations(config.editor, config.miniLocations); - }, 100); - return on ? Prec.highest(highlightExtension) : []; + // NOTE: + // Modified this function to always return the highlightExtension, and instead just toggle whether or not the miniLocations are displayed. + // This is because block based evaluation only updates regions of miniLocations, and those updates need to be kept track of constantly. + // The setTimeout was also removed because it conflicted with the range-specific updates required by + // block based evaluation. + // Not sure if this is the best approach, but for block based eval I can't think of a better way to do it. + + if (config) { + // Toggle the display state for miniLocations + config.editor.dispatch({ effects: displayMiniLocations.of(on) }); + } + + return Prec.highest(highlightExtension); }; diff --git a/packages/codemirror/index.mjs b/packages/codemirror/index.mjs index 2d5b3de2..31de6acc 100644 --- a/packages/codemirror/index.mjs +++ b/packages/codemirror/index.mjs @@ -5,3 +5,4 @@ export * from './slider.mjs'; export * from './themes.mjs'; export * from './widget.mjs'; export { Vim } from './keybindings.mjs'; +export * from './basicSetup.mjs'; diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs index 24437b7e..16c7ec99 100644 --- a/packages/codemirror/keybindings.mjs +++ b/packages/codemirror/keybindings.mjs @@ -6,6 +6,7 @@ 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( @@ -20,6 +21,70 @@ 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 @@ -31,7 +96,7 @@ try { // internal actions and works with current selections/visual mode. try { Vim.defineAction('strudelToggleComment', (cm) => { - const view = cm?.view || cm; + const view = cm.cm6; try { const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true }); document.dispatchEvent(ev); @@ -47,74 +112,23 @@ try { // :q to pause/stop Vim.defineEx('quit', 'q', (cm) => { - 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); - } + const view = cm.cm6; + logger('[vim] :q — stopping repl'); + replStop(view); }); // :w to evaluate Vim.defineEx('write', 'w', (cm) => { - const view = cm?.view || cm; // CM6 Vim passes either an object with view or the view itself + const view = cm.cm6; try { - view?.focus?.(); + 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); } - // 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); - } + replEval(view); } catch (e) { console.error('Error dispatching :w evaluation event', e); } @@ -124,11 +138,49 @@ 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 a3735491..d2432ef8 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "1.2.6", + "version": "1.3.0", "description": "Codemirror Extensions for Strudel", "main": "index.mjs", "publishConfig": { @@ -32,18 +32,19 @@ }, "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { - "@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", + "@codemirror/autocomplete": "catalog:", + "@codemirror/commands": "catalog:", + "@codemirror/lang-javascript": "catalog:", + "@codemirror/language": "catalog:", + "@codemirror/search": "catalog:", + "@codemirror/state": "catalog:", + "@codemirror/view": "catalog:", "@lezer/highlight": "^1.2.1", "@nanostores/persistent": "^0.10.2", "@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/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index 72f95125..be0bad97 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -2,11 +2,11 @@ import { ref, pure } from '@strudel/core'; import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view'; import { StateEffect } from '@codemirror/state'; +// Global state storage for all widget types export let sliderValues = {}; -const getSliderID = (from) => `slider_${from}`; export class SliderWidget extends WidgetType { - constructor(value, min, max, from, to, step, view) { + constructor(value, min, max, from, to, step, view, id) { super(); this.value = value; this.min = min; @@ -16,10 +16,21 @@ export class SliderWidget extends WidgetType { this.to = to; this.step = step; this.view = view; + this.id = id || `${from}:${to}`; // Range-based ID for stability } - eq() { - return false; + eq(other) { + if (!(other instanceof SliderWidget)) { + return false; + } + return ( + this.id === other.id && + this.from === other.from && + this.to === other.to && + this.value === other.value && + this.min === other.min && + this.max === other.max + ); } toDOM() { @@ -38,6 +49,7 @@ export class SliderWidget extends WidgetType { slider.from = this.from; slider.originalFrom = this.originalFrom; slider.to = this.to; + slider.id = this.id; // Store range-based ID in DOM element slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)'; this.slider = slider; slider.addEventListener('input', (e) => { @@ -49,7 +61,7 @@ export class SliderWidget extends WidgetType { slider.originalValue = insert; slider.value = insert; this.view.dispatch({ changes: change }); - const id = getSliderID(slider.originalFrom); // matches id generated in transpiler + const id = slider.id; // Use range-based ID window.postMessage({ type: 'cm-slider', value: Number(next), id }); }); return wrap; @@ -62,19 +74,60 @@ export class SliderWidget extends WidgetType { export const setSliderWidgets = StateEffect.define(); -export const updateSliderWidgets = (view, widgets) => { - view.dispatch({ effects: setSliderWidgets.of(widgets) }); +export const setSliderWidgetsInRange = StateEffect.define(); + +export const updateSliderWidgets = (view, widgets, range = null) => { + if (range) { + // range argument passed for block-based evaluation + view.dispatch({ effects: setSliderWidgetsInRange.of({ widgets, range }) }); + } else { + view.dispatch({ effects: setSliderWidgets.of(widgets) }); + } }; function getSliders(widgetConfigs, view) { - return widgetConfigs - .filter((w) => w.type === 'slider') - .map(({ from, to, value, min, max, step }) => { - return Decoration.widget({ - widget: new SliderWidget(value, min, max, from, to, step, view), - side: 0, - }).range(from /* , to */); - }); + return ( + widgetConfigs + .filter((w) => w.type === 'slider') + // Deduplicate sliders that might appear multiple times (e.g., during paste operations) + .filter((slider, index, self) => index === self.findIndex((s) => s.from === slider.from && s.to === slider.to)) + .sort((a, b) => a.from - b.from) + .map(({ from, to, value, min, max, step, id }) => { + return Decoration.widget({ + widget: new SliderWidget(value, min, max, from, to, step, view, id), + side: 0, + }).range(from /* , to */); + }) + ); +} + +export function getSliderWidgets(view) { + if (!view || !view.state) { + return []; + } + + const sliderPluginInstance = view.plugin(sliderPlugin); + if (!sliderPluginInstance || !sliderPluginInstance.decorations) { + return []; + } + + const sliderWidgets = []; + + sliderPluginInstance.decorations.between(0, view.state.doc.length, (from, to, decoration) => { + if (decoration.widget instanceof SliderWidget) { + sliderWidgets.push({ + type: 'slider', + from: decoration.widget.from, + to: decoration.widget.to, + value: decoration.widget.value, + min: decoration.widget.min, + max: decoration.widget.max, + step: decoration.widget.step, + }); + } + }); + + return sliderWidgets; } export const sliderPlugin = ViewPlugin.fromClass( @@ -101,7 +154,42 @@ export const sliderPlugin = ViewPlugin.fromClass( } } for (let e of tr.effects) { - if (e.is(setSliderWidgets)) { + if (e.is(setSliderWidgetsInRange)) { + // Block-aware slider update logic + const { widgets, range } = e.value; + const [rangeStart, rangeEnd] = range; + + // Get existing slider widgets that should be preserved + const existingSliders = []; + this.decorations.between(0, update.view.state.doc.length, (from, to, decoration) => { + if (decoration.widget instanceof SliderWidget) { + // Preserve sliders outside the evaluation range + // Use strict > for rangeEnd because when code is deleted, slider positions + // map to the deletion boundary (rangeEnd), and those should be removed, not preserved + if (from < rangeStart || from > rangeEnd) { + existingSliders.push({ + from, + to, + value: decoration.widget.value, + min: decoration.widget.min, + max: decoration.widget.max, + step: decoration.widget.step, + id: decoration.widget.id || `${from}:${to}`, + type: 'slider', + }); + } + } + }); + + // Merge preserved sliders with new widgets + const mergedWidgets = [...existingSliders, ...widgets] + .filter( + (slider, index, self) => index === self.findIndex((s) => s.type === 'slider' && s.id === slider.id), + ) + .sort((a, b) => a.from - b.from); + + this.decorations = Decoration.set(getSliders(mergedWidgets, update.view)); + } else if (e.is(setSliderWidgets)) { this.decorations = Decoration.set(getSliders(e.value, update.view)); } } @@ -117,6 +205,7 @@ export const sliderPlugin = ViewPlugin.fromClass( * Displays a slider widget to allow the user manipulate a value * * @name slider + * @tags external_io, visualization * @param {number} value Initial value * @param {number} min Minimum value - optional, defaults to 0 * @param {number} max Maximum value - optional, defaults to 1 @@ -131,6 +220,7 @@ export let sliderWithID = (id, value, min, max) => { sliderValues[id] = value; // sync state at eval time (code -> state) return ref(() => sliderValues[id]); // use state at query time }; + // update state when sliders are moved if (typeof window !== 'undefined') { window.addEventListener('message', (e) => { @@ -139,7 +229,7 @@ if (typeof window !== 'undefined') { // update state when slider is moved sliderValues[e.data.id] = e.data.value; } else { - console.warn(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`); + console.error(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`); } } }); diff --git a/packages/codemirror/themes.mjs b/packages/codemirror/themes.mjs index 1a523b39..dd561d30 100644 --- a/packages/codemirror/themes.mjs +++ b/packages/codemirror/themes.mjs @@ -198,6 +198,7 @@ export function activateTheme(name) { const themeSettings = settings[name] || settings.strudelTheme; // set css variables themeStyle.innerHTML = `:root { + color-scheme: ${themeSettings.light ? 'light' : 'dark'}; ${Object.entries(themeSettings) // important to override fallback .map(([key, value]) => `--${key}: ${value} !important;`) diff --git a/packages/codemirror/themes/CutiePi.mjs b/packages/codemirror/themes/CutiePi.mjs index 64b38eaa..f535a2da 100644 --- a/packages/codemirror/themes/CutiePi.mjs +++ b/packages/codemirror/themes/CutiePi.mjs @@ -17,13 +17,15 @@ export const settings = { background: 'white', lineBackground: 'transparent', foreground: deepPurple, + muted: deepPurple + '50', caret: '#797977', selection: yellowPink, selectionMatch: '#2B323D', - gutterBackground: grey, + gutterBackground: pinkAccent, gutterForeground: lightGrey, gutterBorder: 'transparent', lineHighlight: pinkAccent, + light: true, }; export default createTheme({ diff --git a/packages/codemirror/themes/algoboy.mjs b/packages/codemirror/themes/algoboy.mjs index 049ed596..5eb90ff7 100644 --- a/packages/codemirror/themes/algoboy.mjs +++ b/packages/codemirror/themes/algoboy.mjs @@ -31,6 +31,7 @@ const palette = palettes['Sour Watermelon B']; export const settings = { background: palette[3], foreground: palette[1], + muted: palette[1] + '50', caret: palette[0], selection: palette[0], selectionMatch: palette[1], diff --git a/packages/codemirror/themes/androidstudio.mjs b/packages/codemirror/themes/androidstudio.mjs index 77c16c33..35b5b746 100644 --- a/packages/codemirror/themes/androidstudio.mjs +++ b/packages/codemirror/themes/androidstudio.mjs @@ -8,6 +8,7 @@ export const settings = { background: '#282b2e', lineBackground: '#282b2e99', foreground: '#a9b7c6', + muted: '#a9b7c650', caret: '#00FF00', selection: '#343739', selectionMatch: '#343739', @@ -19,6 +20,7 @@ export default createTheme({ settings: { background: '#282b2e', foreground: '#a9b7c6', + muted: '#a9b7c650', caret: '#00FF00', selection: '#4e5254', selectionMatch: '#4e5254', diff --git a/packages/codemirror/themes/archBtw.mjs b/packages/codemirror/themes/archBtw.mjs index 87546569..3bc5684e 100644 --- a/packages/codemirror/themes/archBtw.mjs +++ b/packages/codemirror/themes/archBtw.mjs @@ -12,6 +12,7 @@ export const settings = { background: hex[0], lineBackground: 'transparent', foreground: hex[1], + muted: hex[2], selection: hex[2], selectionMatch: hex[0], gutterBackground: hex[0], diff --git a/packages/codemirror/themes/atomone.mjs b/packages/codemirror/themes/atomone.mjs index ab9cd360..bc7a3254 100644 --- a/packages/codemirror/themes/atomone.mjs +++ b/packages/codemirror/themes/atomone.mjs @@ -11,6 +11,7 @@ export const settings = { background: '#272C35', lineBackground: '#272C3599', foreground: 'hsl(220, 14%, 71%)', + muted: 'hsl(220, 14%, 41%)', caret: '#797977', selection: '#ffffff30', selectionMatch: '#2B323D', @@ -25,6 +26,7 @@ export default createTheme({ settings: { background: '#272C35', foreground: '#9d9b97', + muted: 'hsl(220, 14%, 41%)', caret: '#797977', selection: '#3d4c64', selectionMatch: '#3d4c64', diff --git a/packages/codemirror/themes/aura.mjs b/packages/codemirror/themes/aura.mjs index b42a5839..08bb5ea2 100644 --- a/packages/codemirror/themes/aura.mjs +++ b/packages/codemirror/themes/aura.mjs @@ -5,6 +5,7 @@ export const settings = { background: '#21202e', lineBackground: '#21202e99', foreground: '#edecee', + muted: '#edecee50', caret: '#a277ff', selection: '#3d375e7f', selectionMatch: '#3d375e7f', @@ -18,6 +19,7 @@ export default createTheme({ settings: { background: '#21202e', foreground: '#edecee', + muted: '#edecee50', caret: '#a277ff', selection: '#5a51898f', selectionMatch: '#5a51898f', diff --git a/packages/codemirror/themes/bbedit.mjs b/packages/codemirror/themes/bbedit.mjs index 535bf9ea..0555ac1a 100644 --- a/packages/codemirror/themes/bbedit.mjs +++ b/packages/codemirror/themes/bbedit.mjs @@ -6,6 +6,7 @@ export const settings = { background: '#FFFFFF', lineBackground: '#FFFFFF99', foreground: '#000000', + muted: '#00000050', caret: '#FBAC52', selection: '#FFD420', selectionMatch: '#FFD420', @@ -20,6 +21,7 @@ export default createTheme({ settings: { background: '#FFFFFF', foreground: '#000000', + muted: '#00000050', caret: '#FBAC52', selection: '#FFD420', selectionMatch: '#FFD420', diff --git a/packages/codemirror/themes/blackscreen.mjs b/packages/codemirror/themes/blackscreen.mjs index 2c45df11..462dc200 100644 --- a/packages/codemirror/themes/blackscreen.mjs +++ b/packages/codemirror/themes/blackscreen.mjs @@ -2,7 +2,8 @@ import { tags as t } from '@lezer/highlight'; import { createTheme } from './theme-helper.mjs'; export const settings = { background: 'black', - foreground: 'white', // whats that? + foreground: 'white', + muted: '#ffffff50', caret: 'white', selection: '#ffffff20', selectionMatch: '#036dd626', diff --git a/packages/codemirror/themes/bluescreen.mjs b/packages/codemirror/themes/bluescreen.mjs index 97d165c9..a3b9648d 100644 --- a/packages/codemirror/themes/bluescreen.mjs +++ b/packages/codemirror/themes/bluescreen.mjs @@ -3,7 +3,8 @@ import { createTheme } from './theme-helper.mjs'; export const settings = { background: '#051DB5', lineBackground: '#051DB550', - foreground: 'white', // whats that? + foreground: 'white', + muted: '#ffffff50', caret: 'white', selection: 'rgba(128, 203, 196, 0.5)', selectionMatch: '#036dd626', diff --git a/packages/codemirror/themes/bluescreenlight.mjs b/packages/codemirror/themes/bluescreenlight.mjs index 031a3da3..7e10659f 100644 --- a/packages/codemirror/themes/bluescreenlight.mjs +++ b/packages/codemirror/themes/bluescreenlight.mjs @@ -11,6 +11,7 @@ export const settings = { background: hex[0], lineBackground: 'transparent', foreground: hex[2], + muted: hex[3], selection: hex[3], selectionMatch: hex[0], gutterBackground: hex[0], diff --git a/packages/codemirror/themes/darcula.mjs b/packages/codemirror/themes/darcula.mjs index ead66e66..ee510764 100644 --- a/packages/codemirror/themes/darcula.mjs +++ b/packages/codemirror/themes/darcula.mjs @@ -9,6 +9,7 @@ export const settings = { background: '#242424', lineBackground: '#24242499', foreground: '#f8f8f2', + muted: '#f8f8f250', caret: '#FFFFFF', selection: 'rgba(255, 255, 255, 0.1)', selectionMatch: 'rgba(255, 255, 255, 0.2)', @@ -23,6 +24,7 @@ export default createTheme({ settings: { background: '#242424', foreground: '#f8f8f2', + muted: '#f8f8f250', caret: '#FFFFFF', selection: 'rgba(255, 255, 255, 0.1)', selectionMatch: 'rgba(255, 255, 255, 0.2)', diff --git a/packages/codemirror/themes/dracula.mjs b/packages/codemirror/themes/dracula.mjs index bdd9617d..514b6371 100644 --- a/packages/codemirror/themes/dracula.mjs +++ b/packages/codemirror/themes/dracula.mjs @@ -11,6 +11,7 @@ export const settings = { background: '#282a36', lineBackground: '#282a3699', foreground: '#f8f8f2', + muted: '#f8f8f250', caret: '#f8f8f0', selection: 'rgba(255, 255, 255, 0.1)', selectionMatch: 'rgba(255, 255, 255, 0.2)', @@ -27,6 +28,7 @@ export default createTheme({ settings: { background: '#282a36', foreground: '#f8f8f2', + muted: '#f8f8f250', caret: '#f8f8f0', selection: 'rgba(255, 255, 255, 0.1)', selectionMatch: 'rgba(255, 255, 255, 0.2)', diff --git a/packages/codemirror/themes/duotoneDark.mjs b/packages/codemirror/themes/duotoneDark.mjs index f54973e8..72e07cd6 100644 --- a/packages/codemirror/themes/duotoneDark.mjs +++ b/packages/codemirror/themes/duotoneDark.mjs @@ -10,6 +10,7 @@ export const settings = { background: '#2a2734', lineBackground: '#2a273499', foreground: '#eeebff', + muted: '#eeebff50', caret: '#ffad5c', selection: 'rgba(255, 255, 255, 0.1)', gutterBackground: '#2a2734', @@ -22,6 +23,7 @@ export default createTheme({ settings: { background: '#2a2734', foreground: '#6c6783', + muted: '#eeebff50', caret: '#ffad5c', selection: '#9a86fd', selectionMatch: '#9a86fd', diff --git a/packages/codemirror/themes/eclipse.mjs b/packages/codemirror/themes/eclipse.mjs index 8082b59d..8c3671eb 100644 --- a/packages/codemirror/themes/eclipse.mjs +++ b/packages/codemirror/themes/eclipse.mjs @@ -6,6 +6,7 @@ export const settings = { background: '#fff', lineBackground: '#ffffff99', foreground: '#000', + muted: '#00000050', caret: '#FFFFFF', selection: '#d7d4f0', selectionMatch: '#d7d4f0', @@ -20,6 +21,7 @@ export default createTheme({ settings: { background: '#fff', foreground: '#000', + muted: '#00000050', caret: '#FFFFFF', selection: '#d7d4f0', selectionMatch: '#d7d4f0', diff --git a/packages/codemirror/themes/fruitDaw.mjs b/packages/codemirror/themes/fruitDaw.mjs index c9e55779..68b7857e 100644 --- a/packages/codemirror/themes/fruitDaw.mjs +++ b/packages/codemirror/themes/fruitDaw.mjs @@ -23,6 +23,7 @@ export const settings = { background: hex[0], lineBackground: 'transparent', foreground: hex[10], + muted: hex[7], selection: hex[8], selectionMatch: hex[0], gutterBackground: hex[3], diff --git a/packages/codemirror/themes/githubDark.mjs b/packages/codemirror/themes/githubDark.mjs index 477e7d7b..3b11eac3 100644 --- a/packages/codemirror/themes/githubDark.mjs +++ b/packages/codemirror/themes/githubDark.mjs @@ -5,6 +5,7 @@ export const settings = { background: '#0d1117', lineBackground: '#0d111799', foreground: '#c9d1d9', + muted: '#c9d1d950', caret: '#c9d1d9', selection: '#003d73', selectionMatch: '#003d73', @@ -16,6 +17,7 @@ export default createTheme({ settings: { background: '#0d1117', foreground: '#c9d1d9', + muted: '#c9d1d950', caret: '#c9d1d9', selection: '#003d73', selectionMatch: '#003d73', diff --git a/packages/codemirror/themes/githubLight.mjs b/packages/codemirror/themes/githubLight.mjs index 2ccec93b..26b1d572 100644 --- a/packages/codemirror/themes/githubLight.mjs +++ b/packages/codemirror/themes/githubLight.mjs @@ -6,6 +6,7 @@ export const settings = { background: '#fff', lineBackground: '#ffffff99', foreground: '#24292e', + muted: '#24292e50', selection: '#BBDFFF', selectionMatch: '#BBDFFF', gutterBackground: '#fff', @@ -17,6 +18,7 @@ export default createTheme({ settings: { background: '#fff', foreground: '#24292e', + muted: '#24292e50', selection: '#BBDFFF', selectionMatch: '#BBDFFF', gutterBackground: '#fff', diff --git a/packages/codemirror/themes/green-text.mjs b/packages/codemirror/themes/green-text.mjs index 08c7a4bb..7d3a47fb 100644 --- a/packages/codemirror/themes/green-text.mjs +++ b/packages/codemirror/themes/green-text.mjs @@ -13,6 +13,7 @@ export const settings = { background: hex[0], lineBackground: 'transparent', foreground: hex[2], + muted: hex[2] + '50', selection: hex[4], selectionMatch: hex[0], gutterBackground: hex[0], diff --git a/packages/codemirror/themes/gruvboxDark.mjs b/packages/codemirror/themes/gruvboxDark.mjs index 67d8bffc..2cc9274e 100644 --- a/packages/codemirror/themes/gruvboxDark.mjs +++ b/packages/codemirror/themes/gruvboxDark.mjs @@ -10,6 +10,7 @@ export const settings = { background: '#282828', lineBackground: '#28282899', foreground: '#ebdbb2', + muted: '#ebdbb250', caret: '#ebdbb2', selection: '#bdae93', selectionMatch: '#bdae93', @@ -23,6 +24,7 @@ export default createTheme({ settings: { background: '#282828', foreground: '#ebdbb2', + muted: '#ebdbb250', caret: '#ebdbb2', selection: '#b99d555c', selectionMatch: '#b99d555c', diff --git a/packages/codemirror/themes/gruvboxLight.mjs b/packages/codemirror/themes/gruvboxLight.mjs index 5d4b4ec0..8e529b21 100644 --- a/packages/codemirror/themes/gruvboxLight.mjs +++ b/packages/codemirror/themes/gruvboxLight.mjs @@ -11,6 +11,7 @@ export const settings = { background: '#fbf1c7', lineBackground: '#fbf1c799', foreground: '#3c3836', + muted: '#3c383650', caret: '#af3a03', selection: '#ebdbb2', selectionMatch: '#bdae93', @@ -25,6 +26,7 @@ export default createTheme({ settings: { background: '#fbf1c7', foreground: '#3c3836', + muted: '#3c383650', caret: '#af3a03', selection: '#bdae9391', selectionMatch: '#bdae9391', diff --git a/packages/codemirror/themes/materialDark.mjs b/packages/codemirror/themes/materialDark.mjs index f1341288..0c4ef054 100644 --- a/packages/codemirror/themes/materialDark.mjs +++ b/packages/codemirror/themes/materialDark.mjs @@ -5,6 +5,7 @@ export const settings = { background: '#212121', lineBackground: '#21212199', foreground: '#bdbdbd', + muted: '#bdbdbd50', caret: '#a0a4ae', selection: '#d7d4f0', selectionMatch: '#d7d4f0', @@ -19,6 +20,7 @@ export default createTheme({ settings: { background: '#212121', foreground: '#bdbdbd', + muted: '#bdbdbd50', caret: '#a0a4ae', selection: '#d7d4f063', selectionMatch: '#d7d4f063', diff --git a/packages/codemirror/themes/materialLight.mjs b/packages/codemirror/themes/materialLight.mjs index d2598c64..798842bf 100644 --- a/packages/codemirror/themes/materialLight.mjs +++ b/packages/codemirror/themes/materialLight.mjs @@ -5,7 +5,8 @@ export const settings = { light: true, background: '#FAFAFA', lineBackground: '#FAFAFA99', - foreground: '#90A4AE', + foreground: '#6182B8', + muted: '#6182B850', caret: '#272727', selection: '#80CBC440', selectionMatch: '#FAFAFA', @@ -19,6 +20,7 @@ export default createTheme({ settings: { background: '#FAFAFA', foreground: '#90A4AE', + muted: '#6182B850', caret: '#272727', selection: '#80CBC440', selectionMatch: '#80CBC440', diff --git a/packages/codemirror/themes/monokai.mjs b/packages/codemirror/themes/monokai.mjs index 294b946b..926e3821 100644 --- a/packages/codemirror/themes/monokai.mjs +++ b/packages/codemirror/themes/monokai.mjs @@ -5,6 +5,7 @@ export const settings = { background: '#272822', lineBackground: '#27282299', foreground: '#FFFFFF', + muted: '#FFFFFF50', caret: '#FFFFFF', selection: '#49483E', selectionMatch: '#49483E', @@ -18,6 +19,7 @@ export default createTheme({ settings: { background: '#272822', foreground: '#FFFFFF', + muted: '#FFFFFF50', caret: '#FFFFFF', selection: '#49483E', selectionMatch: '#49483E', diff --git a/packages/codemirror/themes/noctisLilac.mjs b/packages/codemirror/themes/noctisLilac.mjs index b11c6b40..39804339 100644 --- a/packages/codemirror/themes/noctisLilac.mjs +++ b/packages/codemirror/themes/noctisLilac.mjs @@ -6,6 +6,7 @@ export const settings = { background: '#f2f1f8', lineBackground: '#f2f1f899', foreground: '#0c006b', + muted: '#0c006b50', caret: '#5c49e9', selection: '#d5d1f2', selectionMatch: '#d5d1f2', @@ -19,6 +20,7 @@ export default createTheme({ settings: { background: '#f2f1f8', foreground: '#0c006b', + muted: '#0c006b50', caret: '#5c49e9', selection: '#d5d1f2', selectionMatch: '#d5d1f2', diff --git a/packages/codemirror/themes/nord.mjs b/packages/codemirror/themes/nord.mjs index ce96bca5..efa4b6bf 100644 --- a/packages/codemirror/themes/nord.mjs +++ b/packages/codemirror/themes/nord.mjs @@ -5,6 +5,7 @@ export const settings = { background: '#2e3440', lineBackground: '#2e344099', foreground: '#FFFFFF', + muted: '#FFFFFF50', caret: '#FFFFFF', selection: '#3b4252', selectionMatch: '#e5e9f0', @@ -20,6 +21,7 @@ export default createTheme({ settings: { background: '#2e3440', foreground: '#FFFFFF', + muted: '#FFFFFF50', caret: '#FFFFFF', selection: '#00000073', selectionMatch: '#00000073', diff --git a/packages/codemirror/themes/red-text.mjs b/packages/codemirror/themes/red-text.mjs index 44dda5dc..5ba9d215 100644 --- a/packages/codemirror/themes/red-text.mjs +++ b/packages/codemirror/themes/red-text.mjs @@ -13,6 +13,7 @@ export const settings = { background: hex[0], lineBackground: 'transparent', foreground: hex[2], + muted: hex[2] + '50', selection: hex[4], selectionMatch: hex[0], gutterBackground: hex[0], diff --git a/packages/codemirror/themes/solarizedDark.mjs b/packages/codemirror/themes/solarizedDark.mjs index c6b645f7..6848b537 100644 --- a/packages/codemirror/themes/solarizedDark.mjs +++ b/packages/codemirror/themes/solarizedDark.mjs @@ -5,6 +5,7 @@ export const settings = { background: '#002b36', lineBackground: '#002b3699', foreground: '#93a1a1', + muted: '#93a1a150', caret: '#839496', selection: '#173541', selectionMatch: '#aafe661a', @@ -45,6 +46,7 @@ export default createTheme({ settings: { background: c.background, foreground: c.foreground, + muted: c.muted, caret: c.cursor, selection: c.selection, selectionMatch: c.selection, diff --git a/packages/codemirror/themes/solarizedLight.mjs b/packages/codemirror/themes/solarizedLight.mjs index b6828ff6..0a0c61a1 100644 --- a/packages/codemirror/themes/solarizedLight.mjs +++ b/packages/codemirror/themes/solarizedLight.mjs @@ -8,6 +8,7 @@ export const settings = { background: '#fdf6e3', lineBackground: '#fdf6e399', foreground: '#657b83', + muted: '#657b8350', caret: '#586e75', selection: '#dfd9c8', selectionMatch: '#dfd9c8', @@ -19,6 +20,7 @@ export const settings = { const c = { background: '#FDF6E3', foreground: '#657B83', + muted: '#657b8350', selection: '#EEE8D5', selectionMatch: '#EEE8D5', cursor: '#657B83', diff --git a/packages/codemirror/themes/sonic-pink.mjs b/packages/codemirror/themes/sonic-pink.mjs index 5a19555a..19c5e5e8 100644 --- a/packages/codemirror/themes/sonic-pink.mjs +++ b/packages/codemirror/themes/sonic-pink.mjs @@ -13,6 +13,7 @@ export const settings = { background: '#000000', lineBackground: 'transparent', foreground: hex[4], + muted: hex[6], selection: hex[6], gutterBackground: hex[0], gutterForeground: hex[5], diff --git a/packages/codemirror/themes/strudel-theme.mjs b/packages/codemirror/themes/strudel-theme.mjs index 7c4a3a91..58c67563 100644 --- a/packages/codemirror/themes/strudel-theme.mjs +++ b/packages/codemirror/themes/strudel-theme.mjs @@ -5,6 +5,7 @@ export const settings = { background: '#222', lineBackground: '#22222299', foreground: '#fff', + muted: '#8a919966', caret: '#ffcc00', selection: 'rgba(128, 203, 196, 0.5)', selectionMatch: '#036dd626', diff --git a/packages/codemirror/themes/sublime.mjs b/packages/codemirror/themes/sublime.mjs index 068b9bac..111f1e50 100644 --- a/packages/codemirror/themes/sublime.mjs +++ b/packages/codemirror/themes/sublime.mjs @@ -5,6 +5,7 @@ export const settings = { background: '#303841', lineBackground: '#30384199', foreground: '#FFFFFF', + muted: '#FFFFFF50', caret: '#FBAC52', selection: '#4C5964', selectionMatch: '#3A546E', @@ -18,6 +19,7 @@ export default createTheme({ settings: { background: '#303841', foreground: '#FFFFFF', + muted: '#FFFFFF50', caret: '#FBAC52', selection: '#4C5964', selectionMatch: '#3A546E', diff --git a/packages/codemirror/themes/teletext.mjs b/packages/codemirror/themes/teletext.mjs index 7857cb13..280eda97 100644 --- a/packages/codemirror/themes/teletext.mjs +++ b/packages/codemirror/themes/teletext.mjs @@ -9,7 +9,8 @@ let colorD = '#f8fc55'; export const settings = { background: '#000000', - foreground: colorA, // whats that? + foreground: colorA, + muted: colorA + '50', caret: colorC, selection: colorD, selectionMatch: colorA, diff --git a/packages/codemirror/themes/tokioNightStorm.mjs b/packages/codemirror/themes/tokioNightStorm.mjs index be973d52..ab940da0 100644 --- a/packages/codemirror/themes/tokioNightStorm.mjs +++ b/packages/codemirror/themes/tokioNightStorm.mjs @@ -4,7 +4,8 @@ import { createTheme } from './theme-helper.mjs'; export const settings = { background: '#24283b', lineBackground: '#24283b99', - foreground: '#7982a9', + foreground: '#f9f2f9', + muted: '#f9f2f950', caret: '#c0caf5', selection: '#6f7bb630', selectionMatch: '#1f2335', @@ -18,7 +19,8 @@ export default createTheme({ theme: 'dark', settings: { background: '#24283b', - foreground: '#7982a9', + foreground: '#f9f2f9', + muted: '#f9f2f950', caret: '#c0caf5', selection: '#6f7bb630', selectionMatch: '#343b5f', diff --git a/packages/codemirror/themes/tokyoNight.mjs b/packages/codemirror/themes/tokyoNight.mjs index c325b4ad..29a65667 100644 --- a/packages/codemirror/themes/tokyoNight.mjs +++ b/packages/codemirror/themes/tokyoNight.mjs @@ -4,7 +4,8 @@ import { createTheme } from './theme-helper.mjs'; export const settings = { background: '#1a1b26', lineBackground: '#1a1b2699', - foreground: '#787c99', + foreground: '#f8fcf9', + muted: '#f8fcf950', caret: '#c0caf5', selection: '#515c7e40', selectionMatch: '#16161e', @@ -18,7 +19,8 @@ export default createTheme({ theme: 'dark', settings: { background: '#1a1b26', - foreground: '#787c99', + foreground: '#f8fcf9', + muted: '#f8fcf950', caret: '#c0caf5', selection: '#515c7e40', selectionMatch: '#16161e', diff --git a/packages/codemirror/themes/tokyoNightDay.mjs b/packages/codemirror/themes/tokyoNightDay.mjs index 1cd58a41..a8f5f75b 100644 --- a/packages/codemirror/themes/tokyoNightDay.mjs +++ b/packages/codemirror/themes/tokyoNightDay.mjs @@ -6,6 +6,7 @@ export const settings = { background: '#e1e2e7', lineBackground: '#e1e2e799', foreground: '#3760bf', + muted: '#3760bf50', caret: '#3760bf', selection: '#99a7df', selectionMatch: '#99a7df', @@ -20,6 +21,7 @@ export default createTheme({ settings: { background: '#e1e2e7', foreground: '#3760bf', + muted: '#3760bf50', caret: '#3760bf', selection: '#99a7df', selectionMatch: '#99a7df', diff --git a/packages/codemirror/themes/vscodeDark.mjs b/packages/codemirror/themes/vscodeDark.mjs index 703790fd..9ccc4d07 100644 --- a/packages/codemirror/themes/vscodeDark.mjs +++ b/packages/codemirror/themes/vscodeDark.mjs @@ -5,6 +5,7 @@ export const settings = { background: '#1e1e1e', lineBackground: '#1e1e1e99', foreground: '#fff', + muted: '#ffffff50', caret: '#c6c6c6', selection: '#6199ff2f', selectionMatch: '#72a1ff59', @@ -19,6 +20,7 @@ export default createTheme({ settings: { background: '#1e1e1e', foreground: '#fff', + muted: '#ffffff50', caret: '#c6c6c6', selection: '#6199ff2f', selectionMatch: '#72a1ff59', diff --git a/packages/codemirror/themes/vscodeLight.mjs b/packages/codemirror/themes/vscodeLight.mjs index 9a734511..71817e8a 100644 --- a/packages/codemirror/themes/vscodeLight.mjs +++ b/packages/codemirror/themes/vscodeLight.mjs @@ -5,6 +5,7 @@ export const settings = { background: '#ffffff', lineBackground: '#ffffff50', foreground: '#383a42', + muted: '#383a4250', caret: '#000', selection: '#add6ff', selectionMatch: '#a8ac94', @@ -20,6 +21,7 @@ export default createTheme({ settings: { background: '#ffffff', foreground: '#383a42', + muted: '#383a4250', caret: '#000', selection: '#add6ff', selectionMatch: '#a8ac94', diff --git a/packages/codemirror/themes/whitescreen.mjs b/packages/codemirror/themes/whitescreen.mjs index a2937c38..fa034fb8 100644 --- a/packages/codemirror/themes/whitescreen.mjs +++ b/packages/codemirror/themes/whitescreen.mjs @@ -2,7 +2,8 @@ import { tags as t } from '@lezer/highlight'; import { createTheme } from './theme-helper.mjs'; export const settings = { background: 'white', - foreground: 'black', // whats that? + foreground: 'black', + muted: '#00000050', caret: 'black', selection: 'rgba(128, 203, 196, 0.5)', selectionMatch: '#ffffff26', diff --git a/packages/codemirror/themes/xcodeLight.mjs b/packages/codemirror/themes/xcodeLight.mjs index 8977c133..b2c85798 100644 --- a/packages/codemirror/themes/xcodeLight.mjs +++ b/packages/codemirror/themes/xcodeLight.mjs @@ -6,6 +6,7 @@ export const settings = { background: '#fff', lineBackground: '#ffffff99', foreground: '#3D3D3D', + muted: '#3D3D3D50', selection: '#BBDFFF', selectionMatch: '#BBDFFF', gutterBackground: '#fff', @@ -18,6 +19,7 @@ export default createTheme({ settings: { background: '#fff', foreground: '#3D3D3D', + muted: '#3D3D3D50', selection: '#BBDFFF', selectionMatch: '#BBDFFF', gutterBackground: '#fff', diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs index 42d3b151..6c40d673 100644 --- a/packages/codemirror/widget.mjs +++ b/packages/codemirror/widget.mjs @@ -1,55 +1,111 @@ import { StateEffect, StateField } from '@codemirror/state'; -import { Decoration, EditorView, WidgetType } from '@codemirror/view'; +import { Decoration, EditorView, WidgetType, ViewPlugin } from '@codemirror/view'; import { getWidgetID, registerWidgetType } from '@strudel/transpiler'; import { Pattern } from '@strudel/core'; -export const addWidget = StateEffect.define({ - map: ({ from, to }, change) => { - return { from: change.mapPos(from), to: change.mapPos(to) }; - }, -}); +export const setWidgets = StateEffect.define(); -export const updateWidgets = (view, widgets) => { - view.dispatch({ effects: addWidget.of(widgets) }); +export const setWidgetsInRange = StateEffect.define(); + +export const updateWidgets = (view, widgets, range = null) => { + if (range) { + // range argument passed for block-based evaluation + view.dispatch({ effects: setWidgetsInRange.of({ widgets, range }) }); + } else { + view.dispatch({ effects: setWidgets.of(widgets) }); + } }; -function getWidgets(widgetConfigs) { - return ( - widgetConfigs - // codemirror throws an error if we don't sort - .sort((a, b) => a.to - b.to) - .map((widgetConfig) => { +function getWidgets(widgetConfigs, view) { + const filtered = widgetConfigs + // Filter to widget configs only (exclude sliders) + .filter((w) => w && w.type && w.type !== 'slider') + // Deduplicate widgets by ID, matching slider behavior for stable widget identity + .filter((widget, index, self) => index === self.findIndex((w) => w.type === widget.type && w.id === widget.id)); + + // Filter out widgets whose range is encompassed by another widget + // const nonEncompassed = filterEncompassedWidgets(filtered); + + return filtered + .sort((a, b) => (a.to || 0) - (b.to || 0)) + .map((widgetConfig) => { + try { return Decoration.widget({ - widget: new BlockWidget(widgetConfig), + widget: new BlockWidget(widgetConfig, view), side: 0, - block: true, - }).range(widgetConfig.to); - }) - ); + }).range(widgetConfig.to || widgetConfig.from || 0); + } catch (error) { + console.error('error creating widget', error); + return null; + } + }) + .filter(Boolean); // Remove any null results from failed creations } -const widgetField = StateField.define( - /* */ { - create() { - return Decoration.none; - }, - update(widgets, tr) { - widgets = widgets.map(tr.changes); - for (let e of tr.effects) { - if (e.is(addWidget)) { - try { - widgets = widgets.update({ - filter: () => false, - add: getWidgets(e.value), - }); - } catch (error) { - console.log('err', error); +export const widgetPlugin = ViewPlugin.fromClass( + class { + decorations; //: DecorationSet + + constructor(view /* : EditorView */) { + this.decorations = Decoration.set([]); + } + + update(update /* : ViewUpdate */) { + update.transactions.forEach((tr) => { + if (tr.docChanged) { + this.decorations = this.decorations.map(tr.changes); + const iterator = this.decorations.iter(); + // Apply changes to iterator.from and iterator.to if docChanged + while (iterator.value) { + // when the widgets are moved, we need to tell the dom node the current position + // this is important because the widget functions have to work with the dom node + if (iterator.value?.widget instanceof BlockWidget) { + iterator.value.widget.from = iterator.from; + iterator.value.widget.to = iterator.to; + } + iterator.next(); } } - } - return widgets; - }, - provide: (f) => EditorView.decorations.from(f), + for (let e of tr.effects) { + if (e.is(setWidgetsInRange)) { + // Block-aware widget update logic + const { widgets, range } = e.value; + const [rangeStart, rangeEnd] = range; + + // Get existing widget widgets that should be preserved + const existingWidgets = []; + this.decorations.between(0, update.view.state.doc.length, (from, to, decoration) => { + if (decoration.widget instanceof BlockWidget) { + // Preserve widgets outside the evaluation range + // Use strict > for rangeEnd because when code is deleted, widget positions + // map to the deletion boundary (rangeEnd), and those should be removed, not preserved + if (from < rangeStart || from > rangeEnd) { + existingWidgets.push({ + from: decoration.widget.from, + to: decoration.widget.to, + type: decoration.widget.type, + index: decoration.widget.index, + id: decoration.widget.id, + }); + } + } + }); + + // Merge preserved widgets with new widgets, deduplicating by ID + const mergedWidgets = [...existingWidgets, ...widgets].filter( + (widget, index, self) => index === self.findIndex((w) => w.type === widget.type && w.id === widget.id), + ); + + this.decorations = Decoration.set(getWidgets(mergedWidgets, update.view)); + } else if (e.is(setWidgets)) { + this.decorations = Decoration.set(getWidgets(e.value, update.view)); + } + } + }); + } + }, + { + decorations: (v) => v.decorations, }, ); @@ -60,24 +116,116 @@ export function setWidget(id, el) { } export class BlockWidget extends WidgetType { - constructor(widgetConfig) { + constructor(widgetConfig, view) { super(); + + // Graceful handling of invalid configs like sliders + if (!widgetConfig || typeof widgetConfig !== 'object') { + widgetConfig = { type: 'unknown', from: 0, to: 0 }; + } + + this.from = widgetConfig.from || 0; + this.originalFrom = widgetConfig.from || 0; + this.to = widgetConfig.to || this.from; + this.originalTo = widgetConfig.to || this.from; + this.type = widgetConfig.type || 'unknown'; + this.index = widgetConfig.index || 0; + this.view = view; + + // Use range-based ID for stability, similar to sliders + this.id = widgetConfig.id || getWidgetID?.(widgetConfig); this.widgetConfig = widgetConfig; } - eq() { - return true; + + eq(other) { + if (!(other instanceof BlockWidget)) { + return false; + } + return ( + this.id === other.id && + this.from === other.from && + this.to === other.to && + this.type === other.type && + this.index === other.index + ); } + toDOM() { - const id = getWidgetID(this.widgetConfig); - const el = widgetElements[id]; - return el; + let wrap = document.createElement('span'); + wrap.setAttribute('aria-hidden', 'true'); + wrap.className = 'cm-widget-container'; + + let el = widgetElements[this.id]; + if (el) { + // Ensure the element has the correct ID + el.id = this.id; + wrap.appendChild(el); + } else { + // Create a placeholder element if the widget element doesn't exist + // This prevents CodeMirror errors when widget is missing + const placeholder = document.createElement('span'); + placeholder.setAttribute('aria-hidden', 'true'); + placeholder.className = 'cm-widget-placeholder'; + placeholder.style.cssText = 'display: none;'; // Hide placeholder + placeholder.id = this.id; + wrap.appendChild(placeholder); + } + + return wrap; } + ignoreEvent(e) { return true; } } -export const widgetPlugin = [widgetField]; +export function getActiveWidgets(view) { + if (!view || !view.state) { + return []; + } + + const widgetPluginInstance = view.plugin(widgetPlugin); + if (!widgetPluginInstance || !widgetPluginInstance.decorations) { + return []; + } + + const widgets = []; + + widgetPluginInstance.decorations.between(0, view.state.doc.length, (from, to, decoration) => { + if (decoration.widget instanceof BlockWidget) { + widgets.push({ + type: decoration.widget.type, + from: decoration.widget.from, + to: decoration.widget.to, + index: decoration.widget.index, + id: decoration.widget.id, + }); + } + }); + + return widgets; +} + +export function getAllWidgetIds(view) { + if (!view || !view.state) { + return []; + } + + const widgetPluginInstance = view.plugin(widgetPlugin); + if (!widgetPluginInstance || !widgetPluginInstance.decorations) { + return []; + } + + const widgetIds = []; + + widgetPluginInstance.decorations.between(0, view.state.doc.length, (from, to, decoration) => { + if (decoration.widget instanceof BlockWidget) { + widgetIds.push(decoration.widget.id); + } + }); + + return widgetIds; +} // widget implementer API to create a new widget type export function registerWidget(type, fn) { diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index cecc955a..8f733c32 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -99,6 +99,7 @@ export function registerMultiControl(names, maxControls, ...aliases) { * separated by ':'. * * @name s + * @tags superdough, samples * @param {string | Pattern} sound The sound / pattern of sounds to pick * @synonyms sound * @example @@ -113,6 +114,7 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound'); * Position in the wavetable of the wavetable oscillator * * @name wt + * @tags wavetable, superdough * @param {number | Pattern} position Position in the wavetable from 0 to 1 * @synonyms wavetablePosition * @example @@ -124,6 +126,7 @@ export const { wt, wavetablePosition } = registerControl('wt', 'wavetablePositio * Amount of envelope applied wavetable oscillator's position envelope * * @name wtenv + * @tags wavetable, envelope, superdough * @param {number | Pattern} amount between 0 and 1 */ export const { wtenv } = registerControl('wtenv'); @@ -131,6 +134,7 @@ export const { wtenv } = registerControl('wtenv'); * Attack time of the wavetable oscillator's position envelope * * @name wtattack + * @tags wavetable, envelope, superdough * @synonyms wtatt * @param {number | Pattern} time attack time in seconds */ @@ -140,6 +144,7 @@ export const { wtattack, wtatt } = registerControl('wtattack', 'wtatt'); * Decay time of the wavetable oscillator's position envelope * * @name wtdecay + * @tags wavetable, envelope, superdough * @synonyms wtdec * @param {number | Pattern} time decay time in seconds */ @@ -149,6 +154,7 @@ export const { wtdecay, wtdec } = registerControl('wtdecay', 'wtdec'); * Sustain time of the wavetable oscillator's position envelope * * @name wtsustain + * @tags wavetable, envelope, superdough * @synonyms wtsus * @param {number | Pattern} gain sustain level (0 to 1) */ @@ -158,6 +164,7 @@ export const { wtsustain, wtsus } = registerControl('wtsustain', 'wtsus'); * Release time of the wavetable oscillator's position envelope * * @name wtrelease + * @tags wavetable, envelope, superdough * @synonyms wtrel * @param {number | Pattern} time release time in seconds */ @@ -167,6 +174,7 @@ export const { wtrelease, wtrel } = registerControl('wtrelease', 'wtrel'); * Rate of the LFO for the wavetable oscillator's position * * @name wtrate + * @tags wavetable, lfo, superdough * @param {number | Pattern} rate rate in hertz */ export const { wtrate } = registerControl('wtrate'); @@ -174,6 +182,7 @@ export const { wtrate } = registerControl('wtrate'); * cycle synced rate of the LFO for the wavetable oscillator's position * * @name wtsync + * @tags wavetable, lfo, superdough * @param {number | Pattern} rate rate in cycles */ export const { wtsync } = registerControl('wtsync'); @@ -182,6 +191,7 @@ export const { wtsync } = registerControl('wtsync'); * Depth of the LFO for the wavetable oscillator's position * * @name wtdepth + * @tags wavetable, lfo, superdough * @param {number | Pattern} depth depth of modulation */ export const { wtdepth } = registerControl('wtdepth'); @@ -190,6 +200,7 @@ export const { wtdepth } = registerControl('wtdepth'); * Shape of the LFO for the wavetable oscillator's position * * @name wtshape + * @tags wavetable, lfo, superdough * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) */ export const { wtshape } = registerControl('wtshape'); @@ -198,6 +209,7 @@ export const { wtshape } = registerControl('wtshape'); * DC offset of the LFO for the wavetable oscillator's position * * @name wtdc + * @tags wavetable, lfo, superdough * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar */ export const { wtdc } = registerControl('wtdc'); @@ -206,6 +218,7 @@ export const { wtdc } = registerControl('wtdc'); * Skew of the LFO for the wavetable oscillator's position * * @name wtskew + * @tags wavetable, lfo, superdough * @param {number | Pattern} skew How much to bend the LFO shape */ export const { wtskew } = registerControl('wtskew'); @@ -214,6 +227,7 @@ export const { wtskew } = registerControl('wtskew'); * Amount of warp (alteration of the waveform) to apply to the wavetable oscillator * * @name warp + * @tags wavetable, superdough * @param {number | Pattern} amount Warp of the wavetable from 0 to 1 * @synonyms wavetableWarp * @example @@ -226,6 +240,7 @@ export const { warp, wavetableWarp } = registerControl('warp', 'wavetableWarp'); * Attack time of the wavetable oscillator's warp envelope * * @name warpattack + * @tags wavetable, envelope, superdough * @synonyms warpatt * @param {number | Pattern} time attack time in seconds */ @@ -235,6 +250,7 @@ export const { warpattack, warpatt } = registerControl('warpattack', 'warpatt'); * Decay time of the wavetable oscillator's warp envelope * * @name warpdecay + * @tags wavetable, envelope, superdough * @synonyms warpdec * @param {number | Pattern} time decay time in seconds */ @@ -244,6 +260,7 @@ export const { warpdecay, warpdec } = registerControl('warpdecay', 'warpdec'); * Sustain time of the wavetable oscillator's warp envelope * * @name warpsustain + * @tags wavetable, envelope, superdough * @synonyms warpsus * @param {number | Pattern} gain sustain level (0 to 1) */ @@ -253,6 +270,7 @@ export const { warpsustain, warpsus } = registerControl('warpsustain', 'warpsus' * Release time of the wavetable oscillator's warp envelope * * @name warprelease + * @tags wavetable, envelope, superdough * @synonyms warprel * @param {number | Pattern} time release time in seconds */ @@ -262,6 +280,7 @@ export const { warprelease, warprel } = registerControl('warprelease', 'warprel' * Rate of the LFO for the wavetable oscillator's warp * * @name warprate + * @tags wavetable, lfo, superdough * @param {number | Pattern} rate rate in hertz */ export const { warprate } = registerControl('warprate'); @@ -270,6 +289,7 @@ export const { warprate } = registerControl('warprate'); * Depth of the LFO for the wavetable oscillator's warp * * @name warpdepth + * @tags wavetable, lfo, superdough * @param {number | Pattern} depth depth of modulation */ export const { warpdepth } = registerControl('warpdepth'); @@ -278,6 +298,7 @@ export const { warpdepth } = registerControl('warpdepth'); * Shape of the LFO for the wavetable oscillator's warp * * @name warpshape + * @tags wavetable, lfo, superdough * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) */ export const { warpshape } = registerControl('warpshape'); @@ -286,6 +307,7 @@ export const { warpshape } = registerControl('warpshape'); * DC offset of the LFO for the wavetable oscillator's warp * * @name warpdc + * @tags wavetable, lfo, superdough * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar */ export const { warpdc } = registerControl('warpdc'); @@ -294,6 +316,7 @@ export const { warpdc } = registerControl('warpdc'); * Skew of the LFO for the wavetable oscillator's warp * * @name warpskew + * @tags wavetable, lfo, superdough * @param {number | Pattern} skew How much to bend the LFO shape */ export const { warpskew } = registerControl('warpskew'); @@ -305,6 +328,7 @@ export const { warpskew } = registerControl('warpskew'); * spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip * * @name warpmode + * @tags wavetable, superdough * @param {number | string | Pattern} mode Warp mode * @synonyms wavetableWarpMode * @example @@ -318,6 +342,7 @@ export const { warpmode, wavetableWarpMode } = registerControl('warpmode', 'wave * Amount of randomness of the initial phase of the wavetable oscillator. * * @name wtphaserand + * @tags wavetable, superdough * @param {number | Pattern} amount Randomness of the initial phase. Between 0 (not random) and 1 (fully random) * @synonyms wavetablePhaseRand * @example @@ -330,6 +355,7 @@ export const { wtphaserand, wavetablePhaseRand } = registerControl('wtphaserand' * Amount of envelope applied wavetable oscillator's position envelope * * @name warpenv + * @tags wavetable, envelope, superdough * @param {number | Pattern} amount between 0 and 1 */ export const { warpenv } = registerControl('warpenv'); @@ -338,6 +364,7 @@ export const { warpenv } = registerControl('warpenv'); * cycle synced rate of the LFO for the wavetable warp position * * @name warpsync + * @tags wavetable, lfo, superdough * @param {number | Pattern} rate rate in cycles */ export const { warpsync } = registerControl('warpsync'); @@ -346,6 +373,7 @@ export const { warpsync } = registerControl('warpsync'); * Define a custom webaudio node to use as a sound source. * * @name source + * @tags external_io, superdough * @synonyms src * @param {function} getSource * @synonyms src @@ -353,17 +381,31 @@ export const { warpsync } = registerControl('warpsync'); */ export const { source, src } = registerControl('source', 'src'); /** - * Selects the given index from the sample map. - * Numbers too high will wrap around. - * `n` can also be used to play midi numbers, but it is recommended to use `note` instead. + * Selects the given index: + * - for samples, it picks the sample by index, with wrap around + * - for scales, it picks the scale degree + * - for voicings, it picks the voice index * * @name n + * @tags superdough, samples, tonal * @param {number | Pattern} value sample index starting from 0 * @example * s("bd sd [~ bd] sd,hh*6").n("<0 1>") */ // also see https://codeberg.org/uzu/strudel/pulls/63 export const { n } = registerControl('n'); + +/** + * Selects the given degree. Currently used in `xen` and `tune`: + * + * @name i + * @tags tonal + * @param {number | Pattern} value + * @example + * i("0 1 2 3 4 5 6 7").xen("<5edo 10edo 15edo hexany15>") + */ +export const { i } = registerControl('i'); + /** * Plays the given note name or midi number. A note name consists of * @@ -376,6 +418,7 @@ export const { n } = registerControl('n'); * You can also use midi numbers instead of note names, where 69 is mapped to A4 440Hz in 12EDO. * * @name note + * @tags tonal * @example * note("c a f e") * @example @@ -391,6 +434,7 @@ export const { note } = registerControl(['note', 'n']); * A pattern of numbers that speed up (or slow down) samples while they play. Currently only supported by osc / superdirt. * * @name accelerate + * @tags samples, superdirt * @param {number | Pattern} amount acceleration. * @superdirtOnly * @example @@ -402,6 +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 * @synonyms vel * @example * s("hh*8") @@ -413,6 +458,7 @@ export const { velocity, vel } = registerControl('velocity', 'vel'); * Controls the gain by an exponential amount. * * @name gain + * @tags amplitude, superdough, supradough * @param {number | Pattern} amount gain. * @example * s("hh*8").gain(".4!2 1 .4!2 1 .4 1").fast(2) @@ -423,6 +469,7 @@ export const { gain } = registerControl('gain'); * Gain applied after all effects have been processed. * * @name postgain + * @tags amplitude, superdough, supradough * @example * s("bd sd [~ bd] sd,hh*8") * .compressor("-20:20:10:.002:.02").postgain(1.5) @@ -433,6 +480,7 @@ export const { postgain } = registerControl('postgain'); * Like `gain`, but linear. * * @name amp + * @tags amplitude, superdirt * @param {number | Pattern} amount gain. * @superdirtOnly * @example @@ -440,17 +488,6 @@ export const { postgain } = registerControl('postgain'); * */ export const { amp } = registerControl('amp'); -/** - * Amplitude envelope attack time: Specifies how long it takes for the sound to reach its peak value, relative to the onset. - * - * @name attack - * @param {number | Pattern} attack time in seconds. - * @synonyms att - * @example - * note("c3 e3 f3 g3").attack("<0 .1 .5>") - * - */ -export const { attack, att } = registerControl('attack', 'att'); /** * Sets the Frequency Modulation Harmonicity Ratio. @@ -462,6 +499,7 @@ export const { attack, att } = registerControl('attack', 'att'); * any of the 8 individual FMs (e.g. `fmh2`) * * @name fmh + * @tags fm, superdough, supradough * @param {number | Pattern} harmonicity * @example * note("c e g b g e") @@ -482,6 +520,7 @@ export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerM * `fm3` * * @name fmi + * @tags fm, superdough, supradough * @param {number | Pattern} brightness modulation index * @synonyms fm * @example @@ -505,7 +544,9 @@ 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 * @param {number | Pattern} type lin | exp + * @synonyms fme * @example * note("c e g b g e") * .fm(4) @@ -515,9 +556,10 @@ export const { fmi, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2 * ._scope() * */ -export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fmenv8 } = registerMultiControl( +export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fmenv8, fme } = registerMultiControl( 'fmenv', 8, + 'fme', ); /** @@ -527,8 +569,10 @@ 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 * @synonyms fmatt * @param {number | Pattern} time attack time + * @synonyms fmatt * @example * note("c e g b g e") * .fm(4) @@ -564,6 +608,7 @@ export const { * any of the 8 individual FMs (e.g. `fmwave6`) * * @name fmwave + * @tags fm, superdough, supradough * @param {number | Pattern} wave waveform * @example * n("0 1 2 3".fast(4)).scale("d:minor").s("sine").fmwave("").fm(4).fmh(2.01) @@ -583,8 +628,10 @@ 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 * @synonyms fmdec * @param {number | Pattern} time decay time + * @synonyms fmdec * @example * note("c e g b g e") * .fm(4) @@ -621,8 +668,10 @@ export const { * any of the 8 individual FMs (e.g. `fmsus7`) * * @name fmsustain + * @tags fm, envelope, superdough, supradough * @synonyms fmsus * @param {number | Pattern} level sustain level + * @synonyms fmsus * @example * note("c e g b g e") * .fm(4) @@ -659,6 +708,7 @@ export const { * any of the 8 individual FMs (e.g. `fmrel8`) * * @name fmrelease + * @tags fm, envelope, superdough, supradough * @synonyms fmrel * @param {number | Pattern} time release time * @@ -699,6 +749,7 @@ for (let i = 0; i <= 8; i++) { * Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`. * * @name bank + * @tags samples, superdough * @param {string | Pattern} bank the name of the bank * @example * s("bd sd [~ bd] sd").bank('RolandTR909') // = s("RolandTR909_bd RolandTR909_sd") @@ -710,6 +761,7 @@ export const { bank } = registerControl('bank'); * mix control for the chorus effect * * @name chorus + * @tags pitch * @param {string | Pattern} chorus mix amount between 0 and 1 * @example * note("d d a# a").s("sawtooth").chorus(.5) @@ -722,11 +774,25 @@ export const { analyze } = registerControl('analyze'); // fftSize of analyser export const { fft } = registerControl('fft'); +/** + * Amplitude envelope attack time: Specifies how long it takes for the sound to reach its peak value, relative to the onset. + * + * @name attack + * @tags amplitude, envelope, superdough, supradough + * @param {number | Pattern} attack time in seconds. + * @synonyms att + * @example + * note("c3 e3 f3 g3").attack("<0 .1 .5>") + * + */ +export const { attack, att } = registerControl('attack', 'att'); + /** * Amplitude envelope decay time: the time it takes after the attack time to reach the sustain level. * Note that the decay is only audible if the sustain value is lower than 1. * * @name decay + * @tags amplitude, envelope, superdough, supradough * @param {number | Pattern} time decay time in seconds * @synonyms dec * @example @@ -738,6 +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 * @param {number | Pattern} gain sustain level between 0 and 1 * @synonyms sus * @example @@ -749,6 +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 * @param {number | Pattern} time release time in seconds * @synonyms rel * @example @@ -763,6 +831,7 @@ export const { hold } = registerControl('hold'); * can also optionally supply the 'bpq' parameter separated by ':'. * * @name bpf + * @tags filter, superdough, supradough * @param {number | Pattern} frequency center frequency * @synonyms bandf, bp * @example @@ -775,6 +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 * @param {number | Pattern} q q factor * @synonyms bandq * @example @@ -787,8 +857,8 @@ export const { bandq, bpq } = registerControl('bandq', 'bpq'); /** * A pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample. * - * @memberof Pattern * @name begin + * @tags samples * @param {number | Pattern} amount between 0 and 1, where 1 is the length of the sample * @example * samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/dirt-samples') @@ -801,6 +871,7 @@ export const { begin } = registerControl('begin'); * * @memberof Pattern * @name end + * @tags samples * @param {number | Pattern} length 1 = whole sample, .5 = half sample, .25 = quarter sample etc.. * @example * s("bd*2,oh*4").end("<.1 .2 .5 1>").fast(2) @@ -813,6 +884,7 @@ export const { end } = registerControl('end'); * To change the loop region, use loopBegin / loopEnd. * * @name loop + * @tags samples * @param {number | Pattern} on If 1, the sample is looped * @example * s("casio").loop(1) @@ -825,6 +897,7 @@ export const { loop } = registerControl('loop'); * Note: Samples starting with wt_ will automatically loop! (wt = wavetable) * * @name loopBegin + * @tags samples * @param {number | Pattern} time between 0 and 1, where 1 is the length of the sample * @synonyms loopb * @example @@ -838,6 +911,7 @@ export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb'); * Note that the loop point must be inbetween `begin` and `end`, and after `loopBegin`! * * @name loopEnd + * @tags samples * @param {number | Pattern} time between 0 and 1, where 1 is the length of the sample * @synonyms loope * @example @@ -849,6 +923,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope'); * Bit crusher effect. * * @name crush + * @tags superdough, supradough * @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction). * @example * s(",hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>") @@ -860,6 +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 * @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>") @@ -871,6 +947,7 @@ export const { coarse } = registerControl('coarse'); * Modulate the amplitude of a sound with a continuous waveform * * @name tremolo + * @tags amplitude, lfo, superdough * @synonyms trem * @param {number | Pattern} speed modulation speed in HZ * @example @@ -883,6 +960,7 @@ export const { tremolo, trem } = registerControl(['tremolo', 'tremolodepth', 'tr * Modulate the amplitude of a sound with a continuous waveform * * @name tremolosync + * @tags amplitude, lfo, superdough * @synonyms tremsync * @param {number | Pattern} cycles modulation speed in cycles * @example @@ -898,6 +976,7 @@ export const { tremolosync } = registerControl( * Depth of amplitude modulation * * @name tremolodepth + * @tags amplitude, lfo, superdough * @synonyms tremdepth * @param {number | Pattern} depth * @example @@ -909,6 +988,7 @@ export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth'); * Alter the shape of the modulation waveform * * @name tremoloskew + * @tags amplitude, lfo, superdough * @synonyms tremskew * @param {number | Pattern} amount between 0 & 1, the shape of the waveform * @example @@ -921,6 +1001,7 @@ export const { tremoloskew } = registerControl('tremoloskew', 'tremskew'); * Alter the phase of the modulation waveform * * @name tremolophase + * @tags amplitude, lfo, superdough * @synonyms tremphase * @param {number | Pattern} offset the offset in cycles of the modulation * @example @@ -933,6 +1014,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase'); * Shape of amplitude modulation * * @name tremoloshape + * @tags amplitude, lfo, superdough * @synonyms tremshape * @param {number | Pattern} shape tri | square | sine | saw | ramp * @example @@ -944,6 +1026,7 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape'); * Filter overdrive for supported filter types * * @name drive + * @tags filter, superdough * @param {number | Pattern} amount * @example * note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>") @@ -957,6 +1040,7 @@ export const { drive } = registerControl('drive'); * Can be applied to multiple orbits with the ':' mininotation, e.g. `duckorbit("2:3")` * * @name duckorbit + * @tags amplitude, orbit, superdough * @synonyms duck * @param {number | Pattern} orbit target orbit * @example @@ -977,6 +1061,7 @@ export const { duck } = registerControl('duckorbit', 'duck'); * Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`. * * @name duckdepth + * @tags amplitude, orbit, superdough * @param {number | Pattern} depth depth of modulation from 0 to 1 * @example * stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth("<1 .9 .6 0>")) @@ -996,6 +1081,7 @@ export const { duckdepth } = registerControl('duckdepth'); * Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`. * * @name duckonset + * @tags amplitude, envelope, orbit, superdough * @synonyms duckons * * @param {number | Pattern} time The onset time in seconds @@ -1023,7 +1109,8 @@ export const { duckonset } = registerControl('duckonset', 'duckons'); * Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`. * * @name duckattack - * @synonyms duckatt + * @tags amplitude, envelope, orbit, superdough + * @synonyms duckatt, datt * * @param {number | Pattern} time The attack time in seconds * @example @@ -1035,26 +1122,28 @@ export const { duckonset } = registerControl('duckonset', 'duckons'); * ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:5").duckattack("0.4:0.1") * */ -export const { duckattack } = registerControl('duckattack', 'duckatt'); +export const { duckattack } = registerControl('duckattack', 'duckatt', 'datt'); /** * Create byte beats with custom expressions * * @name byteBeatExpression - * @synonyms bbexpr + * @synonyms bbexpr, bb + * @tags superdough * * @param {number | Pattern} byteBeatExpression bitwise expression for creating bytebeat * @example * s("bytebeat").bbexpr('t*(t>>15^t>>66)') * */ -export const { byteBeatExpression, bbexpr } = registerControl('byteBeatExpression', 'bbexpr'); +export const { byteBeatExpression, bbexpr } = registerControl('byteBeatExpression', 'bbexpr', 'bb'); /** * Create byte beats with custom expressions * * @name byteBeatStartTime * @synonyms bbst + * @tags superdough * * @param {number | Pattern} byteBeatStartTime in samples (t) * @example @@ -1067,6 +1156,7 @@ export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime', * Allows you to set the output channels on the interface * * @name channels + * @tags external_io, superdough * @synonyms ch * * @param {number | Pattern} channels pattern the output channels @@ -1080,6 +1170,7 @@ export const { channels, ch } = registerControl('channels', 'ch'); * Controls the pulsewidth of the pulse oscillator * * @name pw + * @tags superdough * @param {number | Pattern} pulsewidth * @example * note("{f a c e}%16").s("pulse").pw(".8:1:.2") @@ -1092,29 +1183,34 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']); * Controls the lfo rate for the pulsewidth of the pulse oscillator * * @name pwrate + * @synonyms pwr + * @tags superdough, lfo * @param {number | Pattern} rate * @example * n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>") * */ -export const { pwrate } = registerControl('pwrate'); +export const { pwrate } = registerControl('pwrate', 'pwr'); /** * Controls the lfo sweep for the pulsewidth of the pulse oscillator * * @name pwsweep + * @synonyms pws + * @tags superdough, lfo * @param {number | Pattern} sweep * @example * n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>") * */ -export const { pwsweep } = registerControl('pwsweep'); +export const { pwsweep } = registerControl('pwsweep', 'pws'); /** * Phaser audio effect that approximates popular guitar pedals. * * @name phaser + * @tags superdough * @synonyms ph * @param {number | Pattern} speed speed of modulation * @example @@ -1132,6 +1228,7 @@ export const { phaserrate, ph, phaser } = registerControl( * The frequency sweep range of the lfo for the phaser effect. Defaults to 2000 * * @name phasersweep + * @tags superdough, lfo * @synonyms phs * @param {number | Pattern} phasersweep most useful values are between 0 and 4000 * @example @@ -1145,6 +1242,7 @@ export const { phasersweep, phs } = registerControl('phasersweep', 'phs'); * The center frequency of the phaser in HZ. Defaults to 1000 * * @name phasercenter + * @tags superdough * @synonyms phc * @param {number | Pattern} centerfrequency in HZ * @example @@ -1159,6 +1257,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc'); * The amount the signal is affected by the phaser effect. Defaults to 0.75 * * @name phaserdepth + * @tags superdough, superdirt * @synonyms phd, phasdp * @param {number | Pattern} depth number between 0 and 1 * @example @@ -1170,9 +1269,10 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc'); export const { phaserdepth, phd, phasdp } = registerControl('phaserdepth', 'phd', 'phasdp'); /** - * Choose the channel the pattern is sent to in superdirt + * Choose the channel the pattern is sent to * * @name channel + * @tags superdough * @param {number | Pattern} channel channel number * */ @@ -1181,6 +1281,7 @@ export const { channel } = registerControl('channel'); * In the style of classic drum-machines, `cut` will stop a playing sample as soon as another samples with in same cutgroup is to be played. An example would be an open hi-hat followed by a closed one, essentially muting the open. * * @name cut + * @tags superdough * @param {number | Pattern} group cut group number * @example * s("[oh hh]*4").cut(1) @@ -1193,6 +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 * @param {number | Pattern} frequency audible between 0 and 20000 * @synonyms cutoff, ctf, lp * @example @@ -1206,6 +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 * @param {number | Pattern} modulation depth of the lowpass filter envelope between 0 and _n_ * @synonyms lpe * @example @@ -1219,6 +1322,7 @@ export const { lpenv, lpe } = registerControl('lpenv', 'lpe'); /** * Sets the highpass filter envelope modulation depth. * @name hpenv + * @tags filter, envelope, superdough, supradough * @param {number | Pattern} modulation depth of the highpass filter envelope between 0 and _n_ * @synonyms hpe * @example @@ -1232,6 +1336,7 @@ export const { hpenv, hpe } = registerControl('hpenv', 'hpe'); /** * Sets the bandpass filter envelope modulation depth. * @name bpenv + * @tags filter, envelope, superdough, supradough * @param {number | Pattern} modulation depth of the bandpass filter envelope between 0 and _n_ * @synonyms bpe * @example @@ -1245,6 +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 * @param {number | Pattern} attack time of the filter envelope * @synonyms lpa * @example @@ -1258,6 +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 * @param {number | Pattern} attack time of the highpass filter envelope * @synonyms hpa * @example @@ -1271,6 +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 * @param {number | Pattern} attack time of the bandpass filter envelope * @synonyms bpa * @example @@ -1284,6 +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 * @param {number | Pattern} decay time of the filter envelope * @synonyms lpd * @example @@ -1297,6 +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 * @param {number | Pattern} decay time of the highpass filter envelope * @synonyms hpd * @example @@ -1311,6 +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 * @param {number | Pattern} decay time of the bandpass filter envelope * @synonyms bpd * @example @@ -1325,6 +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 * @param {number | Pattern} sustain amplitude of the lowpass filter envelope * @synonyms lps * @example @@ -1339,6 +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 * @param {number | Pattern} sustain amplitude of the highpass filter envelope * @synonyms hps * @example @@ -1353,6 +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 * @param {number | Pattern} sustain amplitude of the bandpass filter envelope * @synonyms bps * @example @@ -1367,6 +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 * @param {number | Pattern} release time of the filter envelope * @synonyms lpr * @example @@ -1382,6 +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 * @param {number | Pattern} release time of the highpass filter envelope * @synonyms hpr * @example @@ -1397,6 +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 * @param {number | Pattern} release time of the bandpass filter envelope * @synonyms bpr * @example @@ -1412,6 +1529,7 @@ export const { bprelease, bpr } = registerControl('bprelease', 'bpr'); /** * Sets the filter type. The ladder filter is more aggressive. More types might be added in the future. * @name ftype + * @tags filter, superdough * @param {number | Pattern} type 12db (0), ladder (1), or 24db (2) * @example * note("{f g g c d a a#}%8").s("sawtooth").lpenv(4).lpf(500).ftype("<0 1 2>").lpq(1) @@ -1427,6 +1545,7 @@ export const { ftype } = registerControl('ftype'); /** * controls the center of the filter envelope. 0 is unipolar positive, .5 is bipolar, 1 is unipolar negative * @name fanchor + * @tags filter, envelope, superdough * @param {number | Pattern} center 0 to 1 * @example * note("{f g g c d a a#}%8").s("sawtooth").lpf("{1000}%2") @@ -1439,6 +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 * @param {number | Pattern} frequency audible between 0 and 20000 * @synonyms hp, hcutoff * @example @@ -1454,6 +1574,7 @@ export const { fanchor } = registerControl('fanchor'); * Rate of the LFO for the lowpass filter * * @name lprate + * @tags filter, lfo, superdough * @param {number | Pattern} rate rate in hertz * @example * note("*16").s("sawtooth").lpf(600).lprate("<4 8 2 1>") @@ -1464,6 +1585,7 @@ export const { lprate } = registerControl('lprate'); * Cycle-synced rate of the LFO for the lowpass filter * * @name lpsync + * @tags filter, lfo, superdough * @param {number | Pattern} rate rate in cycles * @example * note("*16").s("sawtooth").lpf(600).lpsync("<4 8 2 1>") @@ -1474,6 +1596,7 @@ export const { lpsync } = registerControl('lpsync'); * Depth of the LFO for the lowpass filter * * @name lpdepth + * @tags filter, lfo, superdough * @param {number | Pattern} depth depth of modulation * @example * note("*16").s("sawtooth").lpf(600).lpdepth("<1 .5 1.8 0>") @@ -1484,6 +1607,7 @@ export const { lpdepth } = registerControl('lpdepth'); * Depth of the LFO for the lowpass filter, in HZ * * @name lpdepthfrequency + * @tags filter, lfo, superdough * @synonyms lpdepthfreq * @param {number | Pattern} depth depth of modulation * @example @@ -1496,6 +1620,7 @@ export const { lpdepthfrequency, lpdepthfreq } = registerControl('lpdepthfrequen * Shape of the LFO for the lowpass filter * * @name lpshape + * @tags filter, lfo, superdough * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) */ export const { lpshape } = registerControl('lpshape'); @@ -1504,6 +1629,7 @@ export const { lpshape } = registerControl('lpshape'); * DC offset of the LFO for the lowpass filter * * @name lpdc + * @tags filter, lfo, superdough * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar */ export const { lpdc } = registerControl('lpdc'); @@ -1512,6 +1638,7 @@ export const { lpdc } = registerControl('lpdc'); * Skew of the LFO for the lowpass filter * * @name lpskew + * @tags filter, lfo, superdough * @param {number | Pattern} skew How much to bend the LFO shape */ export const { lpskew } = registerControl('lpskew'); @@ -1520,6 +1647,7 @@ export const { lpskew } = registerControl('lpskew'); * Rate of the LFO for the bandpass filter * * @name bprate + * @tags filter, lfo, superdough * @param {number | Pattern} rate rate in hertz */ export const { bprate } = registerControl('bprate'); @@ -1528,6 +1656,7 @@ export const { bprate } = registerControl('bprate'); * Cycle-synced rate of the LFO for the bandpass filter * * @name bpsync + * @tags filter, lfo, superdough * @param {number | Pattern} rate rate in cycles */ export const { bpsync } = registerControl('bpsync'); @@ -1536,6 +1665,7 @@ export const { bpsync } = registerControl('bpsync'); * Depth of the LFO for the bandpass filter * * @name bpdepth + * @tags filter, lfo, superdough * @param {number | Pattern} depth depth of modulation */ export const { bpdepth } = registerControl('bpdepth'); @@ -1544,6 +1674,7 @@ export const { bpdepth } = registerControl('bpdepth'); * Depth of the LFO for the bandpass filter, in HZ * * @name bpdepthfrequency + * @tags filter, lfo, superdough * @synonyms bpdepthfreq * @param {number | Pattern} depth depth of modulation * @example @@ -1556,6 +1687,7 @@ export const { bpdepthfrequency, bpdepthfreq } = registerControl('bpdepthfrequen * Shape of the LFO for the bandpass filter * * @name bpshape + * @tags filter, lfo, superdough * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) */ export const { bpshape } = registerControl('bpshape'); @@ -1564,6 +1696,7 @@ export const { bpshape } = registerControl('bpshape'); * DC offset of the LFO for the bandpass filter * * @name bpdc + * @tags filter, lfo, superdough * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar */ export const { bpdc } = registerControl('bpdc'); @@ -1572,6 +1705,7 @@ export const { bpdc } = registerControl('bpdc'); * Skew of the LFO for the bandpass filter * * @name bpskew + * @tags filter, lfo, superdough * @param {number | Pattern} skew How much to bend the LFO shape */ export const { bpskew } = registerControl('bpskew'); @@ -1580,6 +1714,7 @@ export const { bpskew } = registerControl('bpskew'); * Rate of the LFO for the highpass filter * * @name hprate + * @tags filter, lfo, superdough * @param {number | Pattern} rate rate in hertz */ export const { hprate } = registerControl('hprate'); @@ -1588,6 +1723,7 @@ export const { hprate } = registerControl('hprate'); * Cycle-synced rate of the LFO for the highpass filter * * @name hpsync + * @tags filter, lfo, superdough * @param {number | Pattern} rate rate in cycles */ export const { hpsync } = registerControl('hpsync'); @@ -1596,6 +1732,7 @@ export const { hpsync } = registerControl('hpsync'); * Depth of the LFO for the highpass filter * * @name hpdepth + * @tags filter, lfo, superdough * @param {number | Pattern} depth depth of modulation */ export const { hpdepth } = registerControl('hpdepth'); @@ -1604,6 +1741,7 @@ export const { hpdepth } = registerControl('hpdepth'); * Depth of the LFO for the hipass filter, in hz * * @name hpdepthfrequency + * @tags filter, lfo, superdough * @synonyms hpdepthfreq * @param {number | Pattern} depth depth of modulation * @example @@ -1616,6 +1754,7 @@ export const { hpdepthfrequency, hpdepthfreq } = registerControl('hpdepthfrequen * Shape of the LFO for the highpass filter * * @name hpshape + * @tags filter, lfo, superdough * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) */ export const { hpshape } = registerControl('hpshape'); @@ -1624,6 +1763,7 @@ export const { hpshape } = registerControl('hpshape'); * DC offset of the LFO for the highpass filter * * @name hpdc + * @tags filter, lfo, superdough * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar */ export const { hpdc } = registerControl('hpdc'); @@ -1632,6 +1772,7 @@ export const { hpdc } = registerControl('hpdc'); * Skew of the LFO for the highpass filter * * @name hpskew + * @tags filter, lfo, superdough * @param {number | Pattern} skew How much to bend the LFO shape */ export const { hpskew } = registerControl('hpskew'); @@ -1640,6 +1781,7 @@ export const { hpskew } = registerControl('hpskew'); * Applies a vibrato to the frequency of the oscillator. * * @name vib + * @tags pitch, lfo, superdough, supradough * @synonyms vibrato, v * @param {number | Pattern} frequency of the vibrato in hertz * @example @@ -1657,6 +1799,7 @@ export const { vib, vibrato, v } = registerControl(['vib', 'vibmod'], 'vibrato', * Adds pink noise to the mix * * @name noise + * @tags generators, superdough, supradough * @param {number | Pattern} wet wet amount * @example * sound("/2") @@ -1666,6 +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 * @synonyms vmod * @param {number | Pattern} depth of vibrato (in semitones) * @example @@ -1684,6 +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 * @param {number | Pattern} q resonance factor between 0 and 50 * @synonyms hresonance * @example @@ -1695,6 +1840,7 @@ export const { hresonance, hpq } = registerControl('hresonance', 'hpq'); * Controls the **l**ow-**p**ass **q**-value. * * @name lpq + * @tags filter, superdough, supradough * @param {number | Pattern} q resonance factor between 0 and 50 * @synonyms resonance * @example @@ -1707,6 +1853,7 @@ export const { resonance, lpq } = registerControl('resonance', 'lpq'); * DJ filter, below 0.5 is low pass filter, above is high pass filter. * * @name djf + * @tags filter, superdough * @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter * @example * n(irand(16).seg(8)).scale("d:phrygian").s("supersaw").djf("<.5 .3 .2 .75>") @@ -1723,6 +1870,7 @@ export const { djf } = registerControl('djf'); * * * @name delay + * @tags orbit, superdough, supradough * @param {number | Pattern} level between 0 and 1 * @example * s("bd bd").delay("<0 .25 .5 1>") @@ -1736,6 +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 * @param {number | Pattern} feedback between 0 and 1 * @synonyms delayfb, dfb * @example @@ -1744,46 +1893,53 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback'] */ export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb'); -/** - * Sets the level of the signal that is fed back into the delay. - * Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it - * - * @name delayfeedback - * @param {number | Pattern} feedback between 0 and 1 - * @synonyms delayfb, dfb - * @example - * s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>") - * - */ -export const { delayspeed } = registerControl('delayspeed'); /** * 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 + * @param {number | Pattern} delay in seconds + * @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 { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt'); /** * Sets the time of the delay effect in cycles. * * @name delaysync + * @tags orbit, superdough * @param {number | Pattern} cycles delay length in cycles - * @synonyms delayt, dt + * @synonyms delays, ds * @example * s("bd bd").delay(.25).delaysync("<1 2 3 5>".div(8)) * */ -export const { delaysync } = registerControl('delaysync'); +export const { delaysync } = registerControl('delaysync', 'delays', 'ds'); /** * Specifies whether delaytime is calculated relative to cps. * * @name lock + * @tags superdirt * @param {number | Pattern} enable When set to 1, delaytime is a direct multiple of a cycle. * @superdirtOnly * @example @@ -1794,9 +1950,10 @@ export const { delaysync } = registerControl('delaysync'); export const { lock } = registerControl('lock'); /** - * Set detune for stacked voices of supported oscillators + * Set detune for stacked voices of supported oscillators. * * @name detune + * @tags pitch, superdough * @param {number | Pattern} amount * @synonyms det * @example @@ -1805,9 +1962,10 @@ export const { lock } = registerControl('lock'); */ export const { detune, det } = registerControl('detune', 'det'); /** - * Set number of stacked voices for supported oscillators + * Set number of stacked voices for supported oscillators. * * @name unison + * @tags superdough * @param {number | Pattern} numvoices * @example * note("d f a a# a d3").fast(2).s("supersaw").unison("<1 2 7>") @@ -1819,6 +1977,7 @@ export const { unison } = registerControl('unison'); * Set the stereo pan spread for supported oscillators * * @name spread + * @tags superdough * @param {number | Pattern} spread between 0 and 1 * @example * note("d f a a# a d3").fast(2).s("supersaw").spread("<0 .3 1>") @@ -1829,6 +1988,7 @@ export const { spread } = registerControl('spread'); * Set dryness of reverb. See `room` and `size` for more information about reverb. * * @name dry + * @tags superdirt * @param {number | Pattern} dry 0 = wet, 1 = dry * @example * n("[0,3,7](3,8)").s("superpiano").room(.7).dry("<0 .5 .75 1>").osc() @@ -1836,11 +1996,11 @@ export const { spread } = registerControl('spread'); * */ export const { dry } = registerControl('dry'); -// TODO: does not seem to do anything -/* +/** * Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope. * * @name fadeTime + * @tags superdirt * @synonyms fadeOutTime * @param {number | Pattern} time between 0 and 1 * @example @@ -1848,12 +2008,12 @@ export const { dry } = registerControl('dry'); * */ export const { fadeTime, fadeOutTime } = registerControl('fadeTime', 'fadeOutTime'); -// TODO: see above export const { fadeInTime } = registerControl('fadeInTime'); /** * Set frequency of sound. * * @name freq + * @tags pitch, superdough * @param {number | Pattern} frequency in Hz. the audible range is between 20 and 20000 Hz * @example * freq("220 110 440 110").s("superzow").osc() @@ -1867,6 +2027,7 @@ export const { freq } = registerControl('freq'); * Attack time of pitch envelope. * * @name pattack + * @tags pitch, envelope, superdough, supradough * @synonyms patt * @param {number | Pattern} time time in seconds * @example @@ -1878,6 +2039,7 @@ export const { pattack, patt } = registerControl('pattack', 'patt'); * Decay time of pitch envelope. * * @name pdecay + * @tags pitch, envelope, superdough, supradough * @synonyms pdec * @param {number | Pattern} time time in seconds * @example @@ -1891,6 +2053,7 @@ export const { psustain, psus } = registerControl('psustain', 'psus'); * Release time of pitch envelope * * @name prelease + * @tags pitch, envelope, superdough, supradough * @synonyms prel * @param {number | Pattern} time time in seconds * @example @@ -1905,6 +2068,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 * @param {number | Pattern} semitones change in semitones * @example * note("c") @@ -1916,6 +2080,7 @@ export const { penv } = registerControl('penv'); * Curve of envelope. Defaults to linear. exponential is good for kicks * * @name pcurve + * @tags pitch, envelope, superdough * @param {number | Pattern} type 0 = linear, 1 = exponential * @example * note("g1*4") @@ -1932,6 +2097,7 @@ export const { pcurve } = registerControl('pcurve'); * If you don't set an anchor, the value will default to the psustain value. * * @name panchor + * @tags pitch, envelope, superdough * @param {number | Pattern} anchor anchor offset * @example * note("c c4").penv(12).panchor("<0 .5 1 .5>") @@ -1953,6 +2119,7 @@ export const { gate, gat } = registerControl('gate', 'gat'); * Emulation of a Leslie speaker: speakers rotating in a wooden amplified cabinet. * * @name leslie + * @tags superdirt * @param {number | Pattern} wet between 0 and 1 * @example * n("0,4,7").s("supersquare").leslie("<0 .4 .6 1>").osc() @@ -1964,6 +2131,7 @@ export const { leslie } = registerControl('leslie'); * Rate of modulation / rotation for leslie effect * * @name lrate + * @tags superdirt * @param {number | Pattern} rate 6.7 for fast, 0.7 for slow * @example * n("0,4,7").s("supersquare").leslie(1).lrate("<1 2 4 8>").osc() @@ -1976,6 +2144,7 @@ export const { lrate } = registerControl('lrate'); * Physical size of the cabinet in meters. Be careful, it might be slightly larger than your computer. Affects the Doppler amount (pitch warble) * * @name lsize + * @tags superdirt * @param {number | Pattern} meters somewhere between 0 and 1 * @example * n("0,4,7").s("supersquare").leslie(1).lrate(2).lsize("<.1 .5 1>").osc() @@ -1987,6 +2156,7 @@ export const { lsize } = registerControl('lsize'); * Sets the displayed text for an event on the pianoroll * * @name label + * @tags visualization * @param {string} label text to display */ export const { activeLabel } = registerControl('activeLabel'); @@ -2023,6 +2193,7 @@ export const { nudge } = registerControl('nudge'); * Sets the default octave of a synth. * * @name octave + * @tags superdirt * @synonyms oct * @param {number | Pattern} octave octave number * @example @@ -2036,6 +2207,7 @@ export const { octave, oct } = registerControl('octave', 'oct'); * An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects. * * @name orbit + * @tags superdough * @synonyms o * @param {number | Pattern} number * @example @@ -2054,6 +2226,7 @@ export const { orbit } = registerControl('orbit', 'o'); * otherPat.bmod(..) (to modulate another pattern with the bus) * * @name bus + * @tags superdirt * @param {number | Pattern} number */ export const { bus } = registerControl('bus'); @@ -2062,6 +2235,7 @@ export const { bus } = registerControl('bus'); * Postgain multiplier prior to sending the signal to the audio bus. * * @name busgain + * @tags superdirt * @synonyms bgain * @param {number | Pattern} number */ @@ -2075,6 +2249,7 @@ export const { overshape } = registerControl('overshape'); * Sets position in stereo. * * @name pan + * @tags superdough, supradough * @param {number | Pattern} pan between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel) * @example * s("[bd hh]*2").pan("<.5 1 .5 0>") @@ -2083,22 +2258,22 @@ export const { overshape } = registerControl('overshape'); * */ export const { pan } = registerControl('pan'); -// TODO: this has no effect (see example) -/* +/** * Controls how much multichannel output is fanned out * * @name panspan + * @tags superdirt * @param {number | Pattern} span between -inf and inf, negative is backwards ordering * @example * s("[bd hh]*2").pan("<.5 1 .5 0>").panspan("<0 .5 1>").osc() * */ export const { panspan } = registerControl('panspan'); -// TODO: this has no effect (see example) -/* +/** * Controls how much multichannel output is spread * * @name pansplay + * @tags superdirt * @param {number | Pattern} spread between 0 and 1 * @example * s("[bd hh]*2").pan("<.5 1 .5 0>").pansplay("<0 .5 1>").osc() @@ -2123,6 +2298,7 @@ export const { voice } = registerControl('voice'); /** * The chord to voice * @name chord + * @tags tonal * @param {string | Pattern} symbols chord symbols to voice e.g., C, Eb, Fm7, G7. The symbols can be defined via addVoicings * @example * chord("").voicing() @@ -2132,6 +2308,7 @@ export const { chord } = registerControl('chord'); * Which dictionary to use for the voicings. This falls back to the default dictionary if not provided * * @name dictionary + * @tags tonal * @param {string} dictionaryName which dictionary (having been defined with `addVoicings`) to use * @example * addVoicings('house', { @@ -2146,15 +2323,19 @@ export const { dictionary, dict } = registerControl('dictionary', 'dict'); /** The top note to align the voicing to. Defaults to c5 * * @name anchor - * @param {string | Pattern} anchorNote the note to align the voicings to + * @tags tonal + * @param {string | Pattern} anchorNote the note to align the voicing or scale to * @example * anchor("").chord("C").voicing() + * @example + * n("0 .. 7").anchor("").scale("") **/ export const { anchor } = registerControl('anchor'); /** * Sets how the voicing is offset from the anchored position * * @name offset + * @tags tonal * @param {number | Pattern} shift the amount to shift the voicing up or down * @example * chord("").offset("<0 1 2 3 4 5>") // alter the voicing each time @@ -2164,6 +2345,7 @@ export const { offset } = registerControl('offset'); * How many octaves are voicing steps spread apart, defaults to 1 * * @name octaves + * @tags tonal * @param {number | Pattern} count the number of octaves * @example * chord("").octaves("<2 4>").voicing() @@ -2173,6 +2355,7 @@ export const { octaves } = registerControl('octaves'); * Remove anchor note from the voicing. Useful for melody harmonization * * @name mode + * @tags tonal * @param {string | Pattern} modeName one of {below | above | duck | root} * @example * mode("").chord("C").voicing() @@ -2186,6 +2369,7 @@ export const { mode } = registerControl(['mode', 'anchor']); * When using mininotation, you can also optionally add the 'size' parameter, separated by ':'. * * @name room + * @tags orbit, superdough * @param {number | Pattern} level between 0 and 1 * @example * s("bd sd [~ bd] sd").room("<0 .2 .4 .6 .8 1>") @@ -2199,6 +2383,7 @@ export const { room } = registerControl(['room', 'size']); * When this property is changed, the reverb will be recaculated, so only change this sparsely.. * * @name roomlp + * @tags orbit, superdough * @synonyms rlp * @param {number} frequency between 0 and 20000hz * @example @@ -2212,6 +2397,7 @@ export const { roomlp, rlp } = registerControl('roomlp', 'rlp'); * When this property is changed, the reverb will be recaculated, so only change this sparsely.. * * @name roomdim + * @tags orbit, superdough * @synonyms rdim * @param {number} frequency between 0 and 20000hz * @example @@ -2226,6 +2412,7 @@ export const { roomdim, rdim } = registerControl('roomdim', 'rdim'); * When this property is changed, the reverb will be recaculated, so only change this sparsely.. * * @name roomfade + * @tags orbit, superdough * @synonyms rfade * @param {number} seconds for the reverb to fade * @example @@ -2238,6 +2425,7 @@ export const { roomfade, rfade } = registerControl('roomfade', 'rfade'); /** * Sets the sample to use as an impulse response for the reverb. * @name iresponse + * @tags orbit, superdough * @param {string | Pattern} sample to use as an impulse response * @synonyms ir * @example @@ -2249,6 +2437,7 @@ export const { ir, iresponse } = registerControl(['ir', 'i'], 'iresponse'); /** * Sets speed of the sample for the impulse response. * @name irspeed + * @tags orbit, superdough * @param {string | Pattern} speed * @example * samples('github:switchangel/pad') @@ -2260,6 +2449,7 @@ export const { irspeed } = registerControl('irspeed'); /** * Sets the beginning of the IR response sample * @name irbegin + * @tags orbit, superdough * @param {string | Pattern} begin between 0 and 1 * @synonyms ir * @example @@ -2273,6 +2463,7 @@ export const { irbegin } = registerControl('irbegin'); * When this property is changed, the reverb will be recaculated, so only change this sparsely.. * * @name roomsize + * @tags orbit, superdough * @param {number | Pattern} size between 0 and 10 * @synonyms rsize, sz, size * @example @@ -2296,6 +2487,7 @@ export const { roomsize, size, sz, rsize } = registerControl('roomsize', 'size', * * * @name shape + * @tags distortion, superdough * @param {number | Pattern} distortion between 0 and 1 * @example * s("bd sd [~ bd] sd,hh*8").shape("<0 .2 .4 .6 .8>") @@ -2308,6 +2500,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 * @synonyms dist * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion @@ -2326,7 +2519,8 @@ export const { distort, dist } = registerControl(['distort', 'distortvol', 'dist * Postgain for waveshaping distortion. * * @name distortvol - * @synonyms distvol + * @synonyms distortion, distvol + * @tags superdough, supradough * @param {number | Pattern} volume linear postgain of the distortion * @example * s("bd*4").bank("tr909").distort(2).distortvol(0.8) @@ -2337,6 +2531,7 @@ export const { distortvol } = registerControl('distortvol', 'distvol'); * Type of waveshaping distortion to apply. * * @name distorttype + * @tags distortion, superdough, supradough * @synonyms disttype * @param {number | string | Pattern} type type of distortion to apply * @example @@ -2355,6 +2550,7 @@ export const { distorttype } = registerControl('distorttype', 'disttype'); * More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties) * * @name compressor + * @tags superdough * @example * s("bd sd [~ bd] sd,hh*8") * .compressor("-20:20:10:.002:.02") @@ -2375,6 +2571,7 @@ export const { compressorRelease } = registerControl('compressorRelease'); * Changes the speed of sample playback, i.e. a cheap way of changing pitch. * * @name speed + * @tags pitch, samples * @param {number | Pattern} speed -inf to inf, negative numbers play the sample backwards. * @example * s("bd*6").speed("1 2 4 1 -2 -4") @@ -2392,6 +2589,7 @@ export const { speed } = registerControl('speed'); * and tuning down by octaves with -2, -3, -3.5... * * @name stretch + * @tags pitch, samples * @param {number | Pattern} factor between `-4` and `inf`. Positive increases pitch, 0 does nothing, negative decreases the pitch. * @example * s("gm_flute").stretch("<2 1 0 -2>") @@ -2402,6 +2600,7 @@ export const { stretch } = registerControl('stretch'); * Used in conjunction with `speed`, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`. * * @name unit + * @tags superdirt * @param {number | string | Pattern} unit see description above * @example * speed("1 2 .5 3").s("bd").unit("c").osc() @@ -2416,6 +2615,7 @@ export const { unit } = registerControl('unit'); * "A simplistic pitch-raising algorithm. It's not meant to sound natural; its sound is reminiscent of some weird mixture of filter, ring-modulator and pitch-shifter, depending on the input. The algorithm works by cutting the signal into fragments (delimited by upwards-going zero-crossings) and squeezing those fragments in the time domain (i.e. simply playing them back faster than they came in), leaving silences inbetween. All the parameters apart from memlen can be modulated." * * @name squiz + * @tags superdirt * @param {number | Pattern} squiz Try passing multiples of 2 to it - 2, 4, 8 etc. * @example * squiz("2 4/2 6 [8 16]").s("bd").osc() @@ -2440,6 +2640,7 @@ export const { squiz } = registerControl('squiz'); * Formant filter to make things sound like vowels. * * @name vowel + * @tags superdough * @param {string | Pattern} vowel You can use a e i o u ae aa oe ue y uh un en an on, corresponding to [a] [e] [i] [o] [u] [æ] [ɑ] [ø] [y] [ɯ] [ʌ] [œ̃] [ɛ̃] [ɑ̃] [ɔ̃]. Aliases: aa = å = ɑ, oe = ø = ö, y = ı, ae = æ. * @example * note("[c2 >]*2").s('sawtooth') @@ -2461,9 +2662,10 @@ export const { vowel } = registerControl('vowel'); */ export const { waveloss } = registerControl('waveloss'); /** - * Noise crackle density + * crackle noise density * * @name density + * @tags superdough * @param {number | Pattern} density between 0 and x * @example * s("crackle*4").density("<0.01 0.04 0.2 0.5>".slow(4)) @@ -2513,6 +2715,7 @@ export const { cps } = registerControl('cps'); * Multiplies the duration with the given number. Also cuts samples off at the end if they exceed the duration. * * @name clip + * @tags superdough * @synonyms legato * @param {number | Pattern} factor >= 0 * @example @@ -2525,6 +2728,7 @@ export const { clip, legato } = registerControl('clip', 'legato'); * Sets the duration of the event in cycles. Similar to clip / legato, it also cuts samples off at the end if they exceed the duration. * * @name duration + * @tags superdough * @synonyms dur * @param {number | Pattern} seconds >= 0 * @example @@ -2552,6 +2756,7 @@ export const { zzfx } = registerControl('zzfx'); /** * Sets the color of the hap in visualizations like pianoroll or highlighting. * @name color + * @tags visualization * @synonyms colour * @param {string} color Hexadecimal or CSS color name */ @@ -2566,6 +2771,7 @@ export let createParams = (...names) => * ADSR envelope: Combination of Attack, Decay, Sustain, and Release. * * @name adsr + * @tags envelope, amplitude * @param {number | Pattern} time attack time in seconds * @param {number | Pattern} time decay time in seconds * @param {number | Pattern} gain sustain level (0 to 1) @@ -2600,6 +2806,7 @@ export const ar = register('ar', (t, pat) => { * MIDI channel: Sets the MIDI channel for the event. * * @name midichan + * @tags external_io, midi * @param {number | Pattern} channel MIDI channel number (0-15) * @example * note("c4").midichan(1).midi() @@ -2612,6 +2819,7 @@ export const { midimap } = registerControl('midimap'); * MIDI port: Sets the MIDI port for the event. * * @name midiport + * @tags external_io, midi * @param {number | Pattern} port MIDI port * @example * note("c a f e").midiport("<0 1 2 3>").midi() @@ -2622,6 +2830,7 @@ export const { midiport } = registerControl('midiport'); * MIDI command: Sends a MIDI command message. * * @name midicmd + * @tags external_io, midi * @param {number | Pattern} command MIDI command * @example * midicmd("clock*48,/2").midi() @@ -2632,6 +2841,7 @@ export const { midicmd } = registerControl('midicmd'); * MIDI control: Sends a MIDI control change message. * * @name control + * @tags external_io, midi * @param {number | Pattern} MIDI control number (0-127) * @param {number | Pattern} MIDI controller value (0-127) */ @@ -2647,6 +2857,7 @@ export const control = register('control', (args, pat) => { * MIDI control number: Sends a MIDI control change message. * * @name ccn + * @tags external_io, midi * @param {number | Pattern} MIDI control number (0-127) */ export const { ccn } = registerControl('ccn'); @@ -2654,6 +2865,7 @@ export const { ccn } = registerControl('ccn'); * MIDI control value: Sends a MIDI control change message. * * @name ccv + * @tags external_io, midi * @param {number | Pattern} MIDI control value (0-127) */ export const { ccv } = registerControl('ccv'); @@ -2663,6 +2875,7 @@ export const { ctlNum } = registerControl('ctlNum'); /** * MIDI NRPN non-registered parameter number: Sends a MIDI NRPN non-registered parameter number message. * @name nrpnn + * @tags external_io, midi * @param {number | Pattern} nrpnn MIDI NRPN non-registered parameter number (0-127) * @example * note("c4").nrpnn("1:8").nrpv("123").midichan(1).midi() @@ -2671,6 +2884,7 @@ export const { nrpnn } = registerControl('nrpnn'); /** * MIDI NRPN non-registered parameter value: Sends a MIDI NRPN non-registered parameter value message. * @name nrpv + * @tags external_io, midi * @param {number | Pattern} nrpv MIDI NRPN non-registered parameter value (0-127) * @example * note("c4").nrpnn("1:8").nrpv("123").midichan(1).midi() @@ -2681,6 +2895,7 @@ export const { nrpv } = registerControl('nrpv'); * MIDI program number: Sends a MIDI program change message. * * @name progNum + * @tags external_io * @param {number | Pattern} program MIDI program number (0-127) * @example * note("c4").progNum(10).midichan(1).midi() @@ -2690,6 +2905,7 @@ export const { progNum } = registerControl('progNum'); /** * MIDI sysex: Sends a MIDI sysex message. * @name sysex + * @tags external_io, midi * @param {number | Pattern} id Sysex ID * @param {number | Pattern} data Sysex data * @example @@ -2705,6 +2921,7 @@ export const sysex = register('sysex', (args, pat) => { /** * MIDI sysex ID: Sends a MIDI sysex identifier message. * @name sysexid + * @tags external_io, midi * @param {number | Pattern} id Sysex ID * @example * note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi() @@ -2713,6 +2930,7 @@ export const { sysexid } = registerControl('sysexid'); /** * MIDI sysex data: Sends a MIDI sysex message. * @name sysexdata + * @tags external_io, midi * @param {number | Pattern} data Sysex data * @example * note("c4").sysexid("0x77").sysexdata("0x01:0x02:0x03:0x04").midichan(1).midi() @@ -2722,6 +2940,7 @@ export const { sysexdata } = registerControl('sysexdata'); /** * MIDI pitch bend: Sends a MIDI pitch bend message. * @name midibend + * @tags external_io, midi * @param {number | Pattern} midibend MIDI pitch bend (-1 - 1) * @example * note("c4").midibend(sine.slow(4).range(-0.4,0.4)).midi() @@ -2730,6 +2949,7 @@ export const { midibend } = registerControl('midibend'); /** * MIDI key after touch: Sends a MIDI key after touch message. * @name miditouch + * @tags external_io, midi * @param {number | Pattern} miditouch MIDI key after touch (0-1) * @example * note("c4").miditouch(sine.slow(4).range(0,1)).midi() @@ -2742,6 +2962,7 @@ export const { polyTouch } = registerControl('polyTouch'); /** * The host to send open sound control messages to. Requires running the OSC bridge. * @name oschost + * @tags external_io * @param {string | Pattern} oschost e.g. 'localhost' * @example * note("c4").oschost('127.0.0.1').oscport(57120).osc(); @@ -2751,6 +2972,7 @@ export const { oschost } = registerControl('oschost'); /** * The port to send open sound control messages to. Requires running the OSC bridge. * @name oscport + * @tags external_io * @param {number | Pattern} oscport e.g. 57120 * @example * note("c4").oschost('127.0.0.1').oscport(57120).osc(); @@ -2768,6 +2990,7 @@ export const getControlName = (alias) => { * Sets properties in a batch. * * @name as + * @tags combiners * @param {String | Array} mapping the control names that are set * @example * "c:.5 a:1 f:.25 e:.8".as("note:clip") @@ -2792,6 +3015,7 @@ export const as = register('as', (mapping, pat) => { * Allows you to scrub an audio file like a tape loop by passing values that represents the position in the audio file * in the optional array syntax ex: "0.5:2", the second value controls the speed of playback * @name scrub + * @tags samples * @memberof Pattern * @returns Pattern * @example @@ -2850,6 +3074,7 @@ registerSubControls('lfo', [ ['skew', 'sk'], ['curve', 'cu'], ['sync', 's'], + ['retrig', 'rt'], ['fxi'], ]); registerSubControls('env', [ @@ -2931,17 +3156,19 @@ Pattern.prototype.modulate = function (type, config, idPat) { * a `sometimes`. See example below. * * @name lfo + * @tags lfo, superdough * @param {Object} config LFO configuration. * @param {string | Pattern} [config.control] Node to modulate. Aliases: c * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc * @param {number | Pattern} [config.rate] Modulation rate. Aliases: r + * @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da * @param {number | Pattern} [config.dcoffset] DC offset / bias for the waveform. Aliases: dc * @param {number | Pattern} [config.shape] Shape index. Aliases: sh * @param {number | Pattern} [config.skew] Skew amount. Aliases: sk * @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: cu - * @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s + * @param {number | Pattern} [config.retrig] If > 0.5, the LFO will retrigger on each event. Aliases: rt * @param {number | Pattern} [config.fxi] FX index to target * @param {string | Pattern} id ID to use for this modulator * @returns Pattern @@ -2984,6 +3211,7 @@ export const lfo = (config) => pure({}).lfo(config); * a `sometimes`. See example below. * * @name env + * @tags envelope, superdough * @param {Object} config Envelope configuration. * @param {string | Pattern} [config.control] Node to modulate. Aliases: c * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc @@ -3044,6 +3272,7 @@ export const env = (config) => pure({}).env(config); * a `sometimes`. See example below. * * @name bmod + * @tags superdough * @param {Object} config Bus modulation configuration. * @param {string | Pattern} [config.bus] Bus to get modulation signal from * @param {string | Pattern} [config.control] Node to modulate. Aliases: c @@ -3070,6 +3299,7 @@ export const bmod = (config) => pure({}).bmod(config); * and sustains * * @name transient + * @tags superdough * @param {number | Pattern} attack Emphasis on transients; between -1 (deaccentuate) and 1 (accentuate) * @param {number | Pattern} sustain Emphasis on the sustains; between -1 (deaccentuate) and 1 (accentuate) * @example diff --git a/packages/core/drawLine.mjs b/packages/core/drawLine.mjs index 7509c0f6..212d0627 100644 --- a/packages/core/drawLine.mjs +++ b/packages/core/drawLine.mjs @@ -15,6 +15,7 @@ import Fraction, { gcd } from './fraction.mjs'; * - "-" hold previous value * - "." silence * + * @tags visualization * @param {Pattern} pattern the pattern to use * @param {number} chars max number of characters (approximately) * @returns string diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 98c21a11..49f96384 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -61,6 +61,7 @@ export const bjorklund = function (ons, steps) { * * @memberof Pattern * @name euclid + * @tags temporal * @param {number} pulses the number of onsets/beats * @param {number} steps the number of steps to fill * @returns Pattern @@ -73,6 +74,7 @@ export const bjorklund = function (ons, steps) { * Like `euclid`, but has an additional parameter for 'rotating' the resulting sequence. * @memberof Pattern * @name euclidRot + * @tags temporal * @param {number} pulses the number of onsets/beats * @param {number} steps the number of steps to fill * @param {number} rotation offset in steps @@ -156,6 +158,7 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun * so there will be no gaps. * @name euclidLegato * @memberof Pattern + * @tags temporal * @param {number} pulses the number of onsets/beats * @param {number} steps the number of steps to fill * @param rotation offset in steps @@ -187,6 +190,7 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps, * the resulting sequence * @name euclidLegatoRot * @memberof Pattern + * @tags temporal * @param {number} pulses the number of onsets/beats * @param {number} steps the number of steps to fill * @param {number} rotation offset in steps @@ -208,6 +212,7 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s * @name euclidish * @synonyms eish * @memberof Pattern + * @tags temporal * @param {number} pulses the number of onsets * @param {number} steps the number of steps to fill * @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse) diff --git a/packages/core/evaluate.mjs b/packages/core/evaluate.mjs index 0599d866..bb7b4664 100644 --- a/packages/core/evaluate.mjs +++ b/packages/core/evaluate.mjs @@ -5,6 +5,34 @@ This program is free software: you can redistribute it and/or modify it under th */ export const strudelScope = {}; +// Make strudelScope available globally so transpiled code can access it +globalThis.strudelScope = strudelScope; + +// Track user-defined keys (from block-based eval) so we can clear them without removing strudel functions +export const userDefinedKeys = new Set(); +globalThis.userDefinedKeys = userDefinedKeys; + +/** + * Clears all user-defined variables and functions from the scope. + * This removes variables created during block-based evaluation. + * @name clearScope + * @example + * // After defining variables in blocks: + * // let myVar = 5 + * // function myFunc() { return 10; } + * clearScope() // removes myVar and myFunc from scope + */ +export const clearScope = () => { + for (const key of userDefinedKeys) { + delete strudelScope[key]; + delete globalThis[key]; + } + userDefinedKeys.clear(); + // Return silence if available (for use in pattern expressions), otherwise undefined + return globalThis.silence; +}; +// Make clearScope available globally +globalThis.clearScope = clearScope; export const evalScope = async (...args) => { const results = await Promise.allSettled(args); diff --git a/packages/core/package.json b/packages/core/package.json index abb54e9c..9316ec49 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/core", - "version": "1.2.5", + "version": "1.2.6", "description": "Port of Tidal Cycles to JavaScript", "main": "index.mjs", "type": "module", @@ -31,7 +31,6 @@ }, "homepage": "https://strudel.cc", "dependencies": { - "@kabelsalat/web": "^0.4.1", "fraction.js": "^5.2.1" }, "gitHead": "0e26d4e741500f5bae35b023608f062a794905c2", diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 7466f594..c5b7953c 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -85,6 +85,7 @@ export class Pattern { /** * Returns a new pattern, with the function applied to the value of * each hap. It has the alias `fmap`. + * @tags functional * @synonyms fmap * @param {Function} func to to apply to the value * @returns Pattern @@ -114,6 +115,7 @@ export class Pattern { * Assumes 'this' is a pattern of functions, and given a function to * resolve wholes, applies a given pattern of values to that * pattern of functions. + * @tags functional * @param {Function} whole_func * @param {Function} func * @noAutocomplete @@ -151,6 +153,7 @@ export class Pattern { * In this `_appBoth` variant, where timespans of the function and value haps * are not the same but do intersect, the resulting hap has a timespan of the * intersection. This applies to both the part and the whole timespan. + * @tags functional * @param {Pattern} pat_val * @noAutocomplete * @returns Pattern @@ -178,6 +181,7 @@ export class Pattern { * on. In practice, this means that the pattern structure, including onsets, * are preserved from the pattern of functions (often referred to as the left * hand or inner pattern). + * @tags functional * @param {Pattern} pat_val * @noAutocomplete * @returns Pattern @@ -211,6 +215,7 @@ export class Pattern { * As with `appLeft`, but `whole` timespans are instead taken from the * pattern of values, i.e. structure is preserved from the right hand/outer * pattern. + * @tags functional * @param {Pattern} pat_val * @noAutocomplete * @returns Pattern @@ -401,6 +406,7 @@ export class Pattern { /** * Query haps inside the given time span. * + * @tags internals * @param {Fraction | number} begin from time * @param {Fraction | number} end to time * @returns Hap[] @@ -424,6 +430,7 @@ export class Pattern { * Returns a new pattern, with queries split at cycle boundaries. This makes * some calculations easier to express, as all haps are then constrained to * happen within a cycle. + * @tags internals * @returns Pattern * @noAutocomplete */ @@ -438,6 +445,7 @@ export class Pattern { /** * Returns a new pattern, where the given function is applied to the query * timespan before passing it to the original pattern. + * @tags internals * @param {Function} func the function to apply * @returns Pattern * @noAutocomplete @@ -460,6 +468,7 @@ export class Pattern { /** * As with `withQuerySpan`, but the function is applied to both the * begin and end time of the query timespan. + * @tags internals * @param {Function} func the function to apply * @returns Pattern * @noAutocomplete @@ -472,6 +481,7 @@ export class Pattern { * Similar to `withQuerySpan`, but the function is applied to the timespans * of all haps returned by pattern queries (both `part` timespans, and where * present, `whole` timespans). + * @tags internals * @param {Function} func * @returns Pattern * @noAutocomplete @@ -483,6 +493,7 @@ export class Pattern { /** * As with `withHapSpan`, but the function is applied to both the * begin and end time of the hap timespans. + * @tags internals * @param {Function} func the function to apply * @returns Pattern * @noAutocomplete @@ -493,6 +504,7 @@ export class Pattern { /** * Returns a new pattern with the given function applied to the list of haps returned by every query. + * @tags internals * @param {Function} func * @returns Pattern * @noAutocomplete @@ -505,6 +517,7 @@ export class Pattern { /** * As with `withHaps`, but applies the function to every hap, rather than every list of haps. + * @tags internals * @param {Function} func * @returns Pattern * @noAutocomplete @@ -515,6 +528,7 @@ export class Pattern { /** * Returns a new pattern with the context field set to every hap set to the given value. + * @tags internals * @param {*} context * @returns Pattern * @noAutocomplete @@ -525,6 +539,7 @@ export class Pattern { /** * Returns a new pattern with the given function applied to the context field of every hap. + * @tags internals * @param {Function} func * @returns Pattern * @noAutocomplete @@ -540,6 +555,7 @@ export class Pattern { /** * Returns a new pattern with the context field of every hap set to an empty object. + * @tags internals * @returns Pattern * @noAutocomplete */ @@ -550,6 +566,7 @@ export class Pattern { /** * Returns a new pattern with the given location information added to the * context of every hap. + * @tags internals * @param {Number} start start offset * @param {Number} end end offset * @returns Pattern @@ -573,6 +590,7 @@ export class Pattern { /** * Returns a new Pattern, which only returns haps that meet the given test. + * @tags internals * @param {Function} hap_test - a function which returns false for haps to be removed from the pattern * @returns Pattern * @example @@ -585,6 +603,7 @@ export class Pattern { /** * As with `filterHaps`, but the function is applied to values * inside haps. + * @tags internals * @param {Function} value_test * @returns Pattern * @example @@ -600,6 +619,7 @@ export class Pattern { /** * Returns a new pattern, with haps containing undefined values removed from * query results. + * @tags internals * @returns Pattern * @noAutocomplete */ @@ -611,6 +631,7 @@ export class Pattern { * Returns a new pattern, with all haps without onsets filtered out. A hap * with an onset is one with a `whole` timespan that begins at the same time * as its `part` timespan. + * @tags internals * @returns Pattern * @noAutocomplete */ @@ -624,6 +645,7 @@ export class Pattern { /** * Returns a new pattern, with 'continuous' haps (those without 'whole' * timespans) removed from query results. + * @tags internals * @returns Pattern * @noAutocomplete */ @@ -635,6 +657,7 @@ export class Pattern { /** * Combines adjacent haps with the same value and whole. Only * intended for use in tests. + * @tags internals * @noAutocomplete */ defragmentHaps() { @@ -687,6 +710,7 @@ export class Pattern { /** * Queries the pattern for the first cycle, returning Haps. Mainly of use when * debugging a pattern. + * @tags internals * @param {Boolean} with_context - set to true, otherwise the context field * will be stripped from the resulting haps. * @returns [Hap] @@ -702,6 +726,7 @@ export class Pattern { /** * Accessor for a list of values returned by querying the first cycle. + * @tags internals * @noAutocomplete */ get firstCycleValues() { @@ -710,6 +735,7 @@ export class Pattern { /** * More human-readable version of the `firstCycleValues` accessor. + * @tags internals * @noAutocomplete */ get showFirstCycle() { @@ -721,6 +747,7 @@ export class Pattern { /** * Returns a new pattern, which returns haps sorted in temporal order. Mainly * of use when comparing two patterns for equality, in tests. + * @tags internals * @returns Pattern * @noAutocomplete */ @@ -735,6 +762,10 @@ export class Pattern { ); } + /** + * Returns a new pattern with all values parsed as numerals. + * @tags internals + */ asNumber() { return this.fmap(parseNumeral); } @@ -785,8 +816,8 @@ export class Pattern { /** * Layers the result of the given function(s). Like `superimpose`, but without the original pattern: * @name layer + * @tags combiners * @memberof Pattern - * @synonyms apply * @returns Pattern * @example * "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*8" @@ -800,6 +831,7 @@ export class Pattern { /** * Superimposes the result of the given function(s) on top of the original pattern: * @name superimpose + * @tags combiners * @memberof Pattern * @returns Pattern * @example @@ -859,6 +891,7 @@ export class Pattern { /** * Writes the content of the current event to the console (visible in the side menu). + * @tags visualization * @name log * @memberof Pattern * @example @@ -873,6 +906,7 @@ export class Pattern { /** * A simplified version of `log` which writes all "values" (various configurable parameters) * within the event to the console (visible in the side menu). + * @tags visualization * @name logValues * @memberof Pattern * @example @@ -906,6 +940,7 @@ export class Pattern { * source pattern to be looped, and for an (optional) given function to be * applied. False values result in the corresponding part of the source pattern * to be played unchanged. + * @tags temporal * @name into * @memberof Pattern * @example @@ -945,6 +980,7 @@ Pattern.prototype.collect = function () { /** * Selects indices in in stacked notes. + * @tags temporal * @example * note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>") * .arpWith(haps => haps[2]) @@ -959,6 +995,7 @@ export const arpWith = register('arpWith', (func, pat) => { /** * Selects indices in in stacked notes. + * @tags temporal * @example * note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>") * .arp("0 [0,2] 1 [0,2]") @@ -1017,87 +1054,135 @@ function _composeOp(a, b, func) { return func(a, b); } -// Make composers -(function () { - // pattern composers - const composers = { - set: [(a, b) => b], - keep: [(a) => a], - keepif: [(a, b) => (b ? a : undefined)], +// pattern composers +const COMPOSERS = { + /** + * When called on a pattern `a`, with a input pattern `b` (`a.set(b)`), + * combines `a` and `b` such that anything defined in `b` + * and anything defined in `a` that is *not* defined in `b` + * will be in the resulting pattern. + * + * The structure is maintained from `a`, + * because the default pattern alignment is `in`, + * see the section on `Pattern Alignment` + * in the technical manual in the docs + * + * This is the inverse of `keep` + * + * See examples below + * @name set + * @param {Pattern} pat + * @returns {Pattern} + * @memberof Pattern + * @tags internal, combiners + * @example + * // because input pattern has `s` set, + * // it overrides the "sine" declared earlier + * note("c a f e").s("sine").set(s("triangle")) + */ + set: [(a, b) => b], + /** + * When called on a pattern `a`, with a input pattern `b` (`a.keep(b)`), + * combines `a` and `b` such that anything defined in `a`, + * and anything defined in `b` that is *not* defined in `a` + * will be in the resulting pattern + * + * The structure is maintained from `a`, + * because the default pattern alignment is `in`, + * see the section on `Pattern Alignment` + * in the technical manual in the docs + * + * This is the inverse of `set` + * + * See examples below + * @name keep + * @param {Pattern} pat + * @memberof Pattern + * @returns {Pattern} + * @tags internal, combiners + * @example + * // notes, already defined, will stay "c a f e", + * // while "s", not defined, will be set to "piano" + * note("c a f e").keep(note("e f a c").s("piano")) + */ + keep: [(a) => a], + keepif: [(a, b) => (b ? a : undefined)], - // numerical functions - /** - * - * Assumes a pattern of numbers. Adds the given number to each item in the pattern. - * @name add - * @memberof Pattern - * @example - * // Here, the triad 0, 2, 4 is shifted by different amounts - * n("0 2 4".add("<0 3 4 0>")).scale("C:major") - * // Without add, the equivalent would be: - * // n("<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>").scale("C:major") - * @example - * // You can also use add with notes: - * note("c3 e3 g3".add("<0 5 7 0>")) - * // Behind the scenes, the notes are converted to midi numbers: - * // note("48 52 55".add("<0 5 7 0>")) - */ - add: [numeralArgs((a, b) => a + b)], // support string concatenation - /** - * - * Like add, but the given numbers are subtracted. - * @name sub - * @memberof Pattern - * @example - * n("0 2 4".sub("<0 1 2 3>")).scale("C4:minor") - * // See add for more information. - */ - sub: [numeralArgs((a, b) => a - b)], - /** - * - * Multiplies each number by the given factor. - * @name mul - * @memberof Pattern - * @example - * "<1 1.5 [1.66, <2 2.33>]>*4".mul(150).freq() - */ - mul: [numeralArgs((a, b) => a * b)], - /** - * - * Divides each number by the given factor. - * @name div - * @memberof Pattern - */ - div: [numeralArgs((a, b) => a / b)], - mod: [numeralArgs(_mod)], - pow: [numeralArgs(Math.pow)], - log2: [numeralArgs(Math.log2)], - band: [numeralArgs((a, b) => a & b)], - bor: [numeralArgs((a, b) => a | b)], - bxor: [numeralArgs((a, b) => a ^ b)], - blshift: [numeralArgs((a, b) => a << b)], - brshift: [numeralArgs((a, b) => a >> b)], + // numerical functions + /** + * + * Assumes a pattern of numbers. Adds the given number to each item in the pattern. + * @name add + * @memberof Pattern + * @tags math + * @example + * // Here, the triad 0, 2, 4 is shifted by different amounts + * n("0 2 4".add("<0 3 4 0>")).scale("C:major") + * // Without add, the equivalent would be: + * // n("<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>").scale("C:major") + * @example + * // You can also use add with notes: + * note("c3 e3 g3".add("<0 5 7 0>")) + * // Behind the scenes, the notes are converted to midi numbers: + * // note("48 52 55".add("<0 5 7 0>")) + */ + add: [numeralArgs((a, b) => a + b)], // support string concatenation + /** + * + * Like add, but the given numbers are subtracted. + * @name sub + * @memberof Pattern + * @tags math + * @example + * n("0 2 4".sub("<0 1 2 3>")).scale("C4:minor") + * // See add for more information. + */ + sub: [numeralArgs((a, b) => a - b)], + /** + * + * Multiplies each number by the given factor. + * @name mul + * @memberof Pattern + * @tags math + * @example + * "<1 1.5 [1.66, <2 2.33>]>*4".mul(150).freq() + */ + mul: [numeralArgs((a, b) => a * b)], + /** + * + * Divides each number by the given factor. + * @name div + * @memberof Pattern + * @tags math + */ + div: [numeralArgs((a, b) => a / b)], + mod: [numeralArgs(_mod)], + pow: [numeralArgs(Math.pow)], + band: [numeralArgs((a, b) => a & b)], + bor: [numeralArgs((a, b) => a | b)], + bxor: [numeralArgs((a, b) => a ^ b)], + blshift: [numeralArgs((a, b) => a << b)], + brshift: [numeralArgs((a, b) => a >> b)], - // TODO - force numerical comparison if both look like numbers? - lt: [(a, b) => a < b], - gt: [(a, b) => a > b], - lte: [(a, b) => a <= b], - gte: [(a, b) => a >= b], - eq: [(a, b) => a == b], - eqt: [(a, b) => a === b], - ne: [(a, b) => a != b], - net: [(a, b) => a !== b], - and: [(a, b) => a && b], - or: [(a, b) => a || b], + // TODO - force numerical comparison if both look like numbers? + lt: [(a, b) => a < b], + gt: [(a, b) => a > b], + lte: [(a, b) => a <= b], + gte: [(a, b) => a >= b], + eq: [(a, b) => a == b], + eqt: [(a, b) => a === b], + ne: [(a, b) => a != b], + net: [(a, b) => a !== b], + and: [(a, b) => a && b], + or: [(a, b) => a || b], - // bitwise ops - func: [(a, b) => b(a)], - }; - - const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart', 'Poly']; + // bitwise ops + func: [(a, b) => b(a)], +}; +const _setupAlignments = () => { // generate methods to do what and how - for (const [what, [op, preprocess]] of Object.entries(composers)) { + for (const [what, [op, preprocess]] of Object.entries(COMPOSERS)) { // make plain version, e.g. pat._add(value) adds that plain value // to all the values in pat Pattern.prototype['_' + what] = function (value) { @@ -1106,16 +1191,18 @@ function _composeOp(a, b, func) { // make patternified monster version Object.defineProperty(Pattern.prototype, what, { + // Set to configurable so we can update if the default alignment changes + configurable: true, // a getter that returns a function, so 'pat' can be // accessed by closures that are methods of that function.. get: function () { const pat = this; // wrap the 'in' function as default behaviour - const wrapper = (...other) => pat[what]['in'](...other); + const wrapper = (...other) => pat[what][DEFAULT_ALIGNMENT](...other); // add methods to that function for each behaviour - for (const how of hows) { + for (const how of ALIGNMENTS) { wrapper[how.toLowerCase()] = function (...other) { var howpat = pat; other = sequence(other); @@ -1140,19 +1227,28 @@ function _composeOp(a, b, func) { return wrapper; }, }); - - // Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2) - for (const how of hows) { - Pattern.prototype[how.toLowerCase()] = function (...args) { - return this.set[how.toLowerCase()](args); - }; - } } +}; +let DEFAULT_ALIGNMENT = 'in'; +const ALIGNMENTS = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart', 'Poly']; +const ALIGNMENT_KEYS = ALIGNMENTS.map((how) => how.toLowerCase()); + +// Make composers +(function () { + _setupAlignments(); + + // Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2) + for (const how of ALIGNMENTS) { + Pattern.prototype[how.toLowerCase()] = function (...args) { + return this.set[how.toLowerCase()](args); + }; + } // binary composers /** * Applies the given structure to the pattern: * + * @tags temporal * @example * note("c,eb,g") * .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~") @@ -1167,6 +1263,7 @@ function _composeOp(a, b, func) { /** * Returns silence when mask is 0 or "~" * + * @tags temporal * @example * note("c [eb,g] d [eb,g]").mask("<1 [0 1]>") */ @@ -1179,6 +1276,7 @@ function _composeOp(a, b, func) { /** * Resets the pattern to the start of the cycle for each onset of the reset pattern. * + * @tags temporal * @example * s("[ sd]*2, hh*8").reset("") */ @@ -1192,6 +1290,7 @@ function _composeOp(a, b, func) { * Restarts the pattern for each onset of the restart pattern. * While reset will only reset the current cycle, restart will start from cycle 0. * + * @tags temporal * @example * s("[ sd]*2, hh*8").restart("") */ @@ -1203,6 +1302,28 @@ function _composeOp(a, b, func) { }; })(); +/** + * Sets the default method of combining events from two patterns (aka [alignment](https://strudel.cc/technical-manual/alignment/)) in Strudel. + * The default method is 'in', meaning that patterns to the left will (typically) dictate the event timings when combined with patterns to the right. + * By changing alignment to 'out', the opposite will happen. With 'mix', they will combine their event timings. + * + * Note that we say the _default_ method, because alignments can also be set explicitly with calls like + * 'add.mix', 'set.squeeze', etc. + * + * @param {string} method Default join method to use. Options: 'in', 'out', 'mix', 'squeeze', 'squeezeout', 'reset', 'restart', 'poly' + * @tags combiners + * @example + * setDefaultJoin('mix') // also try 'in', 'out', 'squeeze', etc. + * s("saw").vel("1 0.5").note("F A C E").delay("0 0.2 0.3") + */ +export const setDefaultJoin = (alignment) => { + alignment = alignment?.toLowerCase(); + if (DEFAULT_ALIGNMENT !== alignment && ALIGNMENT_KEYS.includes(alignment)) { + DEFAULT_ALIGNMENT = alignment; + _setupAlignments(); + } +}; + // aliases export const polyrhythm = stack; export const pr = stack; @@ -1232,6 +1353,7 @@ export const pm = polymeter; /** * Does absolutely nothing, but with a given metrical 'steps' * @name gap + * @tags generators * @param {number} steps * @example * gap(3) // "~@3" @@ -1241,6 +1363,7 @@ export const gap = (steps) => new Pattern(() => [], steps); /** * Does absolutely nothing.. * @name silence + * @tags generators * @example * silence // "~" */ @@ -1252,6 +1375,7 @@ export const nothing = gap(0); /** * A discrete value that repeats once per cycle. * + * @tags generators * @returns {Pattern} * @example * pure('e4') // "e4" @@ -1293,7 +1417,10 @@ export function reify(thing) { return pure(thing); } -/** Takes a list of patterns, and returns a pattern of lists. +/** + * Takes a list of patterns, and returns a pattern of lists. + * + * @tags temporal */ export function sequenceP(pats) { let result = pure([]); @@ -1306,6 +1433,7 @@ export function sequenceP(pats) { /** * The given items are played at the same time at the same length. * + * @tags temporal * @return {Pattern} * @synonyms polyrhythm, pr * @example @@ -1390,6 +1518,7 @@ export function stackBy(by, ...pats) { /** * Concatenation: combines a list of patterns, switching between them successively, one per cycle. * + * @tags combiners * @return {Pattern} * @synonyms cat * @example @@ -1423,6 +1552,7 @@ export function slowcat(...pats) { } /** Concatenation: combines a list of patterns, switching between them successively, one per cycle. Unlike slowcat, this version will skip cycles. + * @tags combiners * @param {...any} items - The items to concatenate * @return {Pattern} */ @@ -1438,6 +1568,7 @@ export function slowcatPrime(...pats) { /** The given items are con**cat**enated, where each one takes one cycle. * + * @tags combiners * @param {...any} items - The items to concatenate * @synonyms slowcat * @return {Pattern} @@ -1459,6 +1590,7 @@ export function cat(...pats) { * Allows to arrange multiple patterns together over multiple cycles. * Takes a variable number of arrays with two elements specifying the number of cycles and the pattern to use. * + * @tags combiners * @return {Pattern} * @example * arrange( @@ -1476,12 +1608,13 @@ export function arrange(...sections) { * Similarly to `arrange`, allows you to arrange multiple patterns together over multiple cycles. * Unlike `arrange`, you specify a start and stop time for each pattern rather than duration, which * means that patterns can overlap. + * @tags combiners * @return {Pattern} * @example -seqPLoop([0, 2, "bd(3,8)"], - [1, 3, "cp(3,8)"] - ) - .sound() +seqPLoop( + [0, 2, "bd(3,8)"], + [1, 3, "cp(3,8)"] +).sound() */ export function seqPLoop(...parts) { let total = Fraction(0); @@ -1514,13 +1647,17 @@ export function fastcat(...pats) { return result; } -/** See `fastcat` */ +/** See `fastcat` + * @name sequence + * @tags combiners + */ export function sequence(...pats) { return fastcat(...pats); } /** Like **cat**, but the items are crammed into one cycle. - * @synonyms sequence, fastcat + * @tags combiners + * @synonyms fastcat * @example * seq("e5", "b4", ["d5", "c5"]).note() * // "e5 b4 [d5 c5]".note() @@ -1591,6 +1728,7 @@ export const func = curry((a, b) => reify(b).func(a)); /** * Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register. * + * @tags functional * @param {string | string[]} name name of the function, or an array of names to be used as synonyms * @param {function} func function with 1 or more params, where last is the current pattern * @param {bool} patternify defaults to true; if set to false, you will have more control over the arguments to `func` as they will be @@ -1701,6 +1839,7 @@ function stepRegister(name, func, patternify = true, preserveSteps = false, join * Assumes a numerical pattern. Returns a new pattern with all values rounded * to the nearest integer. * @name round + * @tags math * @memberof Pattern * @returns Pattern * @example @@ -1709,13 +1848,13 @@ function stepRegister(name, func, patternify = true, preserveSteps = false, join export const round = register('round', function (pat) { return pat.asNumber().fmap((v) => Math.round(v)); }); - /** * Assumes a numerical pattern. Returns a new pattern with all values set to * their mathematical floor. E.g. `3.7` replaced with to `3`, and `-4.2` * replaced with `-5`. * @name floor * @memberof Pattern + * @tags math * @returns Pattern * @example * note("42 42.1 42.5 43".floor()) @@ -1724,12 +1863,15 @@ export const floor = register('floor', function (pat) { return pat.asNumber().fmap((v) => Math.floor(v)); }); +export const log2 = register('log2', (pat) => pat.asNumber().fmap((v) => Math.log2(v))); + /** * Assumes a numerical pattern. Returns a new pattern with all values set to * their mathematical ceiling. E.g. `3.2` replaced with `4`, and `-4.2` * replaced with `-4`. * @name ceil * @memberof Pattern + * @tags math * @returns Pattern * @example * note("42 42.1 42.5 43".ceil()) @@ -1740,6 +1882,7 @@ export const ceil = register('ceil', function (pat) { /** * Assumes a numerical pattern, containing unipolar values in the range 0 .. * 1. Returns a new pattern with values scaled to the bipolar range -1 .. 1 + * @tags math * @returns Pattern * @noAutocomplete */ @@ -1750,6 +1893,7 @@ export const toBipolar = register('toBipolar', function (pat) { /** * Assumes a numerical pattern, containing bipolar values in the range -1 .. 1 * Returns a new pattern with values scaled to the unipolar range 0 .. 1 + * @tags math * @returns Pattern * @noAutocomplete */ @@ -1763,6 +1907,7 @@ export const fromBipolar = register('fromBipolar', function (pat) { * Most useful in combination with continuous patterns. * @name range * @memberof Pattern + * @tags math * @returns Pattern * @example * s("[bd sd]*2,hh*8") @@ -1778,6 +1923,7 @@ export const range = register('range', function (min, max, pat) { * following an exponential curve. * @name rangex * @memberof Pattern + * @tags math * @returns Pattern * @example * s("[bd sd]*2,hh*8") @@ -1792,6 +1938,7 @@ export const rangex = register('rangex', function (min, max, pat) { * Returns a new pattern with values scaled to the given min/max range. * @name range2 * @memberof Pattern + * @tags math * @returns Pattern * @example * s("[bd sd]*2,hh*8") @@ -1806,6 +1953,7 @@ export const range2 = register('range2', function (min, max, pat) { * Returns a new pattern with just numbers. * @name ratio * @memberof Pattern + * @tags math * @returns Pattern * @example * ratio("1, 5:4, 3:2").mul(110) @@ -1824,6 +1972,7 @@ export const ratio = register('ratio', (pat) => // Structural and temporal transformations /** Compress each cycle into the given timespan, leaving a gap + * @tags temporal * @example * cat( * s("bd sd").compress(.25,.75), @@ -1845,6 +1994,7 @@ export const { compressSpan, compressspan } = register(['compressSpan', 'compres /** * speeds up a pattern like fast, but rather than it playing multiple times as fast would it instead leaves a gap in the remaining space of the cycle. For example, the following will play the sound pattern "bd sn" only once but compressed into the first half of the cycle, i.e. twice as fast. + * @tags temporal * @name fastGap * @synonyms fastgap * @example @@ -1882,6 +2032,7 @@ export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (f /** * Similar to `compress`, but doesn't leave gaps, and the 'focus' can be bigger than a cycle + * @tags temporal * @example * s("bd hh sd hh").focus(1/4, 3/4) */ @@ -1899,6 +2050,7 @@ export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], fun }); /** The ply function repeats each event the given number of times. + * @tags temporal * @example * s("bd ~ sd cp").ply("<1 2 3>") */ @@ -1913,6 +2065,7 @@ export const ply = register('ply', function (factor, pat) { /** * Speed up a pattern by the given factor. Used by "*" in mini notation. * + * @tags temporal * @name fast * @synonyms density * @memberof Pattern @@ -1937,6 +2090,7 @@ export const { fast, density } = register( /** * Both speeds up the pattern (like 'fast') and the sample playback (like 'speed'). + * @tags temporal * @example * s("bd sd:2").hurry("<1 2 4 3>").slow(1.5) */ @@ -1947,6 +2101,7 @@ export const hurry = register('hurry', function (r, pat) { /** * Slow down a pattern over the given number of cycles. Like the "/" operator in mini notation. * + * @tags temporal * @name slow * @synonyms sparsity * @memberof Pattern @@ -1964,6 +2119,7 @@ export const { slow, sparsity } = register(['slow', 'sparsity'], function (facto /** * Carries out an operation 'inside' a cycle. + * @tags temporal * @example * "0 1 2 3 4 3 2 1".inside(4, rev).scale('C major').note() * // "0 1 2 3 4 3 2 1".slow(4).rev().fast(4).scale('C major').note() @@ -1974,6 +2130,7 @@ export const inside = register('inside', function (factor, f, pat) { /** * Carries out an operation 'outside' a cycle. + * @tags temporal * @example * "<[0 1] 2 [3 4] 5>".outside(4, rev).scale('C major').note() * // "<[0 1] 2 [3 4] 5>".fast(4).rev().slow(4).scale('C major').note() @@ -1984,6 +2141,7 @@ export const outside = register('outside', function (factor, f, pat) { /** * Applies the given function every n cycles, starting from the last cycle. + * @tags temporal * @name lastOf * @memberof Pattern * @param {number} n how many cycles @@ -2000,6 +2158,7 @@ export const lastOf = register('lastOf', function (n, func, pat) { /** * Applies the given function every n cycles, starting from the first cycle. + * @tags temporal * @name firstOf * @memberof Pattern * @param {number} n how many cycles @@ -2011,6 +2170,7 @@ export const lastOf = register('lastOf', function (n, func, pat) { /** * An alias for `firstOf` + * @tags temporal * @name every * @memberof Pattern * @param {number} n how many cycles @@ -2026,19 +2186,19 @@ export const { firstOf, every } = register(['firstOf', 'every'], function (n, fu }); /** - * Like layer, but with a single function: + * Applies the given function to the pattern. Like layer, but with a single function: + * @tags combiners * @name apply - * @memberof Pattern * @example * "".scale('C minor').apply(scaleTranspose("0,2,4")).note() */ -// TODO: remove or dedupe with layer? export const apply = register('apply', function (func, pat) { return func(pat); }); /** * Plays the pattern at the given cycles per minute. + * @tags temporal * @deprecated * @example * s(",hh*2").cpm(90) // = 90 bpm @@ -2051,6 +2211,7 @@ export const cpm = register('cpm', function (cpm, pat) { /** * Nudge a pattern to start earlier in time. Equivalent of Tidal's <~ operator * + * @tags temporal * @name early * @memberof Pattern * @param {number | Pattern} cycles number of cycles to nudge left @@ -2071,6 +2232,7 @@ export const early = register( /** * Nudge a pattern to start later in time. Equivalent of Tidal's ~> operator * + * @tags temporal * @name late * @memberof Pattern * @param {number | Pattern} cycles number of cycles to nudge right @@ -2091,6 +2253,7 @@ export const late = register( /** * Plays a portion of a pattern, specified by the beginning and end of a time span. The new resulting pattern is played over the time period of the original pattern: * + * @tags temporal * @example * s("bd*2 hh*3 [sd bd]*2 perc").zoom(0.25, 0.75) * // s("hh*3 [sd bd]*2") // equivalent @@ -2117,6 +2280,7 @@ export const { zoomArc, zoomarc } = register(['zoomArc', 'zoomarc'], function (a /** * Splits a pattern into the given number of slices, and plays them according to a pattern of slice numbers. * Similar to `slice`, but slices up patterns rather than sound samples. + * @tags temporal * @param {number} number of slices * @param {number} slices to play * @example @@ -2144,6 +2308,7 @@ export const bite = register( /** * Selects the given fraction of the pattern and repeats that part to fill the remainder of the cycle. + * @tags temporal * @param {number} fraction fraction to select * @example * s("lt ht mt cp, [hh oh]*2").linger("<1 .5 .25 .125>") @@ -2164,6 +2329,7 @@ export const linger = register( /** * Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one. + * @tags temporal * @name segment * @synonyms seg * @param {number} segments number of segments per cycle @@ -2176,6 +2342,7 @@ export const { segment, seg } = register(['segment', 'seg'], function (rate, pat /** * The function `swingBy x n` breaks each cycle into `n` slices, and then delays events in the second half of each slice by the amount `x`, which is relative to the size of the (half) slice. So if `x` is 0 it does nothing, `0.5` delays for half the note duration, and 1 will wrap around to doing nothing again. The end result is a shuffle or swing-like rhythm + * @tags temporal * @param {number} subdivision * @param {number} offset * @example @@ -2185,6 +2352,7 @@ export const swingBy = register('swingBy', (swing, n, pat) => pat.inside(n, late /** * Shorthand for swingBy with 1/3: + * @tags temporal * @param {number} subdivision * @example * s("hh*8").swing(4) @@ -2194,6 +2362,7 @@ export const swing = register('swing', (n, pat) => pat.swingBy(1 / 3, n)); /** * Swaps 1s and 0s in a binary pattern. + * @tags temporal * @name invert * @synonyms inv * @example @@ -2211,6 +2380,7 @@ export const { invert, inv } = register( /** * Applies the given function whenever the given pattern is in a true state. + * @tags temporal * @name when * @memberof Pattern * @param {Pattern} binary_pat @@ -2225,6 +2395,7 @@ export const when = register('when', function (on, func, pat) { /** * Superimposes the function result on top of the original pattern, delayed by the given time. + * @tags temporal * @name off * @memberof Pattern * @param {Pattern | number} time offset time @@ -2241,6 +2412,7 @@ export const off = register('off', function (time_pat, func, pat) { * Returns a new pattern where every other cycle is played once, twice as * fast, and offset in time by one quarter of a cycle. Creates a kind of * breakbeat feel. + * @tags temporal * @returns Pattern */ export const brak = register('brak', function (pat) { @@ -2250,6 +2422,7 @@ export const brak = register('brak', function (pat) { /** * Reverse all cycles in a pattern. See also `revv` for reversing a whole pattern. * + * @tags temporal * @name rev * @memberof Pattern * @returns Pattern @@ -2284,6 +2457,7 @@ export const rev = register( * Reverse a whole pattern. See also `rev` for reversing each cycle. * * @name revv + * @tags temporal * @memberof Pattern * @returns Pattern * @example @@ -2300,6 +2474,7 @@ export const revv = register('revv', function (pat) { /** Like press, but allows you to specify the amount by which each * event is shifted. pressBy(0.5) is the same as press, while * pressBy(1/3) shifts each event by a third of its timespan. + * @tags temporal * @example * stack(s("hh*4"), * s("bd mt sd ht").pressBy("<0 0.5 0.25>") @@ -2311,6 +2486,7 @@ export const pressBy = register('pressBy', function (r, pat) { /** * Syncopates a rhythm, by shifting each event halfway into its timespan. + * @tags temporal * @example * stack(s("hh*4"), * s("bd mt sd ht").every(4, press) @@ -2322,6 +2498,7 @@ export const press = register('press', function (pat) { /** * Silences a pattern. + * @tags temporal * @example * stack( * s("bd").hush(), @@ -2334,6 +2511,7 @@ Pattern.prototype.hush = function () { /** * Applies `rev` to a pattern every other cycle, so that the pattern alternates between forwards and backwards. + * @tags temporal * @example * note("c d e g").palindrome() */ @@ -2348,6 +2526,7 @@ export const palindrome = register( /** * Jux with adjustable stereo width. 0 = mono, 1 = full stereo. + * @tags temporal * @name juxBy * @synonyms juxby * @example @@ -2369,6 +2548,7 @@ export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func, /** * The jux function creates strange stereo effects, by applying a function to a pattern, but only in the right-hand channel. + * @tags temporal, superdough * @example * s("bd lt [~ ht] mt cp ~ bd hh").jux(rev) * @example @@ -2382,6 +2562,7 @@ export const jux = register('jux', function (func, pat) { /** * Superimpose and offset multiple times, applying the given function each time. + * @tags temporal, functional * @name echoWith * @synonyms echowith, stutWith, stutwith * @param {number} times how many times to repeat @@ -2401,6 +2582,7 @@ export const { echoWith, echowith, stutWith, stutwith } = register( /** * Superimpose and offset multiple times, gradually decreasing the velocity + * @tags temporal * @name echo * @memberof Pattern * @returns Pattern @@ -2416,6 +2598,7 @@ export const echo = register('echo', function (times, time, feedback, pat) { /** * Deprecated. Like echo, but the last 2 parameters are flipped. + * @tags temporal * @name stut * @param {number} times how many times to repeat * @param {number} feedback velocity multiplicator for each iteration @@ -2437,6 +2620,7 @@ export const applyN = register('applyN', function (n, func, p) { /** * The plyWith function repeats each event the given number of times, applying the given function to each event.\n + * @tags temporal * @name plyWith * @synonyms plywith * @param {number} factor how many times to repeat @@ -2459,6 +2643,7 @@ export const plyWith = register(['plyWith', 'plywith'], function (factor, func, /** * The plyForEach function repeats each event the given number of times, applying the given function to each event. * This version of ply uses the iteration index as an argument to the function, similar to echoWith. + * @tags temporal * @name plyForEach * @synonyms plyforeach * @param {number} factor how many times to repeat @@ -2480,6 +2665,7 @@ export const plyForEach = register(['plyForEach', 'plyforeach'], function (facto /** * Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played. + * @tags temporal * @name iter * @memberof Pattern * @returns Pattern @@ -2507,6 +2693,7 @@ export const iter = register( /** * Like `iter`, but plays the subdivisions in reverse order. Known as iter' in tidalcycles + * @tags temporal * @name iterBack * @synonyms iterback * @memberof Pattern @@ -2525,6 +2712,7 @@ export const { iterBack, iterback } = register( /** * Repeats each cycle the given number of times. + * @tags temporal * @name repeatCycles * @memberof Pattern * @returns Pattern @@ -2548,6 +2736,7 @@ export const { repeatCycles } = register( /** * Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle). + * @tags temporal, functional * @name chunk * @synonyms slowChunk, slowchunk * @memberof Pattern @@ -2579,6 +2768,7 @@ export const { chunk, slowchunk, slowChunk } = register( /** * Like `chunk`, but cycles through the parts in reverse order. Known as chunk' in tidalcycles + * @tags temporal * @name chunkBack * @synonyms chunkback * @memberof Pattern @@ -2599,6 +2789,7 @@ export const { chunkBack, chunkback } = register( /** * Like `chunk`, but the cycles of the source pattern aren't repeated * for each set of chunks. + * @tags temporal * @name fastChunk * @synonyms fastchunk * @memberof Pattern @@ -2619,6 +2810,7 @@ export const { fastchunk, fastChunk } = register( /** * Like `chunk`, but the function is applied to a looped subcycle of the source pattern. + * @tags temporal * @name chunkInto * @synonyms chunkinto * @memberof Pattern @@ -2632,6 +2824,7 @@ export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], fun /** * Like `chunkInto`, but moves backwards through the chunks. + * @tags temporal * @name chunkBackInto * @synonyms chunkbackinto * @memberof Pattern @@ -2662,6 +2855,7 @@ export const bypass = register( /** * Loops the pattern inside an `offset` for `cycles`. * If you think of the entire span of time in cycles as a ribbon, you can cut a single piece and loop it. + * @tags temporal * @name ribbon * @synonyms rib * @param {number} offset start point of loop in cycles @@ -2690,6 +2884,7 @@ export const hsl = register('hsl', (h, s, l, pat) => { /** * Tags each Hap with an identifier. Good for filtering. The function populates Hap.context.tags (Array). * @name tag + * @tags temporal * @param {string} tag anything unique * @example * s("saw!16").note("F1") @@ -2705,6 +2900,7 @@ Pattern.prototype.tag = function (tag) { /** * Filters haps using the given function * @name filter + * @tags temporal, functional * @param {Function} test function to test Hap * @example * s("hh!7 oh").filter(hap => hap.value.s === 'hh') @@ -2714,6 +2910,7 @@ export const filter = register('filter', (test, pat) => pat.withHaps((haps) => h /** * Filters haps by their begin time * @name filterWhen + * @tags temporal, functional * @param {Function} test function to test Hap.whole.begin * @example * oneCycle: s("bd*4").filterWhen((t) => t < 1) @@ -2723,6 +2920,7 @@ export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) = /** * Use within to apply a function to only a part of a pattern. * @name within + * @tags temporal, functional * @param {number} start start within cycle (0 - 1) * @param {number} end end within cycle (0 - 1). Must be > start * @param {Function} func function to be applied to the sub-pattern @@ -2797,6 +2995,7 @@ export function _match(span, hap_p) { * *Experimental* * * Speeds a pattern up or down, to fit to the given number of steps per cycle. + * @tags stepwise * @example * sound("bd sd cp").pace(4) * // The same as sound("{bd sd cp}%4") or sound("*4") @@ -2838,6 +3037,7 @@ export function _polymeterListSteps(steps, ...args) { * *Experimental* * * Aligns the steps of the patterns, creating polymeters. The patterns are repeated until they all fit the cycle. For example, in the below the first pattern is repeated twice, and the second is repeated three times, to fit the lowest common multiple of six steps. + * @tags stepwise * @synonyms pm * @example * // The same as note("{c eb g, c2 g2}%6") @@ -2870,6 +3070,7 @@ export function polymeter(...args) { * The steps can either be inferred from the pattern, or provided as a [length, pattern] pair. * Has the alias `timecat`. * @name stepcat + * @tags stepwise * @synonyms timeCat, timecat * @return {Pattern} * @example @@ -2927,6 +3128,7 @@ export function stepcat(...timepats) { * Concatenates patterns stepwise, according to an inferred 'steps per cycle'. * Similar to `stepcat`, but if an argument is a list, the whole pattern will alternate between the elements in the list. * + * @tags stepwise * @return {Pattern} * @example * stepalt(["bd cp", "mt"], "bd").sound() @@ -2953,6 +3155,7 @@ export function stepalt(...groups) { * * Takes the given number of steps from a pattern (dropping the rest). * A positive number will take steps from the start of a pattern, and a negative number from the end. + * @tags stepwise * @return {Pattern} * @example * "bd cp ht mt".take("2").sound() @@ -2997,6 +3200,7 @@ export const take = stepRegister('take', function (i, pat) { * * Drops the given number of steps from a pattern. * A positive number will drop steps from the start of a pattern, and a negative number from the end. + * @tags stepwise * @return {Pattern} * @example * "tha dhi thom nam".drop("1").sound().bank("mridangam") @@ -3025,6 +3229,7 @@ export const drop = stepRegister('drop', function (i, pat) { * `extend` is similar to `fast` in that it increases its density, but it also increases the step count * accordingly. So `stepcat("a b".extend(2), "c d")` would be the same as `"a b a b c d"`, whereas * `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`. + * @tags stepwise * @example * stepcat( * sound("bd bd - cp").extend(2), @@ -3043,6 +3248,7 @@ export const extend = stepRegister('extend', function (factor, pat) { * `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`. * * TODO: find out how this function differs from extend + * @tags stepwise * @example * stepcat( * sound("bd bd - cp").replicate(2), @@ -3057,6 +3263,7 @@ export const replicate = stepRegister('replicate', function (factor, pat) { * *Experimental* * * Expands the step size of the pattern by the given factor. + * @tags stepwise * @example * sound("tha dhi thom nam").bank("mridangam").expand("3 2 1 1 2 3").pace(8) */ @@ -3068,6 +3275,7 @@ export const expand = stepRegister('expand', function (factor, pat) { * *Experimental* * * Contracts the step size of the pattern by the given factor. See also `expand`. + * @tags stepwise * @example * sound("tha dhi thom nam").bank("mridangam").contract("3 2 1 1 2 3").pace(8) */ @@ -3122,6 +3330,7 @@ export const shrinklist = (amount, pat) => pat.shrinklist(amount); * Progressively shrinks the pattern by 'n' steps until there's nothing left, or if a second value is given (using mininotation list syntax with `:`), * that number of times. * A positive number will progressively drop steps from the start of a pattern, and a negative number from the end. + * @tags stepwise * @return {Pattern} * @example * "tha dhi thom nam".shrink("1").sound() @@ -3161,6 +3370,7 @@ export const shrink = register( * Progressively grows the pattern by 'n' steps until the full pattern is played, or if a second value is given (using mininotation list syntax with `:`), * that number of times. * A positive number will progressively grow steps from the start of a pattern, and a negative number from the end. + * @tags stepwise * @return {Pattern} * @example * "tha dhi thom nam".grow("1").sound() @@ -3200,7 +3410,8 @@ export const grow = register( * Inserts a pattern into a list of patterns. On the first repetition it will be inserted at the end of the list, then moved backwards through the list * on successive repetitions. The patterns are added together stepwise, with all repetitions taking place over a single cycle. Using `pace` to set the * number of steps per cycle is therefore usually recommended. - * + * + * @tags stepwise * @return {Pattern} * @example * "[c g]".tour("e f", "e f g", "g f e c").note() @@ -3227,6 +3438,7 @@ Pattern.prototype.tour = function (...many) { * 'zips' together the steps of the provided patterns. This can create a long repetition, taking place over a single, dense cycle. * Using `pace` to set the number of steps per cycle is therefore usually recommended. * + * @tags stepwise * @returns {Pattern} * @example * zip("e f", "e f g", "g [f e] a f4 c").note() @@ -3278,6 +3490,7 @@ Pattern.prototype.steps = Pattern.prototype.pace; * Cuts each sample into the given number of parts, allowing you to explore a technique known as 'granular synthesis'. * It turns a pattern of samples into a pattern of parts of samples. * @name chop + * @tags samples * @memberof Pattern * @returns Pattern * @example @@ -3308,6 +3521,7 @@ export const chop = register('chop', function (n, pat) { /** * Cuts each sample into the given number of parts, triggering progressive portions of each sample at each loop. * @name striate + * @tags samples * @memberof Pattern * @returns Pattern * @example @@ -3326,13 +3540,13 @@ export const striate = register('striate', function (n, pat) { /** * Makes the sample fit the given number of cycles by changing the speed. * @name loopAt + * @tags samples, pitch * @memberof Pattern * @returns Pattern * @example * samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) * s("rhodes").loopAt(2) */ -// TODO - global cps clock const _loopAt = function (factor, pat, cps = 0.5) { return pat .speed((1 / factor) * cps) @@ -3340,10 +3554,16 @@ const _loopAt = function (factor, pat, cps = 0.5) { .slow(factor); }; +export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) { + const steps = pat._steps ? pat._steps.div(factor) : undefined; + return new Pattern((state) => _loopAt(factor, pat, state.controls._cps).query(state), steps); +}); + /** * Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers. * Instead of a number, it also accepts a list of numbers from 0 to 1 to slice at specific points. * @name slice + * @tags samples * @memberof Pattern * @returns Pattern * @example @@ -3379,6 +3599,7 @@ export const slice = register( * make something happen on event time * uses browser timeout which is innacurate for audio tasks * @name onTriggerTime + * @tags external_io * @memberof Pattern * @returns Pattern * @example @@ -3396,6 +3617,7 @@ Pattern.prototype.onTriggerTime = function (func) { /** * Works the same as slice, but changes the playback speed of each slice to match the duration of its step. * @name splice + * @tags samples, pitch * @example * samples('github:tidalcycles/dirt-samples') * s("breaks165") @@ -3424,15 +3646,11 @@ export const splice = register( false, // turns off auto-patternification ); -export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) { - const steps = pat._steps ? pat._steps.div(factor) : undefined; - return new Pattern((state) => _loopAt(factor, pat, state.controls._cps).query(state), steps); -}); - /** * Makes the sample fit its event duration. Good for rhythmical loops like drum breaks. * Similar to `loopAt`. * @name fit + * @tags samples, pitch * @example * samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) * s("rhodes/2").fit() @@ -3454,22 +3672,23 @@ export const fit = register('fit', (pat) => /** * Makes the sample fit the given number of cycles and cps value, by - * changing the speed. Please note that at some point cps will be - * given by a global clock and this function will be - * deprecated/removed. + * changing the speed. deprecated: use loopAt or fit instead, together with setCps / setCpm. * @name loopAtCps + * @tags samples, pitch * @memberof Pattern + * @deprecated * @returns Pattern * @example * samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) * s("rhodes").loopAtCps(4,1.5).cps(1.5) */ -// TODO - global cps clock export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) { return _loopAt(factor, pat, cps); }); -/** exposes a custom value at query time. basically allows mutating state without evaluation */ +/** exposes a custom value at query time. basically allows mutating state without evaluation + * @tags internals + */ export const ref = (accessor) => pure(1) .withValue(() => reify(accessor())) @@ -3484,6 +3703,7 @@ let fadeGain = (p) => (p < 0.5 ? 1 : 1 - (p - 0.5) / 0.5); * - 1 = (no left, full right) * * @name xfade + * @tags amplitude * @example * xfade(s("bd*2"), "<0 .25 .5 .75 1>", s("hh*8")) */ @@ -3505,6 +3725,7 @@ Pattern.prototype.xfade = function (pos, b) { * creates a structure pattern from divisions of a cycle * especially useful for creating rhythms * @name beat + * @tags temporal * @example * s("bd").beat("0,7,10", 16) * @example @@ -3581,6 +3802,7 @@ export const _morph = (from, to, by) => { * sine.slow(8) // slowly morph between the rhythms * ) * ) + * @tags temporal */ export const morph = (frompat, topat, bypat) => { frompat = reify(frompat); @@ -3607,6 +3829,7 @@ const _distortWithAlg = function (name) { * Soft-clipping distortion * * @name soft + * @tags distortion, superdough * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion * @@ -3617,6 +3840,7 @@ export const soft = _distortWithAlg('soft'); * Hard-clipping distortion * * @name hard + * @tags distortion, superdough * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion * @@ -3627,6 +3851,7 @@ export const hard = _distortWithAlg('hard'); * Cubic polynomial distortion * * @name cubic + * @tags distortion, superdough * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion * @@ -3637,6 +3862,7 @@ export const cubic = _distortWithAlg('cubic'); * Diode-emulating distortion * * @name diode + * @tags distortion, superdough * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion * @@ -3647,6 +3873,7 @@ export const diode = _distortWithAlg('diode'); * Asymmetrical diode distortion * * @name asym + * @tags distortion, superdough * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion * @@ -3657,6 +3884,7 @@ export const asym = _distortWithAlg('asym'); * Wavefolding distortion * * @name fold + * @tags distortion, superdough * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion * @@ -3667,6 +3895,7 @@ export const fold = _distortWithAlg('fold'); * Wavefolding distortion composed with sinusoid * * @name sinefold + * @tags distortion, superdough * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion * @@ -3677,6 +3906,7 @@ export const sinefold = _distortWithAlg('sinefold'); * Distortion via Chebyshev polynomials * * @name chebyshev + * @tags distortion, superdough * @param {number | Pattern} distortion amount of distortion to apply * @param {number | Pattern} volume linear postgain of the distortion * @@ -3687,6 +3917,7 @@ export const chebyshev = _distortWithAlg('chebyshev'); * Turns a list of patterns into a single pattern which outputs list-values * * @name parray + * @tags combiners * @returns Pattern */ export const parray = (pats) => { @@ -3709,6 +3940,7 @@ const _ensureListPattern = (list) => { * Can also be used to create a new synth via `s('user').partials(...)` * * @name partials + * @tags superdough * @param {number[] | Pattern} magnitudes List of [0, 1] magnitudes for partials. 0th entry is the fundamental harmonic (i.e. DC offset is skipped) * @example * s("user").seg(16).n(irand(8)).scale("A:major") @@ -3730,6 +3962,7 @@ export const partials = (list) => { * Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases * * @name phases + * @tags superdough * @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the fundamental phase (i.e. DC offset is skipped) * @example * // Phase cancellation @@ -3751,6 +3984,7 @@ export const phases = (list) => { * calls and/or in a single .FX(fx1, fx2, ..) call. The fx1, .. are _patterns_ which * establish the controls of the given effect. See examples. * @name FX + * @tags superdough * @memberof Pattern * @returns Pattern * @example @@ -3801,6 +4035,7 @@ const _asArrayPattern = (pats) => { * by wrapping them inside a function in K (see example). * * @name K + * @tags generators, superdough * @param {KabelsalatExpression | Function} expr Kabelsalat graph definition * @memberof Pattern * @returns Pattern diff --git a/packages/core/pick.mjs b/packages/core/pick.mjs index 206fa619..3974c695 100644 --- a/packages/core/pick.mjs +++ b/packages/core/pick.mjs @@ -28,6 +28,7 @@ const _pick = function (lookup, pat, modulo = true) { /** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name). * Similar to `inhabit`, but maintains the structure of the original patterns. + * @tags combiners * @param {Pattern} pat * @param {*} xs * @returns {Pattern} @@ -57,6 +58,7 @@ const __pick = register('pick', function (lookup, pat) { * it wraps around, rather than sticking at the maximum value. * For example, if you pick the fifth pattern of a list of three, you'll get the * second one. + * @tags combiners * @param {Pattern} pat * @param {*} xs * @returns {Pattern} @@ -67,32 +69,38 @@ export const pickmod = register('pickmod', function (lookup, pat) { }); /** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern. + * @tags combiners, functional * @param {Pattern} pat - * @param {Pattern} lookup a pattern of indices - * @param {function[]} funcs the array of functions from which to pull + * @param {Pattern} lookup a pattern of indices or names + * @param {function[] | object} lookup the array or lookup object of functions from which to pull * @returns {Pattern} * @example * s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)]) * @example * note("(3,8)").s("square") - * .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)]) + * .pickF("<0 2> 1", [jux(rev), fast(2), x=>x.lpf(800)]) + * @example + * note("(3,8)").s("square") + * .pickF(" f", { jr:jux(rev), f:fast(2), l:x=>x.lpf(800) }) */ -export const pickF = register('pickF', function (lookup, funcs, pat) { - return pat.apply(pick(lookup, funcs)); +export const pickF = register('pickF', function (pickPattern, lookup, pat) { + return pat.apply(pick(lookup, pickPattern)); }); /** * The same as `pickF`, but if you pick a number greater than the size of the functions list, * it wraps around, rather than sticking at the maximum value. + * @tags combiners * @param {Pattern} pat - * @param {Pattern} lookup a pattern of indices - * @param {function[]} funcs the array of functions from which to pull + * @param {Pattern} lookup a pattern of indices or names + * @param {function[] | object} lookup the array or lookup object of functions from which to pull * @returns {Pattern} */ -export const pickmodF = register('pickmodF', function (lookup, funcs, pat) { - return pat.apply(pickmod(lookup, funcs)); +export const pickmodF = register('pickmodF', function (pickPattern, lookup, pat) { + return pat.apply(pickmod(lookup, pickPattern)); }); /** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin. + * @tags combiners * @param {Pattern} pat * @param {*} xs * @returns {Pattern} @@ -103,6 +111,7 @@ export const pickOut = register('pickOut', function (lookup, pat) { /** * The same as `pickOut`, but if you pick a number greater than the size of the list, * it wraps around, rather than sticking at the maximum value. + * @tags combiners * @param {Pattern} pat * @param {*} xs * @returns {Pattern} @@ -112,6 +121,7 @@ export const pickmodOut = register('pickmodOut', function (lookup, pat) { }); /** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered. + * @tags combiners * @param {Pattern} pat * @param {*} xs * @returns {Pattern} @@ -122,6 +132,7 @@ export const pickRestart = register('pickRestart', function (lookup, pat) { /** * The same as `pickRestart`, but if you pick a number greater than the size of the list, * it wraps around, rather than sticking at the maximum value. + * @tags combiners * @param {Pattern} pat * @param {*} xs * @returns {Pattern} @@ -138,6 +149,7 @@ export const pickmodRestart = register('pickmodRestart', function (lookup, pat) }); /** * Similar to `pick`, but the choosen pattern is reset when its index is triggered. + * @tags combiners * @param {Pattern} pat * @param {*} xs * @returns {Pattern} @@ -148,6 +160,7 @@ export const pickReset = register('pickReset', function (lookup, pat) { /** * The same as `pickReset`, but if you pick a number greater than the size of the list, * it wraps around, rather than sticking at the maximum value. + * @tags combiners * @param {Pattern} pat * @param {*} xs * @returns {Pattern} @@ -157,19 +170,22 @@ export const pickmodReset = register('pickmodReset', function (lookup, pat) { }); /** Picks patterns (or plain values) either from a list (by index) or a lookup table (by name). - * Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern. - * @name inhabit - * @synonyms pickSqueeze - * @param {Pattern} pat - * @param {*} xs - * @returns {Pattern} - * @example - * "".inhabit({a: s("bd(3,8)"), - b: s("cp sd") - }) - * @example - * s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4) - */ + * Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern. + * @name inhabit + * @tags combiners + * @synonyms pickSqueeze + * @param {Pattern} pat + * @param {*} xs + * @returns {Pattern} + * @example + * let a = s("bd(3,8)") + * let b = s("cp sd") + * "".inhabit({ a, b }) + * @example + * s("a@2 [a b] a" + * .inhabit({a: "bd(3,8)", b: "sd sd"})) + * .slow(4) + */ export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) { return _pick(lookup, pat, false).squeezeJoin(); }); @@ -180,6 +196,7 @@ export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], fun * second one. * @name inhabitmod * @synonyms pickmodSqueeze + * @tags combiners * @param {Pattern} pat * @param {*} xs * @returns {Pattern} @@ -192,6 +209,7 @@ export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSq /** * Pick from the list of values (or patterns of values) via the index using the given * pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event + * @tags combiners * @param {Pattern} pat * @param {*} xs * @returns {Pattern} diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index ec6a0d98..c1b53981 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -12,7 +12,6 @@ import { import { evalScope } from './evaluate.mjs'; import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; import { reset_state } from './impure.mjs'; -import { SalatRepl } from '@kabelsalat/web'; export function repl({ defaultOutput, @@ -31,7 +30,6 @@ export function repl({ id, mondo = false, }) { - const kabel = new SalatRepl({ localScope: true }); const state = { schedulerError: undefined, evalError: undefined, @@ -40,6 +38,7 @@ export function repl({ pattern: undefined, miniLocations: [], widgets: [], + sliders: [], pending: false, started: false, }; @@ -81,19 +80,135 @@ 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; }; - const compileKabel = (code) => { - const node = kabel.evaluate(code); - return node.compile({ log: false }); - }; - // helper to get a patternified pure value out function unpure(pat) { if (pat._Pattern) { @@ -110,7 +225,60 @@ export function repl({ }; setTime(() => scheduler.now()); // TODO: refactor? - const stop = () => scheduler.stop(); + // 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 start = () => scheduler.start(); const pause = () => scheduler.pause(); const toggle = () => scheduler.toggle(); @@ -123,6 +291,7 @@ export function repl({ * Changes the global tempo to the given cycles per minute * * @name setcpm + * @tags temporal * @alias setCpm * @param {number} cpm cycles per minute * @example @@ -136,7 +305,9 @@ export function repl({ // TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`.. - /** Applies a function to all the running patterns. Note that the patterns are groups together into a single `stack` before the function is applied. This is probably what you want, but see `each` for + let allTransforms = []; + /** + * Applies a function to all the running patterns. Note that the patterns are groups together into a single `stack` before the function is applied. This is probably what you want, but see `each` for * a version that applies the function to each pattern separately. * ``` * $: sound("bd - cp sd") @@ -148,18 +319,21 @@ export function repl({ * $: sound("hh*8") * all(x => x.pianoroll()) * ``` + * + * @tags combiners */ - let allTransforms = []; const all = function (transform) { allTransforms.push(transform); return silence; }; /** Applies a function to each of the running patterns separately. This is intended for future use with upcoming 'stepwise' features. See `all` for a version that applies the function to all the patterns stacked together into a single pattern. + * * ``` * $: sound("bd - cp sd") * $: sound("hh*8") * each(fast("<2 3>")) * ``` + * @tags combiners */ const each = function (transform) { eachTransform = transform; @@ -215,11 +389,10 @@ export function repl({ setcps: setCps, setCpm, setcpm: setCpm, - compileKabel, }); }; - const evaluate = async (code, autostart = true, shouldHush = true) => { + const evaluate = async (code, autostart = true) => { if (!code) { throw new Error('no code to evaluate'); } @@ -227,59 +400,34 @@ export function repl({ updateState({ code, pending: true }); await injectPatternMethods(); setTime(() => scheduler.now()); // TODO: refactor? - await beforeEval?.({ code }); + await beforeEval?.({ code, blockBased: false }); allTransforms = []; // reset all transforms - shouldHush && hush(); + + codeBlocks = {}; + hush(); if (mondo) { 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); - } - } - if (!isPattern(pattern)) { - pattern = silence; - } + 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 }); + + afterEval?.({ code, pattern, meta, range: undefined, widgetRemoved: false }); return pattern; } catch (err) { logger(`[eval] error: ${err.message}`, 'error'); @@ -288,8 +436,121 @@ export function repl({ 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; + } + } + } + + pPatterns = Object.fromEntries( + Object.entries(pPatterns).filter(([key]) => { + return Object.keys(codeBlocks).includes(key); + }), + ); + + pattern = applyPatternTransforms(pattern); + + logger(`[eval] code updated`); + pattern = await setPattern(pattern, autostart); + 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 }); + return pattern; + } catch (err) { + logger(`[eval] error: ${err.message}`, 'error'); + console.error(err); + updateState({ evalError: err, pending: false }); + onEvalError?.(err); + } + }; + const setCode = (code) => updateState({ code }); - return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state }; + return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state }; } export const getTrigger = diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index c9ed2166..4398e2ab 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -24,6 +24,7 @@ export const signal = (func) => { * A sawtooth signal between 0 and 1. * * @return {Pattern} + * @tags generators * @example * note("*8") * .clip(saw.slow(2)) @@ -38,6 +39,7 @@ export const saw = signal((t) => t % 1); * A sawtooth signal between -1 and 1 (like `saw`, but bipolar). * * @return {Pattern} + * @tags generators */ export const saw2 = saw.toBipolar(); @@ -45,6 +47,7 @@ export const saw2 = saw.toBipolar(); * A sawtooth signal between 1 and 0 (like `saw`, but flipped). * * @return {Pattern} + * @tags generators * @example * note("*8") * .clip(isaw.slow(2)) @@ -59,6 +62,7 @@ export const isaw = signal((t) => 1 - (t % 1)); * A sawtooth signal between 1 and -1 (like `saw2`, but flipped). * * @return {Pattern} + * @tags generators */ export const isaw2 = isaw.toBipolar(); @@ -66,12 +70,14 @@ export const isaw2 = isaw.toBipolar(); * A sine signal between -1 and 1 (like `sine`, but bipolar). * * @return {Pattern} + * @tags generators */ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t)); /** * A sine signal between 0 and 1. * @return {Pattern} + * @tags generators * @example * n(sine.segment(16).range(0,15)) * .scale("C:minor") @@ -83,6 +89,7 @@ export const sine = sine2.fromBipolar(); * A cosine signal between 0 and 1. * * @return {Pattern} + * @tags generators * @example * n(stack(sine,cosine).segment(16).range(0,15)) * .scale("C:minor") @@ -94,12 +101,14 @@ export const cosine = sine._early(Fraction(1).div(4)); * A cosine signal between -1 and 1 (like `cosine`, but bipolar). * * @return {Pattern} + * @tags generators */ export const cosine2 = sine2._early(Fraction(1).div(4)); /** * A square signal between 0 and 1. * @return {Pattern} + * @tags generators * @example * n(square.segment(4).range(0,7)).scale("C:minor") * @@ -110,6 +119,7 @@ export const square = signal((t) => Math.floor((t * 2) % 2)); * A square signal between -1 and 1 (like `square`, but bipolar). * * @return {Pattern} + * @tags generators */ export const square2 = square.toBipolar(); @@ -117,6 +127,7 @@ export const square2 = square.toBipolar(); * A triangle signal between 0 and 1. * * @return {Pattern} + * @tags generators * @example * n(tri.segment(8).range(0,7)).scale("C:minor") * @@ -127,6 +138,7 @@ export const tri = fastcat(saw, isaw); * A triangle signal between -1 and 1 (like `tri`, but bipolar). * * @return {Pattern} + * @tags generators */ export const tri2 = fastcat(saw2, isaw2); @@ -134,6 +146,7 @@ export const tri2 = fastcat(saw2, isaw2); * An inverted triangle signal between 1 and 0 (like `tri`, but flipped). * * @return {Pattern} + * @tags generators * @example * n(itri.segment(8).range(0,7)).scale("C:minor") * @@ -144,6 +157,7 @@ export const itri = fastcat(isaw, saw); * An inverted triangle signal between -1 and 1 (like `itri`, but bipolar). * * @return {Pattern} + * @tags generators */ export const itri2 = fastcat(isaw2, saw2); @@ -151,6 +165,7 @@ export const itri2 = fastcat(isaw2, saw2); * A signal representing the cycle time. * * @return {Pattern} + * @tags generators */ export const time = signal(id); @@ -158,6 +173,7 @@ export const time = signal(id); * The mouse's x position value ranges from 0 to 1. * @name mousex * @return {Pattern} + * @tags external_io * @example * n(mousex.segment(4).range(0,7)).scale("C:minor") * @@ -167,6 +183,7 @@ export const time = signal(id); * The mouse's y position value ranges from 0 to 1. * @name mousey * @return {Pattern} + * @tags external_io * @example * n(mousey.segment(4).range(0,7)).scale("C:minor") * @@ -269,6 +286,7 @@ export const getRandsAtTime = (t, n = 1, seed = 0) => { * precise RNG, try `useRNG('precise')`. * * @name useRNG + * @tags generators, math * @param {string} mod - Mode. One of 'legacy', 'precise' * @example * useRNG('legacy') @@ -280,6 +298,7 @@ export const useRNG = (mode = 'legacy') => (RNG_MODE = mode); /** * A discrete pattern of numbers from 0 to n-1 + * @tags generators * @example * n(run(4)).scale("C4:pentatonic") * // n("0 1 2 3").scale("C4:pentatonic") @@ -290,13 +309,14 @@ export const run = (n) => saw.range(0, n).round().segment(n); * Creates a binary pattern from a number. * * @name binary + * @tags generators * @param {number} n - input number to convert to binary * @example * "hh".s().struct(binary(5)) * // "hh".s().struct("1 0 1") */ export const binary = (n) => { - const nBits = reify(n).log2(0).floor().add(1); + const nBits = reify(n).log2().floor().add(1); return binaryN(n, nBits); }; @@ -304,6 +324,7 @@ export const binary = (n) => { * Creates a binary pattern from a number, padded to n bits long. * * @name binaryN + * @tags generators * @param {number} n - input number to convert to binary * @param {number} nBits - pattern length, defaults to 16 * @example @@ -321,12 +342,13 @@ export const binaryN = (n, nBits = 16) => { * Creates a binary list pattern from a number. * * @name binaryL + * @tags generators * @param {number} n - input number to convert to binary * s("saw").seg(8) * .partials(binaryL(irand(4096).add(1))) */ export const binaryL = (n) => { - const nBits = reify(n).log2(0).floor().add(1); + const nBits = reify(n).log2().floor().add(1); return binaryNL(n, nBits); }; @@ -334,6 +356,7 @@ export const binaryL = (n) => { * Creates a binary list pattern from a number, padded to n bits long. * * @name binaryNL + * @tags generators * @param {number} n - input number to convert to binary * @param {number} nBits - pattern length, defaults to 16 */ @@ -353,6 +376,7 @@ export const binaryNL = (n, nBits = 16) => { * Creates a list of random numbers of the given length * * @name randL + * @tags generators * @param {number} n Number of random numbers to sample * @example * s("saw").seg(16).n(irand(12)).scale("F1:minor") @@ -365,7 +389,9 @@ export const randL = (n) => { export const randrun = (n) => { return signal((t, controls) => { // Without adding 0.5, the first cycle is always 0,1,2,3,... - const rands = getRandsAtTime(t.floor().add(0.5), n, controls.randSeed); + let rands = getRandsAtTime(t.floor().add(0.5), n, controls.randSeed); + // Support n = 1 + if (!Array.isArray(rands)) rands = [rands]; const nums = rands .map((n, i) => [n, i]) .sort((a, b) => (a[0] > b[0]) - (a[0] < b[0])) @@ -384,6 +410,7 @@ const _rearrangeWith = (ipat, n, pat) => { * Slices a pattern into the given number of parts, then plays those parts in random order. * Each part will be played exactly once per cycle. * @name shuffle + * @tags temporal * @example * note("c d e f").sound("piano").shuffle(4) * @example @@ -397,6 +424,7 @@ export const shuffle = register('shuffle', (n, pat) => { * Slices a pattern into the given number of parts, then plays those parts at random. Similar to `shuffle`, * but parts might be played more than once, or not at all, per cycle. * @name scramble + * @tags temporal * @example * note("c d e f").sound("piano").scramble(4) * @example @@ -409,6 +437,7 @@ export const scramble = register('scramble', (n, pat) => { /** * Modify a pattern by applying a function to the `randomSeed` control if present * + * @tags math * @param {Function} func Function from seed (or undefined) to seed (or undefined) * @param {Pattern} pat Pattern to update * @returns Pattern @@ -428,6 +457,7 @@ export const withSeed = (func, pat) => { * that use randomness, like `shuffle` and `sometimes`. * * @name seed + * @tags math * @param {number} n A new seed. Can be any number. * @example * $: s("hh*4").degrade(); @@ -441,6 +471,7 @@ export const seed = register('seed', (n, pat) => { * A continuous pattern of random numbers, between 0 and 1. * * @name rand + * @tags generators * @example * // randomly change the cutoff * s("bd*4,hh*8").cutoff(rand.range(500,8000)) @@ -449,6 +480,7 @@ export const seed = register('seed', (n, pat) => { export const rand = signal((t, controls) => getRandsAtTime(t, 1, controls.randSeed)); /** * A continuous pattern of random numbers, between -1 and 1 + * @tags generators */ export const rand2 = rand.toBipolar(); @@ -458,6 +490,7 @@ export const _brandBy = (p) => rand.fmap((x) => x < p); * A continuous pattern of 0 or 1 (binary random), with a probability for the value being 1 * * @name brandBy + * @tags generators * @param {number} probability - a number between 0 and 1 * @example * s("hh*10").pan(brandBy(0.2)) @@ -468,6 +501,7 @@ export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin(); * A continuous pattern of 0 or 1 (binary random) * * @name brand + * @tags generators * @example * s("hh*10").pan(brand) */ @@ -479,6 +513,7 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i)); * A continuous pattern of random integers, between 0 and n-1. * * @name irand + * @tags generators * @param {number} n max value (exclusive) * @example * // randomly select scale notes from 0 - 7 (= C to C) @@ -501,6 +536,7 @@ export const __chooseWith = (pat, xs) => { /** * Choose from the list of values (or patterns of values) using the given * pattern of numbers, which should be in the range of 0..1 + * @tags temporal * @param {Pattern} pat * @param {*} xs * @returns {Pattern} @@ -514,6 +550,7 @@ export const chooseWith = (pat, xs) => { /** * As with {chooseWith}, but the structure comes from the chosen values, rather * than the pattern you're using to choose with. + * @tags temporal * @param {Pattern} pat * @param {*} xs * @returns {Pattern} @@ -524,6 +561,7 @@ export const chooseInWith = (pat, xs) => { /** * Chooses randomly from the given list of elements. + * @tags temporal * @param {...any} xs values / patterns to choose from. * @returns {Pattern} - a continuous pattern. * @example @@ -539,6 +577,7 @@ export const chooseOut = choose; * Chooses from the given list of values (or patterns of values), according * to the pattern that the method is called on. The pattern should be in * the range 0 .. 1. + * @tags temporal * @param {...any} xs * @returns {Pattern} */ @@ -549,6 +588,7 @@ Pattern.prototype.choose = function (...xs) { /** * As with choose, but the pattern that this method is called on should be * in the range -1 .. 1 + * @tags temporal * @param {...any} xs * @returns {Pattern} */ @@ -558,6 +598,7 @@ Pattern.prototype.choose2 = function (...xs) { /** * Picks one of the elements at random each cycle. + * @tags temporal * @synonyms randcat * @returns {Pattern} * @example @@ -600,6 +641,7 @@ const wchooseWith = (...args) => _wchooseWith(...args).outerJoin(); /** * Chooses randomly from the given list of elements by giving a probability to each element + * @tags temporal * @param {...any} pairs arrays of value and weight * @returns {Pattern} - a continuous pattern. * @example @@ -609,6 +651,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs); /** * Picks one of the elements at random each cycle by giving a probability to each element + * @tags temporal * @synonyms wrandcat * @returns {Pattern} * @example @@ -652,6 +695,7 @@ function _berlin(t, seed = 0) { /** * Generates a continuous pattern of [perlin noise](https://en.wikipedia.org/wiki/Perlin_noise), in the range 0..1. * + * @tags generators * @name perlin * @example * // randomly change the cutoff @@ -664,6 +708,7 @@ export const perlin = signal((t, controls) => _perlin(t, controls.randSeed)); * Generates a continuous pattern of [berlin noise](conceived by Jame Coyne and Jade Rowland as a joke but turned out to be surprisingly cool and useful, * like perlin noise but with sawtooth waves), in the range 0..1. * + * @tags generators * @name berlin * @example * // ascending arpeggios @@ -684,6 +729,7 @@ export const degradeByWith = register( * 0 = 0% chance of removal * 1 = 100% chance of removal * + * @tags temporal * @name degradeBy * @memberof Pattern * @param {number} amount - a number between 0 and 1 @@ -709,6 +755,7 @@ export const degradeBy = register( * * Randomly removes 50% of events from the pattern. Shorthand for `.degradeBy(0.5)` * + * @tags temporal * @name degrade * @memberof Pattern * @returns Pattern @@ -725,6 +772,7 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5), true, t * 1 = 0% chance of removal * Events that would be removed by degradeBy are let through by undegradeBy and vice versa (see second example). * + * @tags temporal * @name undegradeBy * @memberof Pattern * @param {number} amount - a number between 0 and 1 @@ -753,6 +801,7 @@ export const undegradeBy = register( * Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)` * Events that would be removed by degrade are let through by undegrade and vice versa (see second example). * + * @tags temporal * @name undegrade * @memberof Pattern * @returns Pattern @@ -771,6 +820,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5), t * Randomly applies the given function by the given probability. * Similar to `someCyclesBy` * + * @tags temporal * @name sometimesBy * @memberof Pattern * @param {number | Pattern} probability - a number between 0 and 1 @@ -790,6 +840,7 @@ export const sometimesBy = register('sometimesBy', function (patx, func, pat) { * * Applies the given function with a 50% chance * + * @tags temporal * @name sometimes * @memberof Pattern * @param {function} function - the transformation to apply @@ -811,6 +862,7 @@ export const sometimes = register('sometimes', function (func, pat) { * @param {number | Pattern} probability - a number between 0 and 1 * @param {function} function - the transformation to apply * @returns Pattern + * @tags temporal * @example * s("bd,hh*8").someCyclesBy(.3, x=>x.speed("0.5")) */ @@ -833,6 +885,7 @@ export const someCyclesBy = register('someCyclesBy', function (patx, func, pat) * @name someCycles * @memberof Pattern * @returns Pattern + * @tags temporal * @example * s("bd,hh*8").someCycles(x=>x.speed("0.5")) */ @@ -847,6 +900,7 @@ export const someCycles = register('someCycles', function (func, pat) { * @name often * @memberof Pattern * @returns Pattern + * @tags temporal * @example * s("hh*8").often(x=>x.speed("0.5")) */ @@ -861,6 +915,7 @@ export const often = register('often', function (func, pat) { * @name rarely * @memberof Pattern * @returns Pattern + * @tags temporal * @example * s("hh*8").rarely(x=>x.speed("0.5")) */ @@ -872,6 +927,7 @@ export const rarely = register('rarely', function (func, pat) { * * Shorthand for `.sometimesBy(0.1, fn)` * + * @tags temporal * @name almostNever * @memberof Pattern * @returns Pattern @@ -886,6 +942,7 @@ export const almostNever = register('almostNever', function (func, pat) { * * Shorthand for `.sometimesBy(0.9, fn)` * + * @tags temporal * @name almostAlways * @memberof Pattern * @returns Pattern @@ -900,6 +957,7 @@ export const almostAlways = register('almostAlways', function (func, pat) { * * Shorthand for `.sometimesBy(0, fn)` (never calls fn) * + * @tags temporal * @name never * @memberof Pattern * @returns Pattern @@ -914,6 +972,7 @@ export const never = register('never', function (_, pat) { * * Shorthand for `.sometimesBy(1, fn)` (always calls fn) * + * @tags temporal * @name always * @memberof Pattern * @returns Pattern @@ -942,6 +1001,7 @@ export function _keyDown(keyname) { * Do something on a keypress, or array of keypresses * [Key name reference](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values) * + * @tags external_io * @name whenKey * @memberof Pattern * @returns Pattern @@ -958,6 +1018,7 @@ export const whenKey = register('whenKey', function (input, func, pat) { * returns true when a key or array of keys is held * [Key name reference](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values) * + * @tags external_io * @name keyDown * @memberof Pattern * @returns Pattern @@ -975,6 +1036,8 @@ export const keyDown = register('keyDown', function (pat) { * event durations, from the pattern that it is combined with. * For example `cyclesPer.struct("1 1 [1 1] 1")` would give the same as `"0.25 0.25 [0.125 0.125] 0.25"`. * See also its reciprocal, `per`, also known as `perCycle`. + * + * @tags temporal * @example * // Shorter events are lower in pitch * sound("saw saw [saw saw] saw") @@ -993,6 +1056,7 @@ export const cyclesPer = new Pattern(function (state) { * event durations, from the pattern that it is combined with. * For example `per.struct("1 1 [1 1] 1")` would give the same as `"4 4 [8 8] 4"`. * See also its reciprocal, `cyclesPer`. + * @tags temporal * @synonyms perCycle * @example * // Shorter events are more distorted @@ -1010,6 +1074,7 @@ export const perCycle = per; * particular, where the event duration halves, the * returned value increases by one. `perx.struct("1 1 [1 [1 1]] 1")` would therefore be * the same as `"3 3 [4 [5 5]] 3"`. + * @tags temporal */ export const perx = new Pattern(function (state) { const n = Fraction(1).div(state.span.duration); diff --git a/packages/core/test/signal.test.mjs b/packages/core/test/signal.test.mjs index b02f8a08..26c1d656 100644 --- a/packages/core/test/signal.test.mjs +++ b/packages/core/test/signal.test.mjs @@ -9,7 +9,7 @@ import Fraction from 'fraction.js'; import { describe, it, expect, vi } from 'vitest'; import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs'; -import { fastcat, sequence, State, TimeSpan, Hap } from '../index.mjs'; +import { fastcat, sequence, State, TimeSpan, Hap, note } from '../index.mjs'; const st = (begin, end) => new State(ts(begin, end)); const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end)); @@ -59,3 +59,9 @@ describe('perx', () => { ); }); }); + +describe('shuffle', () => { + it('returns original pattern if input is 1', () => { + expect(note('c d e f').sound('piano').shuffle(1).firstCycle()).toEqual(note('c d e f').sound('piano').firstCycle()); + }); +}); diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 915a2cb7..c38c8e05 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -339,8 +339,13 @@ export function uniqsortr(a) { export function unicodeToBase64(text) { const utf8Bytes = new TextEncoder().encode(text); - const base64String = btoa(String.fromCharCode(...utf8Bytes)); - return base64String; + let binaryString = ''; + const chunkSize = 0x8000; + for (let i = 0; i < utf8Bytes.length; i += chunkSize) { + const chunk = utf8Bytes.subarray(i, i + chunkSize); + binaryString += String.fromCharCode.apply(null, chunk); + } + return btoa(binaryString); } export function base64ToUnicode(base64String) { diff --git a/packages/csound/index.mjs b/packages/csound/index.mjs index e4476522..e459f387 100644 --- a/packages/csound/index.mjs +++ b/packages/csound/index.mjs @@ -135,6 +135,8 @@ export async function loadOrc(url) { * p4 -- MIDI key number (as a real number, not an integer but in [0, 127]. * p5 -- MIDI velocity (as a real number, not an integer but in [0, 127]. * p6 -- Strudel controls, as a string. + * + * @tags external_io */ export const csoundm = register('csoundm', (instrument, pat) => { let p1 = instrument; diff --git a/packages/csound/package.json b/packages/csound/package.json index 762131d6..4af015bb 100644 --- a/packages/csound/package.json +++ b/packages/csound/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/csound", - "version": "1.2.6", + "version": "1.3.0", "description": "csound bindings for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index 95f3aed5..f915e911 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -78,6 +78,16 @@ export const cleanupDraw = (clearScreen = true, id) => { stopAllAnimations(id); }; +export const cleanupDrawContext = (replID) => { + const ctx = getDrawContext(); + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + + // clear the big canvas context, ignore inline widgets + Object.keys(animationFrames).forEach( + (id) => (!replID || id.startsWith(replID)) && !id.startsWith('_') && stopAnimationFrame(id), + ); +}; + Pattern.prototype.onPaint = function (painter) { return this.withState((state) => { if (!state.controls.painters) { diff --git a/packages/draw/package.json b/packages/draw/package.json index b0af8418..4a6f9a5a 100644 --- a/packages/draw/package.json +++ b/packages/draw/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/draw", - "version": "1.2.5", + "version": "1.2.6", "description": "Helpers for drawing with Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/draw/pianoroll.mjs b/packages/draw/pianoroll.mjs index 1cf218fa..6fb1dfc7 100644 --- a/packages/draw/pianoroll.mjs +++ b/packages/draw/pianoroll.mjs @@ -42,6 +42,7 @@ const getValue = (e) => { * * @name pianoroll * @synonyms punchcard + * @tags visualization * @param {Object} options Object containing all the optional following parameters as key value pairs: * @param {integer} cycles number of cycles to be displayed at the same time - defaults to 4 * @param {number} playhead location of the active notes on the time axis - 0 to 1, defaults to 0.5 @@ -299,6 +300,7 @@ Pattern.prototype.punchcard = function (options) { * Supports all the same options as pianoroll. * * @name wordfall + * @tags visualization */ Pattern.prototype.wordfall = function (options) { return this.punchcard({ vertical: 1, labels: 1, stroke: 0, fillActive: 1, active: 'white', ...options }); diff --git a/packages/draw/pitchwheel.mjs b/packages/draw/pitchwheel.mjs index ba76df07..aa9cd21c 100644 --- a/packages/draw/pitchwheel.mjs +++ b/packages/draw/pitchwheel.mjs @@ -54,11 +54,42 @@ export function pitchwheel({ } if (edo) { + edo = haps.length >= 1 && haps[0].value && haps[0].value.edo ? haps[0].value.edo : edo; + root = haps.length >= 1 && haps[0].value && haps[0].value.root ? haps[0].value.root : root; + const degreeIndexes = + haps.length >= 1 && haps[0].value && haps[0].value.degreeIndexes ? haps[0].value.degreeIndexes : null; + const intLabels = haps.length >= 1 && haps[0].value && haps[0].value.intLabels ? haps[0].value.intLabels : null; + ctx.font = '20px sans-serif'; + const label = `${edo} EDO`; + // Draw EDO label: + ctx.fillText(label, centerX + radius - ctx.measureText(label).width + 15, centerY + radius); Array.from({ length: edo }, (_, i) => { const angle = freq2angle(root * Math.pow(2, i / edo), root); + const [x, y] = circlePos(centerX, centerY, radius, angle); ctx.beginPath(); - ctx.arc(x, y, hapRadius, 0, 2 * Math.PI); + // Draw interval label for degree i when it exists: + if (degreeIndexes === null || degreeIndexes.includes(i)) { + ctx.globalAlpha = 1; + ctx.arc(x, y, hapRadius, 0, 2 * Math.PI); + if (intLabels !== null) { + const degree = degreeIndexes.indexOf(i); + if (intLabels[degree]) { + if (angle < 0.32 && angle > 0.125) { + ctx.fillText(intLabels[degree], x - 34, y); + } else { + if (angle < 0.1 && angle > -1.125) { + ctx.fillText(intLabels[degree], x - 7, y - 12); + } else { + ctx.fillText(intLabels[degree], x + 9, y); + } + } + } + } + } else { + ctx.globalAlpha = 0.15; + ctx.arc(x, y, hapRadius, 0, 2 * Math.PI); + } ctx.fill(); }); ctx.stroke(); @@ -116,6 +147,7 @@ export function pitchwheel({ /** * Renders a pitch circle to visualize frequencies within one octave * @name pitchwheel + * @tags visualization * @param {number} hapcircles * @param {number} circle * @param {number} edo diff --git a/packages/draw/spiral.mjs b/packages/draw/spiral.mjs index cebf3d37..c18c66c5 100644 --- a/packages/draw/spiral.mjs +++ b/packages/draw/spiral.mjs @@ -129,6 +129,7 @@ function drawSpiral(options) { * Displays a spiral visual. * * @name spiral + * @tags visualization * @param {Object} options Object containing all the optional following parameters as key value pairs: * @param {number} stretch controls the rotations per cycle ratio, where 1 = 1 cycle / 360 degrees * @param {number} size the diameter of the spiral diff --git a/packages/edo/README.md b/packages/edo/README.md new file mode 100644 index 00000000..482ad9c1 --- /dev/null +++ b/packages/edo/README.md @@ -0,0 +1,41 @@ +# @strudel/edo + +This package adds EDO scale functions to strudel Patterns. + +## Install + +```sh +npm i @strudel/edo --save +``` + +## Example + +```js +import { n } from '@strudel/core'; +import '@strudel/edo'; + +// E.g. edoScale for Gorgo-6 scale, 16 EDO, LLsLLLs +// base note C3, large step size 3, small step size 1: +// C3:LLsLLL:3:1 +const [baseNote, sequence, largeStep, smallStep] = ['C3', 'LLsLLL', 3, 1] +const pattern = n("0 2 4 6 4 2").edoScale([baseNote, sequence, largeStep, smallStep]); + +const events = pattern.firstCycle().map((e) => e.show()); +console.log(events); +``` + +yields: +``` +[ + "[ 0/1 → 1/1 | + { + \"degree\":1, + \"degreeIndexes\":[0,3,6,7,10,13], + \"intLabels\":[null,\"S2\",\"d4\",\"N4\",\"s6\",\"s7\",\"P8\"], + \"root\":\"130.8128\", + \"freq\":130.813, + \"edo\":16 + } + ]" +] +``` diff --git a/packages/edo/edo.mjs b/packages/edo/edo.mjs new file mode 100644 index 00000000..1e99c243 --- /dev/null +++ b/packages/edo/edo.mjs @@ -0,0 +1,114 @@ +/* +edo.mjs - Equal division of the octave (EDO) scale functions for strudel +Copyright (C) 2025 Rob McKinnon and Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +import { register, pure, noteToMidi, isNote, tokenizeNote } from '@strudel/core'; +import { EdoScale } from './edoscale.mjs'; +import { Intervals } from './intervals.mjs'; +import { Pitches } from './pitches.mjs'; + +const pitchesCache = new Map(); + +/** + * Turns numbers into notes in the given EDO scale (zero indexed). + * + * An EDO scale definition looks like this: + * + * e.g. C:LLsLLLs:2:1 <- this is the C major scale, 12 EDO + * + * e.g. C:LLsLLL:3:1 <- this is the Gorgo 6 note scale, 16 EDO + * + * An EDO scale, e.g. C:LLsLLLs:2:1, consists of a root note (e.g. C) + * followed by semicolon (':') + * and then a [Large/small step notation sequence](https://en.xen.wiki/w/MOS_scale) + * (e.g. LLsLLLs) + * followed by semicolon, then the large step size (e.g. 2) + * followed by semicolon, then the small step size (e.g. 1). + * + * The number of divisions of the octave is calculated as the sum + * of the steps in the EDO scale definition. + * + * e.g. C:LLsLLLs:2:1 is 2+2+1+2+2+2+1 = 12 EDO, 7 note scale + * + * e.g. C:LLsLLL:3:1 is 3+3+1+3+3+3 = 16 EDO, 6 note scale + * + * The root note defaults to octave 3, if no octave number is given. + * + * @name edoScale + * @param {string} scale Definition of EDO scale. + * @returns Pattern + * @example + * n("0 2 4 6 4 2").edoScale("C:LLsLLLs:2:1") + * @example + * n("[0,7] 4 [2,7] 4") + * .edoScale("G2::3:1") + * .s("piano")._pitchwheel() + * @example + * n(rand.range(0,5).segment(6)) + * .edoScale(":LLsLL:3:1") + * .s("piano")._pitchwheel() + */ +export const edoScale = register( + 'edoScale', + function (scaleDefinition, pat) { + // console.log(scaleDefinition); + + // if (Array.isArray(scale)) { + const key = scaleDefinition.flat().join(':'); + // } + // console.log(scaleDefinition); + let pitches; + if (pitchesCache.has(key)) { + pitches = pitchesCache.get(key); + } else { + // console.log({ key }); + const [base_note, sequence, large, small] = scaleDefinition; + const root_octave = tokenizeNote(base_note)[2] || 3; + // console.log({ root_octave }); + const scale = new EdoScale(large, small, sequence); + const intervals = new Intervals(scale); + // console.log({ intervals }); + pitches = new Pitches(scale, intervals, 440, noteToMidi(base_note), root_octave); + pitchesCache.set(key, pitches); + // console.log({ base_note: noteToMidi(base_note) }); + // console.log({ sequence }); + // console.log({ edivisions: scale.edivisions }); + // console.log({ intervals }); + // console.log({ pat }); + // console.log({ pitches: pitches }); + } + return pat + .fmap((value) => { + const isObject = typeof value === 'object'; + const n = isObject ? value.n : value; + if (isObject) { + delete value.n; // remove n so it won't cause trouble + } + if (isNote(n)) { + // legacy.. + return pure(n); + } + const deg = (typeof n === 'string' ? parseInt(n, 10) : Number.isInteger(n) ? n : Math.round(n)) + 1; + + const [oct, degree] = pitches.octdeg(deg); + const freq = pitches.octdegfreq(oct, degree); + const note = pitches.octdegmidi(oct, degree); + const edo = pitches.scale.edivisions; + const root = pitches.base_freq; + const degreeIndexes = pitches.scale.divisions; + const intLabels = pitches.intervals.intLabels; + // const color = 'red'; + value = pure(isObject ? { ...value, degree, degreeIndexes, intLabels, root, freq, edo } : note); + // value = pure(isObject ? { ...value, key } : note); + // value = pure(isObject ? { ...value, edo } : note); + // console.log({ value }); + return value; + }) + .outerJoin() + .withHap((hap) => hap.setContext({ ...hap.context, scaleDefinition })); + }, + true, + true, // preserve tactus +); diff --git a/packages/edo/edoscale.mjs b/packages/edo/edoscale.mjs new file mode 100644 index 00000000..03205408 --- /dev/null +++ b/packages/edo/edoscale.mjs @@ -0,0 +1,213 @@ +/* +edoscale.mjs - EdoScale defines equal division of the octave (EDO) scale in Ls notation + - Port of pitfalls/lib/Scale.lua - see +Copyright (C) 2025 Rob McKinnon and Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ +const M = 2; +const L = 1; +const S = 0; +const LABELS = ['s', 'L', 'M']; + +export class EdoScale { + constructor(large, small, sequence, medium) { + this.stepbackup = [L, L, S, L, L, L, S, L, L, L, S, L, L, L, S, L]; + this.large = large; + this.medium = medium || large; + this.small = small; + this.divisions = []; + this.edivisions = null; + this.sequence = null; + this.tonic = 1; + this.mode = 1; + this.max_steps = 12; + this.min_steps = 3; + this.setSequence(sequence); + } + + hasMedium() { + return this.step.some((_, i) => this.step[this.offset(i)] === M); + } + + stepSize(i) { + return LABELS[this.step[this.offset(i)]]; + } + + sequence() { + return this.step.map((_, i) => this.stepSize(i)).join(''); + } + + stepValue(i) { + const step = this.step[this.offset(i)]; + return step === L ? this.large : step === M ? this.medium : this.small; + } + + offset(i) { + if (this.mode === 1) { + return i; + } else { + const offset = (this.mode - 1 + i) % this.length; + return offset === 0 ? this.length : offset; + } + } + + static setMaxSteps(max) { + this.max_steps = max; + } + + static setMinSteps(min) { + this.min_steps = min; + } + + setSequence(sequence) { + if (this.sequence !== sequence) { + this.sequence = sequence; + this.length = sequence.length; + this.step = []; + for (let i = 0; i < sequence.length; i++) { + const char = sequence[i]; + this.step[i] = char === 'L' ? L : char === 'M' ? M : S; + } + this.updateEdo(); + } else { + return false; + } + } + + setLarge(l) { + if (this.large !== l) { + this.large = l; + this.updateEdo(); + } else { + return false; + } + } + + setMedium(m) { + if (this.medium !== m) { + this.medium = m; + this.updateEdo(); + } else { + return false; + } + } + + setSmall(s) { + if (this.small !== s) { + this.small = s; + this.updateEdo(); + } else { + return false; + } + } + + setMode(mode) { + this.mode = mode; + } + + setTonic(tonic) { + this.tonic = tonic; + } + + changeMode(d) { + const orig = this.mode; + this.mode = Math.max(1, Math.min(this.mode + d, this.length)); + return orig !== this.mode; + } + + changeTonic(d) { + const orig = this.tonic; + this.tonic = Math.max(1, Math.min(this.tonic + d, this.edivisions)); + return orig !== this.tonic; + } + + updateEdo() { + const orig = this.edivisions; + this.edivisions = this.step.reduce((sum, _, i) => { + this.divisions[i] = sum; + return sum + this.stepValue(i); + }, 0); + // console.log(this.divisions); + const changed = orig !== this.edivisions; + if (changed) { + this.tonic = Math.max(1, Math.min(this.tonic, this.edivisions)); + } + return changed; + } + + changeStep(d, i) { + const index = this.offset(i); + const orig = this.step[index]; + this.step[index] = Math.max(S, Math.min(this.step[index] + d, M)); + this.stepbackup[index] = this.step[index]; + const changed = orig !== this.step[index]; + if (changed) { + this.updateEdo(); + } + return changed; + } + + changeLarge(d) { + const orig = this.large; + this.setLarge(Math.max(this.small + 1, Math.min(this.large + d, this.large + 1))); + const changed = this.large !== orig; + if (changed) { + if (this.large <= this.medium) { + this.setMedium(Math.max(this.small + 1, Math.min(this.large - 1, this.large))); + } + this.updateEdo(); + } + return changed; + } + + changeMedium(d) { + const orig = this.medium; + this.setMedium(Math.max(this.small + 1, Math.min(this.medium + d, this.large - 1))); + const changed = this.medium !== orig; + if (changed) { + this.updateEdo(); + } + return changed; + } + + changeSmall(d) { + const orig = this.small; + const value = this.small + d; + + if (this.hasMedium()) { + this.setSmall(Math.max(1, Math.min(value, this.medium - 1))); + } else { + this.setSmall(Math.max(1, Math.min(value, this.large - 1))); + } + + const changed = this.small !== orig; + if (changed) { + if (this.small >= this.medium) { + this.setMedium(Math.max(this.small + 1, Math.min(this.large))); + } + this.updateEdo(); + } + return changed; + } + + changeLength(d) { + const orig = this.length; + if (d === 1) { + this.length = Math.min(this.length + 1, this.max_steps); + } else if (d === -1) { + this.length = Math.max(this.length - 1, this.min_steps); + } + + const changed = this.length !== orig; + if (changed) { + this.mode = 1; + if (d === 1) { + this.step[this.length] = this.stepbackup[this.length] || L; + } else if (d === -1 && this.length >= this.min_steps) { + this.step.pop(); + } + this.updateEdo(); + } + return changed; + } +} diff --git a/packages/edo/index.mjs b/packages/edo/index.mjs new file mode 100644 index 00000000..0bd5a41a --- /dev/null +++ b/packages/edo/index.mjs @@ -0,0 +1,5 @@ +import './edo.mjs'; + +export * from './edo.mjs'; + +export const packageName = '@strudel/edo'; diff --git a/packages/edo/intervals.mjs b/packages/edo/intervals.mjs new file mode 100644 index 00000000..e79afe3d --- /dev/null +++ b/packages/edo/intervals.mjs @@ -0,0 +1,103 @@ +/* +intervals.mjs - defines Intervals for equal division of the octave (EDO) scale + - Port of pitfalls/lib/Intervals.lua - see +Copyright (C) 2025 Rob McKinnon and Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ +import ratiointervals from './ratios.mjs'; + +function ratio(division, edivisions) { + return division === 0 ? 1 : Math.pow(2, division / edivisions); +} + +export class Intervals { + constructor(scale) { + this.scale = scale; + this.intLabels = []; + this.intNoms = []; + this.intRatios = []; + this.uniqLabels = []; + this.intErrors = []; + this.ratios = []; + + const BLANK = ''; + let division = 0; + const labToErr = {}; + const labToInd = {}; + + this.ratios[0] = 1; + + for (let i = 0; i < scale.length; i++) { + division += scale.stepValue(i); + this.ratios[i + 1] = ratio(division, scale.edivisions); + + if (i < scale.length) { + const nearest = ratiointervals.nearestInterval(this.ratios[i + 1]); + const closeness = nearest[0]; + const ratio = nearest[1]; + const intLabel = ratiointervals.key(ratio); + this.intLabels[i + 1] = intLabel; + this.intErrors[i + 1] = closeness; + this.intNoms[i + 1] = ratio ? ratiointervals.nom(ratio) : 0; + this.intRatios[i + 1] = ratio ? `${ratiointervals.nom(ratio)}/${ratiointervals.denom(ratio)}` : ''; + this.uniqLabels[i + 1] = BLANK; + + if (intLabel && intLabel !== 'P1' && intLabel !== 'P8') { + if (!labToErr[intLabel]) { + this.uniqLabels[i + 1] = intLabel; + labToInd[intLabel] = i + 1; + labToErr[intLabel] = closeness; + } else if (closeness < labToErr[intLabel]) { + this.uniqLabels[labToInd[intLabel]] = BLANK; + this.uniqLabels[i + 1] = intLabel; + labToInd[intLabel] = i + 1; + labToErr[intLabel] = closeness; + } + } + } + } + } + + ratio(i) { + return this.ratios[i]; + } + + intervalLabel(i) { + return this.intLabels[i]; + } + + intervalNominator(i) { + return this.intNoms[i]; + } + + intervalRatio(i) { + return this.intRatios[i]; + } + + uniqIntervalLabel(i) { + return this.uniqLabels[i]; + } + + intervalError(i) { + return this.intErrors[i]; + } + + nearestDegreeTo(r, threshold) { + let min = 1; + let degree = null; + + for (const [i, v] of Object.entries(this.ratios)) { + const diff = Math.abs((r - v) / r); + if (diff < min) { + min = diff; + degree = parseInt(i, 10); + } + } + + if (threshold == null) { + return degree; + } else { + return min < threshold ? degree : 1; + } + } +} diff --git a/packages/edo/package.json b/packages/edo/package.json new file mode 100644 index 00000000..42097279 --- /dev/null +++ b/packages/edo/package.json @@ -0,0 +1,42 @@ +{ + "name": "@strudel/edo", + "version": "0.1.0", + "description": "Equal division of the octave (EDO) scale functions for strudel", + "main": "index.mjs", + "publishConfig": { + "main": "dist/index.mjs" + }, + "scripts": { + "build": "vite build", + "test": "vitest run", + "prepublishOnly": "npm run build" + }, + "type": "module", + "repository": { + "type": "git", + "url": "git+https://codeberg.org/uzu/strudel.git" + }, + "keywords": [ + "tidalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Rob McKinnon ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://codeberg.org/uzu/strudel/issues" + }, + "homepage": "https://codeberg.org/uzu/strudel#readme", + "dependencies": { + "@strudel/core": "workspace:*", + "@tonaljs/tonal": "^4.10.0", + "chord-voicings": "^0.0.1", + "webmidi": "^3.1.12" + }, + "devDependencies": { + "vite": "^6.0.11", + "vitest": "^3.0.4" + } +} diff --git a/packages/edo/pitches.mjs b/packages/edo/pitches.mjs new file mode 100644 index 00000000..003e15b7 --- /dev/null +++ b/packages/edo/pitches.mjs @@ -0,0 +1,98 @@ +/* +pitches.mjs - defines Pitches for equal division of the octave (EDO) scale + - Port of pitfalls/lib/Pitches.lua - see +Copyright (C) 2025 Rob McKinnon and Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +function ratio(division, edivisions) { + return division === 0 ? 1 : Math.pow(2, division / edivisions); +} + +// This function gets frequency based on index, edo, octave, and base frequency +function get_freq(base_freq, edo, index, oct, base_octave) { + let f = base_freq * ratio(index - 1, edo); + if (oct < base_octave) { + f /= Math.pow(2, base_octave - oct); + } else if (oct > base_octave) { + f *= Math.pow(2, oct - base_octave); + } + return f; +} + +// Convert MIDI number to Hz using a given tuning multiplier +function midi_to_hz(n, tuning) { + return tuning * Math.pow(2, (n - 69) / 12.0); +} + +const denom = Math.log(2); +function hz_to_midi(freq, tuning) { + return 12.0 * (Math.log(freq / tuning) / denom) + 69; +} + +export class Pitches { + constructor(scale, intervals, tuning, midi_start, root_octave) { + this.scale = scale; + this.intervals = intervals; + this.base_freq = midi_to_hz(midi_start, tuning); + this.root_octave = root_octave; + this.freqs = {}; + this.midis = {}; + this.degrees = {}; + this.octdegfreqs = {}; + this.octdegmidis = {}; + let index = 0; + let f = null; + + for (let oct = 0; oct <= 8; oct++) { + this.octdegfreqs[oct] = {}; + this.octdegmidis[oct] = {}; + f = get_freq(this.base_freq, scale.edivisions, scale.tonic, oct, this.root_octave); + for (let deg = 0; deg < scale.length; deg++) { + index = index + 1; + this.freqs[index] = parseFloat((f * intervals.ratio(deg)).toFixed(3)); + this.midis[index] = parseFloat(hz_to_midi(f * intervals.ratio(deg), tuning).toFixed(4)); + this.degrees[index] = deg; + this.octdegfreqs[oct][deg + 1] = this.freqs[index]; + this.octdegmidis[oct][deg + 1] = this.midis[index]; + } + } + this.base_freq = parseFloat(this.base_freq).toFixed(4); + } + + base_freq() { + return this.base_freq; + } + + degree(index) { + return this.degrees[index]; + } + + freq(index) { + return this.freqs[index]; + } + + octdeg(deg) { + const higherOcatve = deg > this.scale.length; + const octave = this.root_octave + (higherOcatve ? Math.floor((deg - 1) / this.scale.length) : 0); + const degree = higherOcatve ? (deg % this.scale.length === 0 ? this.scale.length : deg % this.scale.length) : deg; + // console.log([octave, degree]); + return [octave, degree]; + } + + octdegfreq(oct, deg) { + if (this.octdegfreqs[oct]) { + return this.octdegfreqs[oct][deg]; + } else { + return null; + } + } + + octdegmidi(oct, deg) { + if (this.octdegmidis[oct]) { + return this.octdegmidis[oct][deg]; + } else { + return null; + } + } +} diff --git a/packages/edo/ratios.mjs b/packages/edo/ratios.mjs new file mode 100644 index 00000000..b431a237 --- /dev/null +++ b/packages/edo/ratios.mjs @@ -0,0 +1,95 @@ +/* +ratios.mjs - lists whole number ratios for pitch intervals + - Port of pitfalls/lib/ratios.lua - see +Copyright (C) 2025 Rob McKinnon and Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ +const ratiointervals = {}; + +// FJS Calculators - https://misotanni.github.io/fjs/en/calc.html +// List of pitch intervals - https://en.wikipedia.org/wiki/List_of_pitch_intervals +// Gallery of just intervals - https://en.xen.wiki/w/Gallery_of_just_intervals +// Two letter codes changed to make different interval labels more unique. +ratiointervals.list = new Map([ + [1, ['P1', 'P1', 'P1', 1, 1]], // unison P1 + [16 / 15, ['m2', 'm2', 'm2_5', 16, 15]], // minor second m2 + [15 / 14, ['A1', 'A1', 'A1^5_7', 15, 14]], // augmented unison + [13 / 12, ['t2', 'm2', 'm2^13', 13, 12]], // tridecimal neutral second + [12 / 11, ['N2', 'M2', 'M2_11', 12, 11]], // undecimal neutral second + [11 / 10, ['n2', 'm2', 'm2^11_5', 11, 10]], // undecimal submajor second + [10 / 9, ['T2', 'M2', 'M2^5', 10, 9]], // classic (whole) tone + [9 / 8, ['M2', 'M2', 'M2', 9, 8]], // major second M2 + [8 / 7, ['S2', 'M2', 'M2_7', 8, 7]], // septimal major second + [7 / 6, ['s3', 'm3', 'm3^7', 7, 6]], // septimal minor third + [19 / 16, ['o3', 'm3', 'm3^19', 19, 16]], // otonal minor third + [6 / 5, ['m3', 'm3', 'm3_5', 6, 5]], // minor third m3 + [17 / 14, ['t3', 'm3', 'm3^17_7', 17, 14]], // septendecimal supraminor third + [11 / 9, ['n3', 'm3', 'm3^11', 11, 9]], // undecimal neutral third + [5 / 4, ['M3', 'M3', 'M3^5', 5, 4]], // major third M3 + [9 / 7, ['S3', 'M3', 'M3_7', 9, 7]], // septimal major third SM3 + [13 / 10, ['d4', 'd4', 'd4^13_5', 13, 10]], // Barbados third + [4 / 3, ['P4', 'P4', 'P4', 4, 3]], // perfect fourth P4 + [19 / 14, ['N4', 'P4', 'P4^19_7', 19, 14]], // undevicesimal wide fourth + [11 / 8, ['n4', 'P4', 'P4^11', 11, 8]], // super-fourth + [25 / 18, ['a4', 'A4', 'A4^5,5', 25, 18]], // classic augmented fourth + [7 / 5, ['sT', 'd5', 'd5^7_5', 7, 5]], // lesser septimal tritone + [45 / 32, ['A4', 'A4', 'A4^5', 45, 32]], // just augmented fourth + [17 / 12, ['d5', 'd5', 'd5^17', 17, 12]], // larger septendecimal tritone + [10 / 7, ['ST', 'A4', 'A4^5_7', 10, 7]], // greater septimal tritone + [13 / 9, ['t5', 'd5', 'd5^13', 13, 9]], // tridecimal diminished fifth + [3 / 2, ['P5', 'P5', 'P5', 3, 2]], // perfect fifth P5 + [14 / 9, ['s6', 'M6', 'm6^7', 14, 9]], // subminor sixth or septimal sixth + [25 / 16, ['a5', 'A5', 'A5^5,5', 25, 16]], // classic augmented fifth + [11 / 7, ['A5', 'P5', 'P5^11_7', 11, 7]], // undecimal minor sixth + [8 / 5, ['m6', 'm6', 'm6_5', 8, 5]], // minor sixth m6 + [13 / 8, ['N6', 'm6', 'm6^13', 13, 8]], // tridecimal neutral sixth + [18 / 11, ['n6', 'M6', 'M6_11', 18, 11]], // undecimal neutral sixth + [5 / 3, ['M6', 'M6', 'M6^5', 5, 3]], // just major sixth M6 + [128 / 75, ['d7', 'd7', 'd7_5,5', 128, 75]], // diminished seventh + [17 / 10, ['T6', 'd7', 'd7^17_5', 17, 10]], // septendecimal diminished seventh + [12 / 7, ['S6', 'M6', 'M6_7', 12, 7]], // septimal major sixth + [7 / 4, ['s7', 'm7', 'm7^7', 7, 4]], // septimal minor seventh + [16 / 9, ['m7', 'm7', 'm7', 16, 9]], // lesser minor seventh + [9 / 5, ['g7', 'm7', 'm7_5', 9, 5]], // greater just minor seventh + [11 / 6, ['n7', 'm7', 'm7^11', 11, 6]], // undecimal neutral seventh + [13 / 7, ['N7', 'm7', 'm7^13_7', 13, 7]], // tridecimal neutral seventh + [15 / 8, ['M7', 'M7', 'M7^5', 15, 8]], // major seventh + [17 / 9, ['T7', 'd8', 'd8^17', 17, 9]], // large septendecimal major seventh + [19 / 10, ['d8', 'd8', 'd8^19_5', 19, 10]], // large undevicesimal major seventh + [2, ['P8', 'P8', 'P8', 2, 1]], // octave P8 +]); + +ratiointervals.key = function (ratio) { + return ratio == null ? '' : ratiointervals.list.get(ratio)?.[0] || ''; +}; + +ratiointervals.label = function (ratio) { + return ratio == null ? '' : ratiointervals.list.get(ratio)?.[1] || ''; +}; + +ratiointervals.fjs = function (ratio) { + return ratio == null ? '' : ratiointervals.list.get(ratio)?.[2] || ''; +}; + +ratiointervals.nom = function (ratio) { + return ratio == null ? null : ratiointervals.list.get(ratio)?.[3] || null; +}; + +ratiointervals.denom = function (ratio) { + return ratio == null ? null : ratiointervals.list.get(ratio)?.[4] || null; +}; + +ratiointervals.nearestInterval = function (v) { + let min = 1; + let match = null; + for (const [ratio, _labels] of ratiointervals.list) { + const diff = Math.abs((ratio - v) / ratio); + if (diff < min) { + min = diff; + match = ratio; + } + } + return min < 0.01 ? [min, match] : [null, null]; +}; + +export default ratiointervals; diff --git a/packages/edo/test/edo.test.mjs b/packages/edo/test/edo.test.mjs new file mode 100644 index 00000000..7cebc34b --- /dev/null +++ b/packages/edo/test/edo.test.mjs @@ -0,0 +1,34 @@ +/* +edo.test.mjs - tests of edo.mjs +Copyright (C) 2025 Rob McKinnon and Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ +import '../edo.mjs'; +import { n } from '@strudel/core'; +import { describe, it, expect } from 'vitest'; + +describe('edoScale', () => { + it('Should run tonal functions ', () => { + // base note A3 = 220Hz + let baseNote = 'A3'; + let root = 220; + let freq = 220; + let pattern = n('0').edoScale([baseNote, 'LLsLLLs', 2, 1]); + let cycle = pattern.firstCycleValues[0]; + expect(cycle.freq).toEqual(freq); + expect(cycle.edo).toEqual(12); + expect(cycle.degree).toEqual(1); + expect(parseFloat(cycle.root).toFixed(0)).toEqual(root.toFixed(0)); + + // base note A4 = 440Hz + baseNote = 'A4'; + root = 440; + freq = 880; + pattern = n('7').edoScale([baseNote, 'LLsLLLs', 2, 1]); + cycle = pattern.firstCycleValues[0]; + expect(cycle.freq).toEqual(freq); + expect(cycle.edo).toEqual(12); + expect(cycle.degree).toEqual(1); + expect(parseFloat(cycle.root).toFixed(0)).toEqual(root.toFixed(0)); + }); +}); diff --git a/packages/edo/test/edoscale.test.mjs b/packages/edo/test/edoscale.test.mjs new file mode 100644 index 00000000..531a1b57 --- /dev/null +++ b/packages/edo/test/edoscale.test.mjs @@ -0,0 +1,21 @@ +/* +edoscale.test.mjs - tests of EdoScale from edoscale.mjs +Copyright (C) 2025 Rob McKinnon and Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ +import '../edoscale.mjs'; // need to import this to add prototypes +import { EdoScale } from '../edoscale.mjs'; +import { describe, it, expect } from 'vitest'; + +describe('EdoScale', () => { + it('updates edivisions', () => { + let scale = new EdoScale(2, 1, ['L', 'L', 's', 'L', 'L', 'L', 's']); + expect(scale.stepSize(0)).toEqual('L'); + expect(scale.stepValue(0)).toEqual(2); + + expect(scale.stepSize(2)).toEqual('s'); + expect(scale.stepValue(2)).toEqual(1); + + expect(scale.edivisions).toEqual(12); + }); +}); diff --git a/packages/edo/test/ratios.test.mjs b/packages/edo/test/ratios.test.mjs new file mode 100644 index 00000000..f833177d --- /dev/null +++ b/packages/edo/test/ratios.test.mjs @@ -0,0 +1,18 @@ +/* +ratios.test.mjs - tests of ratios.mjs +Copyright (C) 2025 Rob McKinnon and Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ +import ratiointervals from '../ratios.mjs'; +import { describe, it, expect } from 'vitest'; + +describe('ratiointervals', () => { + it('updates edivisions', () => { + let ratio = 9 / 7; + expect(ratiointervals.key(ratio)).toEqual('S3'); + expect(ratiointervals.label(ratio)).toEqual('M3'); + expect(ratiointervals.fjs(ratio)).toEqual('M3_7'); + expect(ratiointervals.nom(ratio)).toEqual(9); + expect(ratiointervals.denom(ratio)).toEqual(7); + }); +}); diff --git a/packages/edo/vite.config.js b/packages/edo/vite.config.js new file mode 100644 index 00000000..5df3edc1 --- /dev/null +++ b/packages/edo/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import { dependencies } from './package.json'; +import { resolve } from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + build: { + lib: { + entry: resolve(__dirname, 'index.mjs'), + formats: ['es'], + fileName: (ext) => ({ es: 'index.mjs' })[ext], + }, + rollupOptions: { + external: [...Object.keys(dependencies)], + }, + target: 'esnext', + }, +}); diff --git a/packages/embed/package.json b/packages/embed/package.json index ffafdc8b..84a8cd6f 100644 --- a/packages/embed/package.json +++ b/packages/embed/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/embed", - "version": "1.1.1", + "version": "1.1.2", "description": "Embeddable Web Component to load a Strudel REPL into an iframe", "main": "embed.js", "type": "module", diff --git a/packages/gamepad/package.json b/packages/gamepad/package.json index 6a07df96..21ad8eef 100644 --- a/packages/gamepad/package.json +++ b/packages/gamepad/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/gamepad", - "version": "1.2.5", + "version": "1.2.6", "description": "Gamepad Inputs for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/hydra/package.json b/packages/hydra/package.json index 16d6f0f4..67419ce0 100644 --- a/packages/hydra/package.json +++ b/packages/hydra/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/hydra", - "version": "1.2.5", + "version": "1.2.6", "description": "Hydra integration for strudel", "main": "hydra.mjs", "type": "module", diff --git a/packages/midi/input.mjs b/packages/midi/input.mjs new file mode 100644 index 00000000..18fb687d --- /dev/null +++ b/packages/midi/input.mjs @@ -0,0 +1,193 @@ +/* +input.mjs - MIDI input wrapper +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +import { WebMidi } from 'webmidi'; +import { logger, ref } from '@strudel/core'; +import { getDevice } from './util.mjs'; + +/** + * MIDI input device wrapper that manages connection and reconnection, tracks + * persisted CC states, etc. These instances are long-lived and are maintained as singletons + * (keyed globally by input string/number). + */ +export class MidiInput { + /** + * + * @param {string | number} input MIDI device name or index defaulting to 0 + */ + constructor(input) { + this.input = input; + this.stateKey = typeof input === 'string' ? input : undefined; // Saved state is not tracked for numeric index inputs + + this._refs = {}; + this._refsByChan = {}; + + this._loadAllStates(); + + this.initialDevice = this._startDeviceListener(); + } + + /** + * Implementation for the cc() factory function tied to this specific input. + * @param {number} cc MIDI CC number + * @param {number | undefined} chan MIDI channel (1-16) or undefined for all channels + */ + createCC(cc, chan) { + const lookupMap = chan === undefined ? this._refs : this._refsByChan[chan]; + if (!(cc in lookupMap)) { + const initialState = this._loadState(chan); + lookupMap[cc] = initialState[cc] || 0; + } + + return ref(() => lookupMap[cc]); + } + + _startDeviceListener() { + const initialDevice = getDevice(this.input, WebMidi.inputs); + + // Background connection loop + (async () => { + const midiListener = this._onMidiMessage.bind(this); + let device = initialDevice; + + while (true) { + if (!device) { + device = await this._waitForDevice(); + } + + // Wait a bit for device to be ready to receive last state + await new Promise((resolve) => setTimeout(resolve, 2000)); + + try { + // Still continue if sending did not work + this._sendAllStates(device); + } catch (err) { + console.error('midiin: failed to send last state on connect:', device.name, err); + } + + // Listen for incoming MIDI messages and for disconnection + device.addListener('midimessage', midiListener); + + await this._waitForDeviceDisconnect(device); + + device.removeListener('midimessage', midiListener); + device = null; // Clear var to trigger wait for connection + } + })(); + + return initialDevice; + } + + // Returns a promise that resolves when the specified device is connected + _waitForDevice() { + return new Promise((resolve) => { + const connListener = () => { + const device = getDevice(this.input, WebMidi.inputs); + if (device) { + logger(`[midi] device reconnected: ${device.name}`); + + WebMidi.removeListener('connected', connListener); + resolve(device); + } + }; + + WebMidi.addListener('connected', connListener); + }); + } + + // Returns a promise that resolves when the specified device is disconnected + _waitForDeviceDisconnect(device) { + return new Promise((resolve) => { + const disconnListener = (e) => { + if (e.port.name === device.name) { + logger(`[midi] device disconnected: ${device.name}`); + + WebMidi.removeListener('disconnected', disconnListener); + resolve(); + } + }; + + WebMidi.addListener('disconnected', disconnListener); + }); + } + + _onMidiMessage(e) { + const ccNum = e.dataBytes[0]; + const v = e.dataBytes[1]; + const chan = e.message.channel; + const scaled = v / 127; + + this._refs[ccNum] = scaled; + this._refsByChan[chan] ??= {}; + this._refsByChan[chan][ccNum] = scaled; + + this._saveState(undefined, ccNum, scaled); + this._saveState(chan, ccNum, scaled); + } + + _loadAllStates() { + Object.assign(this._refs, this._loadState(undefined)); + + for (let chan = 1; chan <= 16; chan++) { + this._refsByChan[chan] ??= {}; + Object.assign(this._refsByChan[chan], this._loadState(chan)); + } + } + + _loadState(chan) { + if (!this.stateKey) { + return {}; + } + + const initialDataRaw = localStorage.getItem( + `strudel-midin-${this.stateKey}-chan${chan !== undefined ? chan : 'all'}`, + ); + if (!initialDataRaw) { + return {}; + } + + try { + return JSON.parse(initialDataRaw); + } catch (err) { + console.warn( + `Failed to parse MIDI state from localStorage for input "${this.stateKey}" and channel "${chan}"`, + initialDataRaw, + err, + ); + return {}; + } + } + + _saveState(chan, cc, value) { + if (!this.stateKey) { + return; + } + + const state = this._loadState(chan); + state[cc] = value; + localStorage.setItem( + `strudel-midin-${this.stateKey}-chan${chan !== undefined ? chan : 'all'}`, + JSON.stringify(state), + ); + } + + // Send CC values back to device to restore encoders and motorized sliders + _sendAllStates(device) { + const output = WebMidi.outputs.find((o) => o.name === device.name); + if (!output) { + return; + } + + for (const [chan, refs] of Object.entries(this._refsByChan)) { + const channel = Number(chan); + for (const [cc, value] of Object.entries(refs)) { + const ccn = Number(cc); + const scaled = Math.round(value * 127); + output.sendControlChange(ccn, scaled, channel); + } + } + } +} diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index d0a1ce18..bd40b743 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -23,6 +23,8 @@ import { noteToMidi, getControlName } from '@strudel/core'; import { Note } from 'webmidi'; import { getAudioContext } from '@strudel/webaudio'; import { scheduleAtTime } from '../superdough/helpers.mjs'; +import { getMidiDeviceNamesString, getDevice } from './util.mjs'; +import { MidiInput } from './input.mjs'; // if you use WebMidi from outside of this package, make sure to import that instance: export const { WebMidi } = _WebMidi; @@ -31,10 +33,6 @@ function supportsMidi() { return typeof navigator.requestMIDIAccess === 'function'; } -function getMidiDeviceNamesString(devices) { - return devices.map((o) => `'${o.name}'`).join(' | '); -} - export function enableWebMidi(options = {}) { const { onReady, onConnected, onDisconnected, onEnabled } = options; if (WebMidi.enabled) { @@ -72,29 +70,6 @@ export function enableWebMidi(options = {}) { }); } -function getDevice(indexOrName, devices) { - if (!devices.length) { - throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`); - } - if (typeof indexOrName === 'number') { - return devices[indexOrName]; - } - const byName = (name) => devices.find((output) => output.name.includes(name)); - if (typeof indexOrName === 'string') { - return byName(indexOrName); - } - // attempt to default to first IAC device if none is specified - const IACOutput = byName('IAC'); - const device = IACOutput ?? devices[0]; - if (!device) { - throw new Error( - `🔌 MIDI device '${device ? device : ''}' not found. Use one of ${getMidiDeviceNamesString(devices)}`, - ); - } - - return IACOutput ?? devices[0]; -} - // send start/stop messages to outputs when repl starts/stops if (typeof window !== 'undefined') { window.addEventListener('message', (e) => { @@ -138,6 +113,7 @@ function githubPath(base, subpath = '') { /** * configures the default midimap, which is used when no "midimap" port is set + * @tags external_io, midi * @example * defaultmidimap({ lpf: 74 }) * $: note("c a f e").midi(); @@ -151,6 +127,7 @@ let loadCache = {}; /** * Adds midimaps to the registry. Inside each midimap, control names (e.g. lpf) are mapped to cc numbers. + * @tags external_io, midi * @example * midimaps({ mymap: { lpf: 74 } }) * $: note("c a f e") @@ -305,6 +282,7 @@ function sendNote(note, velocity, duration, device, midichan, targetTime) { /** * MIDI output: Opens a MIDI output port. + * @tags external_io * @param {string | number} midiport MIDI device name or index defaulting to 0 * @param {object} options Additional MIDI configuration options * @example @@ -492,9 +470,9 @@ Pattern.prototype.midi = function (midiport, options = {}) { }; /** - * Initialize a midi device + * Initialize a midi input device */ -async function _initialize(input) { +async function _initializeInput(input) { if (isPattern(input)) { throw new Error( `[midi] Midi input cannot be a pattern. Make sure to pass device name with single quotes. Example: midin('${ @@ -502,30 +480,38 @@ async function _initialize(input) { }')`, ); } + const initial = await enableWebMidi(); // only returns on first init - const device = getDevice(input, WebMidi.inputs); - if (!device) { - throw new Error( - `[midi] Midi device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`, - ); - } + + const instance = midiInputs[input] || new MidiInput(input); + midiInputs[input] = instance; + if (initial) { + const device = instance.initialDevice; + const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name); logger( - `[midi] Midi enabled! Using "${device.name}". ${ - otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : '' - }`, + device + ? `[midi] Midi enabled! Using "${device.name}". ${ + otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : '' + }` + : `[midi] Midi enabled! Waiting for device "${input}"... Currently connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`, ); } - return device; + + return instance; } +// MIDI input wrappers, by specified input string/index +const midiInputs = {}; + /** * MIDI input: Opens a MIDI input port to receive MIDI control change messages. * * The output is a function that accepts a midi cc value to query as well as (optionally) a midi channel * * @name midin + * @tags external_io, midi * @param {string | number} input MIDI device name or index defaulting to 0 * @returns {function(number, number=): Pattern} A function from (cc, channel?) to a pattern. * When queried, the pattern will produces the most recently received midi value (normalized to 0 to 1) @@ -539,31 +525,10 @@ async function _initialize(input) { * note("c a f e").s("saw") * .when(cc(0).gt(0), x => x.postgain(0)) */ -let listeners = {}; -const refs = {}; -const refsByChan = {}; export async function midin(input) { - const device = await _initialize(input); - refs[input] ??= {}; - refsByChan[input] ??= {}; - const cc = (cc, chan) => { - if (chan !== undefined) { - return ref(() => refsByChan[input][cc]?.[chan] || 0); - } - return ref(() => refs[input][cc] || 0); - }; + const instance = await _initializeInput(input); - listeners[input] && device.removeListener('midimessage', listeners[input]); - listeners[input] = (e) => { - const [ccNum, v] = e.dataBytes; - const chan = e.message.channel; - const scaled = v / 127; - refsByChan[input][ccNum] ??= {}; - refsByChan[input][ccNum][chan] = scaled; - refs[input][ccNum] = scaled; - }; - device.addListener('midimessage', listeners[input]); - return cc; + return instance.createCC.bind(instance); } /** @@ -573,6 +538,7 @@ export async function midin(input) { * note durations * * @name midikeys + * @tags external_io, midi * @param {string | number} input MIDI device name or index defaulting to 0 * @returns {function((number | Pattern)=): Pattern} A function that produces a pattern. * When queried, the pattern will produces the most recently played midi notes and velocities, @@ -617,7 +583,16 @@ function _triggerKeyboard(input, cps, now, latencyCycles) { return true; } export async function midikeys(input) { - const device = await _initialize(input); + const instance = await _initializeInput(input); + + // TODO: support unpluggable device usage + const device = instance.initialDevice; + if (!device) { + throw new Error( + `[midi] Midi device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`, + ); + } + if (!kHaps[input]) { kHaps[input] = []; } diff --git a/packages/midi/package.json b/packages/midi/package.json index 11fe9a3f..1881e024 100644 --- a/packages/midi/package.json +++ b/packages/midi/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/midi", - "version": "1.2.6", + "version": "1.3.0", "description": "Midi API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/midi/util.mjs b/packages/midi/util.mjs new file mode 100644 index 00000000..f02f1f64 --- /dev/null +++ b/packages/midi/util.mjs @@ -0,0 +1,44 @@ +/* +util.mjs - MIDI utility functions +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +import { Input, Output } from 'webmidi'; + +/** + * Get a string listing device names for error messages. + * @param {Input[] | Output[]} devices + * @returns {string} + */ +export function getMidiDeviceNamesString(devices) { + return devices.map((o) => `'${o.name}'`).join(' | '); +} + +/** + * Look up a device by index or name. Otherwise return a default device, or fail if none are connected. + * + * @param {string | number} indexOrName + * @param {Input[] | Output[]} devices + * @returns {Input | Output | undefined} + */ +export function getDevice(indexOrName, devices) { + if (typeof indexOrName === 'number') { + return devices[indexOrName]; + } + const byName = (name) => devices.find((output) => output.name.includes(name)); + if (typeof indexOrName === 'string') { + return byName(indexOrName); + } + // attempt to default to first IAC device if none is specified + const IACOutput = byName('IAC'); + const device = IACOutput ?? devices[0]; + if (!device) { + if (!devices.length) { + throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`); + } + throw new Error(`🔌 Default MIDI device not found. Use one of ${getMidiDeviceNamesString(devices)}`); + } + + return device; +} diff --git a/packages/mini/package.json b/packages/mini/package.json index b9a50fb6..43230655 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mini", - "version": "1.2.5", + "version": "1.2.6", "description": "Mini notation for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mondo/package.json b/packages/mondo/package.json index ebdbd329..1dba882e 100644 --- a/packages/mondo/package.json +++ b/packages/mondo/package.json @@ -1,6 +1,6 @@ { "name": "mondolang", - "version": "1.1.1", + "version": "1.1.2", "description": "a language for functional composition that translates to js", "main": "mondo.mjs", "type": "module", diff --git a/packages/mondough/package.json b/packages/mondough/package.json index 9b170fde..919c5da2 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mondo", - "version": "1.1.5", + "version": "1.1.6", "description": "mondo notation for strudel", "main": "mondough.mjs", "type": "module", diff --git a/packages/motion/motion.mjs b/packages/motion/motion.mjs index 875704ea..fed0fbe0 100644 --- a/packages/motion/motion.mjs +++ b/packages/motion/motion.mjs @@ -7,6 +7,7 @@ import { signal } from '../core/signal.mjs'; * @name accelerationX * @return {Pattern} * @synonyms accX + * @tags external_io * @example * n(accelerationX.segment(4).range(0,7)).scale("C:minor") * @@ -17,6 +18,7 @@ import { signal } from '../core/signal.mjs'; * @name accelerationY * @return {Pattern} * @synonyms accY + * @tags external_io * @example * n(accelerationY.segment(4).range(0,7)).scale("C:minor") * @@ -27,6 +29,7 @@ import { signal } from '../core/signal.mjs'; * @name accelerationZ * @return {Pattern} * @synonyms accZ + * @tags external_io * @example * n(accelerationZ.segment(4).range(0,7)).scale("C:minor") * @@ -37,6 +40,7 @@ import { signal } from '../core/signal.mjs'; * @name gravityX * @return {Pattern} * @synonyms gravX + * @tags external_io * @example * n(gravityX.segment(4).range(0,7)).scale("C:minor") * @@ -47,6 +51,7 @@ import { signal } from '../core/signal.mjs'; * @name gravityY * @return {Pattern} * @synonyms gravY + * @tags external_io * @example * n(gravityY.segment(4).range(0,7)).scale("C:minor") * @@ -57,6 +62,7 @@ import { signal } from '../core/signal.mjs'; * @name gravityZ * @return {Pattern} * @synonyms gravZ + * @tags external_io * @example * n(gravityZ.segment(4).range(0,7)).scale("C:minor") * @@ -67,6 +73,7 @@ import { signal } from '../core/signal.mjs'; * @name rotationAlpha * @return {Pattern} * @synonyms rotA, rotZ, rotationZ + * @tags external_io * @example * n(rotationAlpha.segment(4).range(0,7)).scale("C:minor") * @@ -77,6 +84,7 @@ import { signal } from '../core/signal.mjs'; * @name rotationBeta * @return {Pattern} * @synonyms rotB, rotX, rotationX + * @tags external_io * @example * n(rotationBeta.segment(4).range(0,7)).scale("C:minor") * @@ -87,6 +95,7 @@ import { signal } from '../core/signal.mjs'; * @name rotationGamma * @return {Pattern} * @synonyms rotG, rotY, rotationY + * @tags external_io * @example * n(rotationGamma.segment(4).range(0,7)).scale("C:minor") * @@ -97,6 +106,7 @@ import { signal } from '../core/signal.mjs'; * @name orientationAlpha * @return {Pattern} * @synonyms oriA, oriZ, orientationZ + * @tags external_io * @example * n(orientationAlpha.segment(4).range(0,7)).scale("C:minor") * @@ -107,6 +117,7 @@ import { signal } from '../core/signal.mjs'; * @name orientationBeta * @return {Pattern} * @synonyms oriB, oriX, orientationX + * @tags external_io * @example * n(orientationBeta.segment(4).range(0,7)).scale("C:minor") * @@ -117,6 +128,7 @@ import { signal } from '../core/signal.mjs'; * @name orientationGamma * @return {Pattern} * @synonyms oriG, oriY, orientationY + * @tags external_io * @example * n(orientationGamma.segment(4).range(0,7)).scale("C:minor") * @@ -127,6 +139,7 @@ import { signal } from '../core/signal.mjs'; * @name absoluteOrientationAlpha * @return {Pattern} * @synonyms absOriA, absOriZ, absoluteOrientationZ + * @tags external_io * @example * n(absoluteOrientationAlpha.segment(4).range(0,7)).scale("C:minor") * @@ -137,6 +150,7 @@ import { signal } from '../core/signal.mjs'; * @name absoluteOrientationBeta * @return {Pattern} * @synonyms absOriB, absOriX, absoluteOrientationX + * @tags external_io * @example * n(absoluteOrientationBeta.segment(4).range(0,7)).scale("C:minor") * @@ -147,6 +161,7 @@ import { signal } from '../core/signal.mjs'; * @name absoluteOrientationGamma * @return {Pattern} * @synonyms absOriG, absOriY, absoluteOrientationY + * @tags external_io * @example * n(absoluteOrientationGamma.segment(4).range(0,7)).scale("C:minor") * diff --git a/packages/motion/package.json b/packages/motion/package.json index fd1fae89..dda3c368 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/motion", - "version": "1.2.5", + "version": "1.2.6", "description": "DeviceMotion API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json index 492d915d..9fa9da9b 100644 --- a/packages/mqtt/package.json +++ b/packages/mqtt/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mqtt", - "version": "1.2.5", + "version": "1.2.6", "description": "MQTT API for strudel", "main": "mqtt.mjs", "type": "module", diff --git a/packages/osc/osc.mjs b/packages/osc/osc.mjs index 68caab3a..0744cb8c 100644 --- a/packages/osc/osc.mjs +++ b/packages/osc/osc.mjs @@ -79,6 +79,7 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) { * For more info, read [MIDI & OSC in the docs](https://strudel.cc/learn/input-output/) * * @name osc + * @tags external_io * @memberof Pattern * @returns Pattern */ diff --git a/packages/osc/package.json b/packages/osc/package.json index 00faffbd..56924225 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/osc", - "version": "1.3.0", + "version": "1.3.2", "description": "OSC messaging for strudel", "main": "osc.mjs", "bin": "./server.js", diff --git a/packages/osc/server.js b/packages/osc/server.js old mode 100644 new mode 100755 diff --git a/packages/reference/package.json b/packages/reference/package.json index 6d1fa180..783c1bbc 100644 --- a/packages/reference/package.json +++ b/packages/reference/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/reference", - "version": "1.2.1", + "version": "1.2.2", "description": "Headless reference of all strudel functions", "main": "index.mjs", "type": "module", diff --git a/packages/repl/package.json b/packages/repl/package.json index e5216316..81bf9b02 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/repl", - "version": "1.2.7", + "version": "1.3.0", "description": "Strudel REPL as a Web Component", "module": "index.mjs", "publishConfig": { @@ -36,6 +36,7 @@ "@strudel/codemirror": "workspace:*", "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", + "@strudel/edo": "workspace:*", "@strudel/hydra": "workspace:*", "@strudel/midi": "workspace:*", "@strudel/mini": "workspace:*", diff --git a/packages/repl/prebake.mjs b/packages/repl/prebake.mjs index 26d875c2..d863ed09 100644 --- a/packages/repl/prebake.mjs +++ b/packages/repl/prebake.mjs @@ -8,6 +8,7 @@ export async function prebake() { core, import('@strudel/draw'), import('@strudel/mini'), + import('@strudel/edo'), import('@strudel/tonal'), import('@strudel/webaudio'), import('@strudel/codemirror'), diff --git a/packages/sampler/package.json b/packages/sampler/package.json index b9ea4c2a..a960fbbe 100644 --- a/packages/sampler/package.json +++ b/packages/sampler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/sampler", - "version": "0.2.3", + "version": "0.2.4", "description": "", "keywords": [ "tidalcycles", diff --git a/packages/serial/package.json b/packages/serial/package.json index 9dc6c899..e98a897e 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/serial", - "version": "1.2.5", + "version": "1.2.6", "description": "Webserial API for strudel", "main": "serial.mjs", "type": "module", diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json index 9bf0c400..a904942a 100644 --- a/packages/soundfonts/package.json +++ b/packages/soundfonts/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/soundfonts", - "version": "1.2.6", + "version": "1.3.0", "description": "Soundsfont support for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 584b1967..07199aff 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -172,6 +172,7 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => { 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)]; }; diff --git a/packages/superdough/modulators.mjs b/packages/superdough/modulators.mjs index 05674baa..76fc4c7f 100644 --- a/packages/superdough/modulators.mjs +++ b/packages/superdough/modulators.mjs @@ -98,6 +98,7 @@ export const connectLFO = (id, params, nodeTracker) => { fxi = 'main', depth = 1, depthabs, + retrig = 0, ...filteredParams } = params; const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl); @@ -109,7 +110,7 @@ export const connectLFO = (id, params, nodeTracker) => { const modParams = { ...filteredParams, frequency: sync !== undefined ? sync * cps : rate, - time: cycle / cps, + time: retrig > 0.5 ? 0 : cycle / cps, depth: depthValue, min, max, diff --git a/packages/superdough/ola-processor.js b/packages/superdough/ola-processor.js index 38d45a25..0000d1bb 100644 --- a/packages/superdough/ola-processor.js +++ b/packages/superdough/ola-processor.js @@ -35,7 +35,9 @@ class OLAProcessor extends AudioWorkletProcessor { } /** Handles dynamic reallocation of input/output channels buffer - (channel numbers may lety during lifecycle) **/ + * (channel numbers may vary during lifecycle) + * @tags internals + **/ reallocateChannelsIfNeeded(inputs, outputs) { for (let i = 0; i < this.nbInputs; i++) { let nbChannels = inputs[i].length; @@ -88,7 +90,10 @@ class OLAProcessor extends AudioWorkletProcessor { } } - /** Read next web audio block to input buffers **/ + /** + * Read next web audio block to input buffers + * @tags internals + **/ readInputs(inputs) { // when playback is paused, we may stop receiving new samples if (inputs[0].length && inputs[0][0].length == 0) { @@ -108,7 +113,9 @@ class OLAProcessor extends AudioWorkletProcessor { } } - /** Write next web audio block from output buffers **/ + /** Write next web audio block from output buffers + * @tags internals + **/ writeOutputs(outputs) { for (let i = 0; i < this.nbInputs; i++) { for (let j = 0; j < this.inputBuffers[i].length; j++) { @@ -118,7 +125,9 @@ class OLAProcessor extends AudioWorkletProcessor { } } - /** Shift left content of input buffers to receive new web audio block **/ + /** Shift left content of input buffers to receive new web audio block + * @tags internals + **/ shiftInputBuffers() { for (let i = 0; i < this.nbInputs; i++) { for (let j = 0; j < this.inputBuffers[i].length; j++) { @@ -127,7 +136,9 @@ class OLAProcessor extends AudioWorkletProcessor { } } - /** Shift left content of output buffers to receive new web audio block **/ + /** Shift left content of output buffers to receive new web audio block + * @tags internals + **/ shiftOutputBuffers() { for (let i = 0; i < this.nbOutputs; i++) { for (let j = 0; j < this.outputBuffers[i].length; j++) { @@ -137,7 +148,9 @@ class OLAProcessor extends AudioWorkletProcessor { } } - /** Copy contents of input buffers to buffer actually sent to process **/ + /** Copy contents of input buffers to buffer actually sent to process + * @tags internals + **/ prepareInputBuffersToSend() { for (let i = 0; i < this.nbInputs; i++) { for (let j = 0; j < this.inputBuffers[i].length; j++) { @@ -146,7 +159,9 @@ class OLAProcessor extends AudioWorkletProcessor { } } - /** Add contents of output buffers just processed to output buffers **/ + /** Add contents of output buffers just processed to output buffers + * @tags internals + **/ handleOutputBuffersToRetrieve() { for (let i = 0; i < this.nbOutputs; i++) { for (let j = 0; j < this.outputBuffers[i].length; j++) { diff --git a/packages/superdough/package.json b/packages/superdough/package.json index 78db8338..ce17ab5e 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -1,6 +1,6 @@ { "name": "superdough", - "version": "1.2.6", + "version": "1.3.0", "description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.", "main": "index.mjs", "type": "module", @@ -37,6 +37,7 @@ }, "dependencies": { "@kabelsalat/lib": "^0.4.1", + "@kabelsalat/web": "^0.4.1", "nanostores": "^0.11.3" }, "engines": { diff --git a/packages/superdough/reverbGen.mjs b/packages/superdough/reverbGen.mjs index 0949a30d..1a14718e 100644 --- a/packages/superdough/reverbGen.mjs +++ b/packages/superdough/reverbGen.mjs @@ -18,6 +18,7 @@ var reverbGen = {}; /** Generates a reverb impulse response. + @tags internals @param {!Object} params TODO: Document the properties. @param {!function(!AudioBuffer)} callback Function to call when the impulse response has been generated. The impulse response @@ -50,7 +51,7 @@ reverbGen.generateReverb = function (params, callback) { /** Creates a canvas element showing a graph of the given data. - + @tags internals @param {!Float32Array} data An array of numbers, or a Float32Array. @param {number} width Width in pixels of the canvas. @param {number} height Height in pixels of the canvas. @@ -81,7 +82,9 @@ reverbGen.generateGraph = function (data, width, height, min, max) { @param {number} lpFreqEnd @param {number} lpFreqEndAt @param {!function(!AudioBuffer)} callback May be called - immediately within the current execution context, or later.*/ + immediately within the current execution context, or later. + @tags internals + */ var applyGradualLowpass = function (input, lpFreqStart, lpFreqEnd, lpFreqEndAt, callback) { if (lpFreqStart == 0) { callback(input); diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 25c34c64..d45e1050 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,5 +1,5 @@ import { getBaseURL, getCommonSampleInfo } from './util.mjs'; -import { registerSound, registerWaveTable } from './index.mjs'; +import { registerSound, registerWaveTable, soundMap } from './index.mjs'; import { getAudioContext } from './audioContext.mjs'; import { getADSRValues, @@ -30,6 +30,29 @@ function humanFileSize(bytes, si) { return bytes.toFixed(1) + ' ' + units[u]; } +/** + * Returns the duration, in seconds, of the given sample. + * Has optional param `n` (for instance, the `2` in `s("casio:2")`) + * + * Note: `must` be called with await, otherwise you'll get a pending Promise object. + * + * @name getDuration,getDur + * @tag samples + * @param {string} sampleName + * @param {number} (optional) n + * + * @example + * // Set a patterns cycle length to exactly the length of the sample + * samples('github:tidalcycles/dirt-samples') + * let k = await getDuration('sax') + * s("sax").cps(1/k) + */ +export const getDuration = (s, n = 0) => { + return getSampleBufferSource({ s, n }, soundMap.get(s)[s].data.samples).then((x) => x.bufferDuration); +}; + +export const getDur = getDuration; + export function getSampleInfo(hapValue, bank) { const { speed = 1.0 } = hapValue; const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank); @@ -235,6 +258,7 @@ export async function fetchSampleMap(url) { /** * Loads a collection of samples to use with `s` + * @tags samples * @example * samples('github:tidalcycles/dirt-samples'); * s("[bd ~]*2, [~ hh]*2, ~ sd") diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index ec44e8af..98d1fb67 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -25,7 +25,7 @@ import { import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { connectLFO, connectEnvelope, connectBusModulator } from './modulators.mjs'; -import { loadBuffer } from './sampler.mjs'; +import { getSampleBufferSource, loadBuffer } from './sampler.mjs'; import { getAudioContext } from './audioContext.mjs'; import { SuperdoughAudioController } from './superdoughoutput.mjs'; import { resetSeenKeys } from './wavetable.mjs'; @@ -40,6 +40,7 @@ export let maxPolyphony = DEFAULT_MAX_POLYPHONY; * start to die out in first-in-first-out order once the max polyphony has been hit * * @name setMaxPolyphony + * @tags superdough * @param {number} Max polyphony. Defaults to 128 * @example * setMaxPolyphony(4) @@ -73,6 +74,7 @@ export function applyGainCurve(val) { * quadratic, exponential, etc. rather than linear * * @name setGainCurve + * @tags amplitude, superdough * @param {Function} function to apply to all gain values * @example * setGainCurve((x) => x * x) // quadratic gain @@ -128,6 +130,8 @@ async function aliasBankPath(path) { * Optionally accepts a single argument string of a path to a JSON file containing bank aliases. * @param {string} bank - The bank to alias * @param {string} alias - The alias to use for the bank + * + * @tags samples */ export async function aliasBank(...args) { switch (args.length) { @@ -146,6 +150,7 @@ export async function aliasBank(...args) { /** * Register an alias for a sound. + * @tags samples * @param {string} original - The original sound name * @param {string} alias - The alias to use for the sound */ @@ -255,6 +260,14 @@ export function loadWorklets() { return workletsLoading; } +let kabel; +async function initKabelsalat() { + const { SalatRepl } = await import('@kabelsalat/web'); + logger('[kabelsalat] ready'); + kabel = new SalatRepl({ localScope: true }); + return kabel; +} + // this function should be called on first user interaction (to avoid console warning) export async function initAudio(options = {}) { const { @@ -301,6 +314,7 @@ export async function initAudio(options = {}) { } catch (err) { console.warn('could not load AudioWorklet effects', err); } + await initKabelsalat(); logger('[superdough] ready'); } let audioReady; @@ -436,6 +450,14 @@ class Chain { } } +const compileKabel = (code) => { + if (!kabel) { + throw new Error('kabelsalat not loaded'); + } + const node = kabel.evaluate(code); + return node.compile({ log: false }); +}; + export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { // mapping from main FX and numbered FX chains to nodes const nodes = { main: {} }; @@ -613,7 +635,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // Kabelsalat if (fx.workletSrc !== undefined) { - const workletNode = getWorklet(ac, 'generic-processor', {}); + const workletNode = getWorklet(ac, 'generic-processor', {}, { outputChannelCount: [2] }); chain.connect(workletNode); const workletSrc = fx.workletSrc .replace(/\bpat\[(\d+)\]/g, (_, i) => fx.workletInputs[i]) diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 48830541..caa58fa8 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -110,7 +110,9 @@ export function getCommonSampleInfo(hapValue, bank) { return { transpose, url, index, midi, label }; } -/** Selects entries from `source` and renames them via `map` */ +/** Selects entries from `source` and renames them via `map` + * @tags internals + */ export const pickAndRename = (source, map) => { return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]])); }; diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 6ef1f0eb..9c2974fd 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -186,6 +186,7 @@ export function registerWaveTable(key, tables, params) { * Loads a collection of wavetables to use with `s` * * @name tables + * @tags wavetable */ export const tables = async (url, frameLen, json, options = {}) => { if (json !== undefined) return _processTables(json, url, frameLen); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 6969ee2c..6afd7a0a 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -214,7 +214,7 @@ class CoarseProcessor extends AudioWorkletProcessor { coarse = Math.max(1, coarse); for (let n = 0; n < blockSize; n++) { for (let i = 0; i < input.length; i++) { - output[i][n] = n % coarse === 0 ? input[i][n] : output[i][n - 1]; + output[i][n] = n % coarse < 1 ? input[i][n] : output[i][n - 1]; } } return true; @@ -646,14 +646,18 @@ class PhaseVocoderProcessor extends OLAProcessor { this.timeCursor += this.hopSize; } - /** Apply Hann window in-place */ + /** Apply Hann window in-place + * @tags internals + */ applyHannWindow(input) { for (let i = 0; i < this.blockSize; i++) { input[i] *= this.hannWindow[i] * 1.62; } } - /** Compute squared magnitudes for peak finding **/ + /** Compute squared magnitudes for peak finding + * @tags internals + **/ computeMagnitudes() { let i = 0, j = 0; @@ -667,7 +671,9 @@ class PhaseVocoderProcessor extends OLAProcessor { } } - /** Find peaks in spectrum magnitudes **/ + /** Find peaks in spectrum magnitudes + * @tags internals + **/ findPeaks() { this.nbPeaks = 0; let i = 2; @@ -688,7 +694,9 @@ class PhaseVocoderProcessor extends OLAProcessor { } } - /** Shift peaks and regions of influence by pitchFactor into new specturm */ + /** Shift peaks and regions of influence by pitchFactor into new specturm + * @tags internals + */ shiftPeaks(pitchFactor) { // zero-fill new spectrum this.freqComplexBufferShifted.fill(0); @@ -841,7 +849,9 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor { registerProcessor('pulse-oscillator', PulseOscillatorProcessor); -/** BYTE BEATS */ +/** BYTE BEATS + * @tags internals + */ const chyx = { /*bit*/ bitC: function (x, y, z) { return x & y ? z : 0; @@ -1540,12 +1550,20 @@ class GenericProcessor extends AudioWorkletProcessor { this.gateNode?.setValue(0); this.gateEnded = true; } - const outL = outputs[0][0]; - const outR = outputs[0][1] ?? outputs[0][0]; + const output = outputs[0]; + const outL = output[0]; + const outR = output[1]; for (let n = 0; n < blockSize; n++) { this.genSample(this.playPos, this.nodes, input ? input[n] : 0, this.registers, this.outputs, this.sources); - outL[n] = this.outputs[0]; - outR[n] = this.outputs[1]; + const left = this.outputs[0]; + const right = this.outputs[1]; + // Spread to stereo if possible; else mixdown to mono + if (outR) { + outL[n] = left; + outR[n] = right; + } else { + outL[n] = 0.5 * (left + right); + } this.playPos += 1 / sampleRate; } return true; diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs index 7231792c..f7eae6ef 100644 --- a/packages/supradough/dough.mjs +++ b/packages/supradough/dough.mjs @@ -19,6 +19,7 @@ function applyGainCurve(val) { * @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) { diff --git a/packages/tonal/package.json b/packages/tonal/package.json index 14ffabc7..a6a539c9 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/tonal", - "version": "1.2.5", + "version": "1.2.6", "description": "Tonal functions for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 951f0f7e..4bba4561 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -100,6 +100,7 @@ function scaleOffset(scale, offset, note) { * - 5P = perfect fifth * - 5d = diminished fifth * + * @tags tonal * @param {string | number} amount Either number of semitones or interval string. * @returns Pattern * @memberof Pattern @@ -154,6 +155,7 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr * * @memberof Pattern * @name scaleTranspose + * @tags tonal * @param {offset} offset number of steps inside the scale * @returns Pattern * @synonyms scaleTrans, strans @@ -244,6 +246,7 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) { * The root note defaults to octave 3, if no octave number is given. * * @name scale + * @tags tonal * @param {string} scale Name of scale * @returns Pattern * @example diff --git a/packages/tonal/voicings.mjs b/packages/tonal/voicings.mjs index d81911e0..7dcc6a77 100644 --- a/packages/tonal/voicings.mjs +++ b/packages/tonal/voicings.mjs @@ -90,6 +90,7 @@ export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistr * Adds a new custom voicing dictionary. * * @name addVoicings + * @tags tonal * @memberof Pattern * @param {string} name identifier for the voicing dictionary * @param {Object} dictionary maps chord symbol to possible voicings @@ -133,6 +134,7 @@ const getVoicing = (chord, dictionaryName, lastVoicing) => { * Uses [chord-voicings package](https://github.com/felixroos/chord-voicings#chord-voicings). * * @name voicings + * @tags tonal * @memberof Pattern * @param {string} dictionary which voicing dictionary to use. * @returns Pattern @@ -157,6 +159,7 @@ export const voicings = register('voicings', function (dictionary, pat) { * Maps the chords of the incoming pattern to root notes in the given octave. * * @name rootNotes + * @tags tonal * @memberof Pattern * @param {octave} octave octave to use * @returns Pattern @@ -189,6 +192,7 @@ export const rootNotes = register('rootNotes', function (octave, pat) { * If you pass a pattern of strings to voicing, they will be interpreted as chords. * * @name voicing + * @tags tonal * @returns Pattern * @example * n("0 1 2 3").chord("").voicing() diff --git a/packages/transpiler/README.md b/packages/transpiler/README.md index 9914174a..44519f6a 100644 --- a/packages/transpiler/README.md +++ b/packages/transpiler/README.md @@ -1,10 +1,31 @@ # @strudel/transpiler -This package contains a JS code transpiler with the following features: +This package contains a JS code transpiler with a plugin system that can be used to transform the code. + +A plugin can be registered via a `registerTranspilerPlugin` call : + +```js +registerTranspilerPlugin({ + walk: (context) => ({ + enter: function(node, parent, prop, index) {}, + leave: function(node, parent, prop, index) {} + }) +}) +``` + +where the enter/leave functions (both optional) follow the `estree-walker` walk API. + +4 plugins are currently hosted inside `@strudel/transpiler` + +- mini: add locations of mini notation strings (double quoted or backticked) for highlighting +- widgets: add handling of sliders & draw widgets +- sample: make it possible to call a sample without await +- kabelsalat: transform the code to handle the `K(..)` kabelsalat notation + +it also -- add locations of mini notation strings (double quoted or backticked) for highlighting -- converts pseudo note variables to note strings - adds return statement to the last expression +- handles label capturing for block-based eval ## Install diff --git a/packages/transpiler/index.mjs b/packages/transpiler/index.mjs index 484c3e70..3baf1593 100644 --- a/packages/transpiler/index.mjs +++ b/packages/transpiler/index.mjs @@ -1,5 +1,17 @@ +/* +index.mjs - +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ import { evaluate as _evaluate } from '@strudel/core'; import { transpiler } from './transpiler.mjs'; + export * from './transpiler.mjs'; -export const evaluate = (code) => _evaluate(code, transpiler); +import './plugin-kabelsalat.mjs'; +import './plugin-mini.mjs'; +import './plugin-sample.mjs'; +import './plugin-widgets.mjs'; + +export { registerWidgetType, getWidgetID } from './plugin-widgets.mjs'; +export const evaluate = (code, transpilerOptions) => _evaluate(code, transpiler, transpilerOptions); diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 51355044..7b2a053f 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/transpiler", - "version": "1.2.5", + "version": "1.2.6", "description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.", "main": "index.mjs", "type": "module", diff --git a/packages/transpiler/plugin-kabelsalat.mjs b/packages/transpiler/plugin-kabelsalat.mjs new file mode 100644 index 00000000..69b8c453 --- /dev/null +++ b/packages/transpiler/plugin-kabelsalat.mjs @@ -0,0 +1,235 @@ +/* +plugin-kabelsalat.mjs - +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +import { walk } from 'estree-walker'; +import escodegen from 'escodegen'; +import { getLanguages, registerTranspilerPlugin } from './transpiler.mjs'; + +export function genExprSource(expr) { + return escodegen.generate(expr, { format: { semicolons: false } }); +} + +function isStrudelPatternWrap(node) { + if (node.type !== 'CallExpression') { + return false; + } + const callee = node.callee; + if (callee.type === 'Identifier') { + return callee.name === 'S'; + } + if (callee.type === 'MemberExpression' && !callee.computed) { + return callee.property?.name === 'S'; + } + return false; +} + +// Used to identify transpiled `m(...)` calls for proper conversion +// to, say, kabelsalat placeholders +function isMiniCall(node) { + if (node.type !== 'CallExpression') { + return false; + } + const callee = node.callee; + if (callee.type !== 'Identifier') { + return false; + } + if (callee.name !== getMinilangName()) { + return false; + } + const firstArg = node.arguments?.[0]; + return firstArg?.type === 'Literal' && typeof firstArg.value === 'string'; +} + +function getMinilangName() { + const minilang = getLanguages().get('minilang'); + return minilang?.name || 'm'; +} + +function replaceNode(node, replacement, parents, currentRoot) { + const info = parents.get(node); + if (!info || !info.parent) { + return replacement; + } + + const { parent, prop, index } = info; + if (Array.isArray(parent[prop])) { + parent[prop][index] = replacement; + } else { + parent[prop] = replacement; + } + parents.set(replacement, { parent, prop, index }); + return currentRoot; +} + +// If `start` is available, we use it. If it's already been transpiled +// to `m(...)`, use the provided offset +function getPatternNodeOrder(node) { + if (typeof node.start === 'number') { + return node.start; + } + if (isMiniCall(node)) { + const offsetArg = node.arguments?.[1]; + if (offsetArg?.type === 'Literal' && typeof offsetArg.value === 'number') { + return offsetArg.value; + } + } + return 0; +} + +function placeholderAst(index) { + return { + type: 'MemberExpression', + object: { type: 'Identifier', name: 'pat' }, + property: { type: 'Literal', value: index }, + computed: true, + optional: false, + }; +} + +function getStrudelPatternExpr(node) { + if (isStrudelPatternWrap(node)) { + const arg = node.arguments?.[0]; + if (!arg) { + throw new Error('S(...) requires an argument'); + } + return arg; + } + if (isMiniCall(node)) { + return node; + } + return null; +} + +function cloneNode(node) { + return JSON.parse(JSON.stringify(node)); +} + +export function extractPatternPlaceholders(expr) { + const templateExpr = cloneNode(expr); + const parents = new Map(); + const targets = []; + + walk(templateExpr, { + enter(node, parent, prop, index) { + parents.set(node, { parent, prop, index }); + const patternExpr = getStrudelPatternExpr(node); + if (patternExpr) { + targets.push({ node, patternExpr }); + this.skip(); + } + }, + }); + + if (!targets.length) { + return { template: genExprSource(templateExpr), patternExprs: [] }; + } + + targets.sort((a, b) => getPatternNodeOrder(a.node) - getPatternNodeOrder(b.node)); + + const patternExprs = targets.map(({ patternExpr }) => cloneNode(patternExpr)); + + let currentExpr = templateExpr; + targets.forEach(({ node }, index) => { + currentExpr = replaceNode(node, placeholderAst(index), parents, currentExpr); + }); + + const template = genExprSource(currentExpr); + return { template, patternExprs }; +} + +const transpilerPlugin = { + walk: (context) => ({ + leave: function (node, parent, prop, index) { + if (!isKabelCall(node)) return; + let [expr, ...rest] = node.arguments; + if (!expr) throw new Error('K(...) requires an expression'); + if (shouldCallKabelExpression(expr)) { + expr = { + type: 'CallExpression', + callee: expr, + arguments: [], + optional: false, + }; + } + const language = 'kabelsalat'; + const { template, patternExprs } = extractPatternPlaceholders(expr); + if (patternExprs.length) { + const workletArgs = [ + /*{ type: 'Literal', value: language },*/ + { type: 'Literal', value: template }, + ...patternExprs, + ...rest, + ]; + let callee = node.callee; + if (callee.type === 'ChainExpression') callee = callee.expression; + if (callee.type === 'MemberExpression') { + return this.replace({ + type: 'CallExpression', + callee: workletMemberAst(callee.object), + arguments: workletArgs, + optional: false, + }); + } + return this.replace({ + type: 'CallExpression', + callee: { type: 'Identifier', name: 'worklet' }, + arguments: workletArgs, + optional: false, + }); + } + + const kabelSrc = genExprSource(expr); + const workletArgs = [/*{ type: 'Literal', value: language },*/ { type: 'Literal', value: kabelSrc }, ...rest]; + + let callee = node.callee; + if (callee.type === 'ChainExpression') callee = callee.expression; + if (callee.type === 'MemberExpression') { + return this.replace({ + type: 'CallExpression', + callee: workletMemberAst(callee.object), + arguments: workletArgs, + optional: false, + }); + } + return this.replace({ + type: 'CallExpression', + callee: { type: 'Identifier', name: 'worklet' }, + arguments: workletArgs, + optional: false, + }); + }, + }), +}; + +registerTranspilerPlugin(transpilerPlugin); + +function isKabelCall(node) { + if (node.type !== 'CallExpression') return false; + let callee = node.callee; + if (callee.type === 'ChainExpression') callee = callee.expression; + if (callee.type === 'MemberExpression') return !callee.computed && callee.property?.name === 'K'; + return callee.type === 'Identifier' && callee.name === 'K'; +} + +function shouldCallKabelExpression(expr) { + if (expr.type !== 'ArrowFunctionExpression' && expr.type !== 'FunctionExpression') { + return false; + } + if (expr.params.length) { + return false; + } + return expr.body?.type === 'BlockStatement'; +} + +function workletMemberAst(objectExpr) { + return { + type: 'MemberExpression', + object: objectExpr, + property: { type: 'Identifier', name: 'worklet' }, + computed: false, + optional: false, + }; +} diff --git a/packages/transpiler/plugin-mini.mjs b/packages/transpiler/plugin-mini.mjs new file mode 100644 index 00000000..0653ffef --- /dev/null +++ b/packages/transpiler/plugin-mini.mjs @@ -0,0 +1,200 @@ +/* +plugin-mini.mjs - +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ +import { getLeafLocations } from '@strudel/mini'; +import { getLanguages, registerTranspilerPlugin } from './transpiler.mjs'; + +const languageLiteral = { + walk: (context) => ({ + enter: function (node, parent, prop, index) { + if (!isLanguageLiteral(node)) return; + context.miniLocations ??= []; + const { options, miniLocations } = context; + const { emitMiniLocations } = options; + const { name } = node.tag; + const language = getLanguages().get(name); + const code = node.quasi.quasis[0].value.raw; + const offset = node.quasi.start + 1; + if (emitMiniLocations) { + const locs = language.getLocations(code, offset); + miniLocations.push(...locs); + } + this.skip(); + return this.replace(languageWithLocation(name, code, offset)); + }, + }), +}; + +const tidal = { + walk: (context) => ({ + enter: function (node, parent, prop, index) { + if (!isTemplateLiteral(node, 'tidal')) return; + context.miniLocations ??= []; + const { options, miniLocations } = context; + const { emitMiniLocations } = options; + const raw = node.quasi.quasis[0].value.raw; + const offset = node.quasi.start + 1; + if (emitMiniLocations) { + const stringLocs = collectHaskellMiniLocations(raw, offset); + miniLocations.push(...stringLocs); + } + this.skip(); + return this.replace(tidalWithLocation(raw, offset)); + }, + }), +}; + +const backtick = { + walk: (context) => ({ + enter: function (node, parent, prop, index) { + if (!isBackTickString(node, parent)) return; + context.miniLocations ??= []; + const { options, input, miniDisableRanges, miniLocations } = context; + const { emitMiniLocations } = options; + if (isMiniDisabled(node.start, miniDisableRanges)) { + return; + } + const { quasis } = node; + const { raw } = quasis[0].value; + this.skip(); + emitMiniLocations && collectMiniLocations(raw, node, miniLocations, input); + return this.replace(miniWithLocation(raw, node)); + }, + }), +}; + +const doublequotes = { + walk: (context) => ({ + enter: function (node, parent, prop, index) { + if (!isStringWithDoubleQuotes(node)) return; + context.miniLocations ??= []; + const { options, input, miniDisableRanges, miniLocations } = context; + const { emitMiniLocations } = options; + if (isMiniDisabled(node.start, miniDisableRanges)) { + return; + } + const { value } = node; + this.skip(); + emitMiniLocations && collectMiniLocations(value, node, miniLocations, input); + return this.replace(miniWithLocation(value, node)); + }, + }), +}; + +function isLanguageLiteral(node) { + return node.type === 'TaggedTemplateExpression' && getLanguages().has(node.tag.name); +} + +function languageWithLocation(name, value, offset) { + return { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: name, + }, + arguments: [ + { type: 'Literal', value }, + { type: 'Literal', value: offset }, + ], + optional: false, + }; +} + +function collectHaskellMiniLocations(haskellCode, offset) { + return haskellCode + .split('') + .reduce((acc, char, i) => { + if (char !== '"') { + return acc; + } + if (!acc.length || acc[acc.length - 1].length > 1) { + acc.push([i + 1]); + } else { + acc[acc.length - 1].push(i); + } + return acc; + }, []) + .map(([start, end]) => { + const miniString = haskellCode.slice(start, end); + return getLeafLocations(`"${miniString}"`, offset + start - 1); + }) + .flat(); +} + +function isTemplateLiteral(node, value) { + return node.type === 'TaggedTemplateExpression' && node.tag.name === value; +} + +function tidalWithLocation(value, offset) { + return { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'tidal', + }, + arguments: [ + { type: 'Literal', value }, + { type: 'Literal', value: offset }, + ], + optional: false, + }; +} + +function isBackTickString(node, parent) { + return node.type === 'TemplateLiteral' && parent.type !== 'TaggedTemplateExpression'; +} + +function isMiniDisabled(offset, miniDisableRanges) { + for (const [start, end] of miniDisableRanges) { + if (offset >= start && offset < end) { + return true; + } + } + return false; +} + +function miniWithLocation(value, node) { + const { start: fromOffset } = node; + + const minilang = getLanguages().get('minilang'); + let name = 'm'; + if (minilang && minilang.name) { + name = minilang.name; // name is expected to be exported from the package of the minilang + } + + return { + type: 'CallExpression', + callee: { + type: 'Identifier', + name, + }, + arguments: [ + { type: 'Literal', value }, + { type: 'Literal', value: fromOffset }, + ], + optional: false, + }; +} + +const collectMiniLocations = (value, node, miniLocations, input) => { + const minilang = getLanguages().get('minilang'); + if (minilang) { + const code = `[${value}]`; + const locs = minilang.getLocations(code, node.start); + miniLocations.push(...locs); + } else { + const leafLocs = getLeafLocations(`"${value}"`, node.start, input); + miniLocations.push(...leafLocs); + } +}; + +function isStringWithDoubleQuotes(node, locations, code) { + if (node.type !== 'Literal') { + return false; + } + return node.raw[0] === '"'; +} + +registerTranspilerPlugin([languageLiteral, tidal, backtick, doublequotes]); diff --git a/packages/transpiler/plugin-sample.mjs b/packages/transpiler/plugin-sample.mjs new file mode 100644 index 00000000..0b129af0 --- /dev/null +++ b/packages/transpiler/plugin-sample.mjs @@ -0,0 +1,29 @@ +/* +plugin-sample.mjs - +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +import { registerTranspilerPlugin } from './transpiler.mjs'; + +export function withAwait(node) { + return { + type: 'AwaitExpression', + argument: node, + }; +} + +const bareSample = { + walk: (context) => ({ + enter: function (node, parent, prop, index) { + if (!isBareSamplesCall(node, parent)) return; + this.replace(withAwait(node)); + }, + }), +}; + +function isBareSamplesCall(node, parent) { + return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression'; +} + +registerTranspilerPlugin(bareSample); diff --git a/packages/transpiler/plugin-widgets.mjs b/packages/transpiler/plugin-widgets.mjs new file mode 100644 index 00000000..a6a46eb3 --- /dev/null +++ b/packages/transpiler/plugin-widgets.mjs @@ -0,0 +1,128 @@ +/* +plugin-widgets.mjs - +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +import { registerTranspilerPlugin } from './transpiler.mjs'; + +let widgetMethods = []; +export function registerWidgetType(type) { + widgetMethods.push(type); +} + +const widgetTranspilerPlugin = { + walk: (context) => ({ + enter: function (node, parent, prop, index) { + if (!isWidgetMethod(node)) return; + context.widgets ??= []; + const { widgets, options } = context; + const { emitWidgets } = options; + const type = node.callee.property.name; + const idx = widgets.filter((w) => w.type === type).length; + const widgetConfig = { + from: node.start, + to: node.end, + index: idx, + type, + id: options.id, + }; + emitWidgets && widgets.push(widgetConfig); + return this.replace(widgetWithLocation(node, widgetConfig)); + }, + }), +}; + +const sliderTranspilerPlugin = { + walk: (context) => ({ + enter: function (node, parent, prop, index) { + if (!isSliderFunction(node)) return; + context.widgets ??= []; + context.sliders ??= []; + const { options, widgets, sliders, nodeOffset } = 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, + 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)); + }, + }), +}; + +export const widgetTranspilerPlugins = [sliderTranspilerPlugin, widgetTranspilerPlugin]; + +// these functions are connected to @strudel/codemirror -> slider.mjs +// maybe someday there will be pluggable transpiler functions, then move this there +function isSliderFunction(node) { + return node.type === 'CallExpression' && node.callee.name === 'slider'; +} + +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}`; + + // add loc as identifier to first argument + // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) + node.arguments.unshift({ + type: 'Literal', + value: id, + raw: id, + }); + node.callee.name = 'sliderWithID'; + return node; +} + +export function getWidgetID(widgetConfig) { + // the widget id is used as id for the dom element + as key for eventual resources + // for example, for each scope widget, a new analyser + buffer (large) is created + // Update: use range-based ID generation for better stability during block evaluation + // When we have both from and to, use them together for stability + // Otherwise fall back to position-based ID for backward compatibility + let uniqueIdentifier; + if (widgetConfig.from !== undefined && widgetConfig.to !== undefined) { + // Use range for more stable identification + uniqueIdentifier = `${widgetConfig.from}-${widgetConfig.to}`; + } else { + // Fallback to single position (for backward compatibility) + uniqueIdentifier = widgetConfig.to || widgetConfig.from || 0; + } + const baseId = `${widgetConfig.id || ''}_widget_${widgetConfig.type}`; + return `${baseId}_${widgetConfig.index}_${uniqueIdentifier}`; +} + +function widgetWithLocation(node, widgetConfig) { + const id = getWidgetID(widgetConfig); + // Store the unique ID back into the config so it's available for widget management + // This is critical for block-based evaluation to match existing widgets with new ones + widgetConfig.id = id; + // add loc as identifier to first argument + // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) + node.arguments.unshift({ + type: 'Literal', + value: id, + raw: id, + }); + return node; +} + +registerTranspilerPlugin(widgetTranspilerPlugins); diff --git a/packages/transpiler/test/transpiler.test.mjs b/packages/transpiler/test/transpiler.test.mjs index 986635be..74bfaa91 100644 --- a/packages/transpiler/test/transpiler.test.mjs +++ b/packages/transpiler/test/transpiler.test.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import { describe, it, expect } from 'vitest'; -import { transpiler } from '../transpiler.mjs'; +import { transpiler } from '../index.mjs'; const simple = { wrapAsync: false, addReturn: false, simpleLocs: true }; diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 45f9dabd..142f17c7 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -1,13 +1,12 @@ -import { getLeafLocations } from '@strudel/mini'; +/* +transpiler.mjs - +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ import { parse } from 'acorn'; import escodegen from 'escodegen'; import { walk } from 'estree-walker'; -let widgetMethods = []; -export function registerWidgetType(type) { - widgetMethods.push(type); -} - let languages = new Map(); // config = { getLocations: (code: string, offset?: number) => number[][] } // see mondough.mjs for example use @@ -17,9 +16,31 @@ let languages = new Map(); export function registerLanguage(type, config) { languages.set(type, config); } +export function getLanguages() { + return languages; +} + +const plugins = []; + +export function registerTranspilerPlugin(plugin) { + plugins.push(plugin); +} +export function getPlugins() { + return plugins.flat(Infinity); +} export function transpiler(input, options = {}) { - const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options; + options = { + wrapAsync: false, + addReturn: true, + emitMiniLocations: true, + emitWidgets: true, + blockBased: false, + range: [], + ...options, + }; + + const { wrapAsync, addReturn, emitMiniLocations, emitWidgets, blockBased, range } = options; const comments = []; let ast = parse(input, { @@ -29,169 +50,103 @@ export function transpiler(input, options = {}) { onComment: comments, }); - const miniDisableRanges = findMiniDisableRanges(comments, input.length); - let miniLocations = []; - const collectMiniLocations = (value, node) => { - const minilang = languages.get('minilang'); - if (minilang) { - const code = `[${value}]`; - const locs = minilang.getLocations(code, node.start); - miniLocations = miniLocations.concat(locs); - } else { - const leafLocs = getLeafLocations(`"${value}"`, node.start, input); - miniLocations = miniLocations.concat(leafLocs); - } - }; - let widgets = []; + let miniDisableRanges = findMiniDisableRanges(comments, input.length); + + // 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 = []; + + let labels = []; + + const context = { options, input, nodeOffset, miniDisableRanges, labels }; + const plugins = getPlugins().map((plugin) => plugin.walk?.(context)); walk(ast, { - enter(node, parent /* , prop, index */) { - if (isLanguageLiteral(node)) { - const { name } = node.tag; - const language = languages.get(name); - const code = node.quasi.quasis[0].value.raw; - const offset = node.quasi.start + 1; - if (emitMiniLocations) { - const locs = language.getLocations(code, offset); - miniLocations = miniLocations.concat(locs); + 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); } - this.skip(); - return this.replace(languageWithLocation(name, code, offset)); } - if (isTemplateLiteral(node, 'tidal')) { - const raw = node.quasi.quasis[0].value.raw; - const offset = node.quasi.start + 1; - if (emitMiniLocations) { - const stringLocs = collectHaskellMiniLocations(raw, offset); - miniLocations = miniLocations.concat(stringLocs); - } - this.skip(); - return this.replace(tidalWithLocation(raw, offset)); - } - if (isBackTickString(node, parent)) { - if (isMiniDisabled(node.start, miniDisableRanges)) { - return; - } - const { quasis } = node; - const { raw } = quasis[0].value; - this.skip(); - emitMiniLocations && collectMiniLocations(raw, node); - return this.replace(miniWithLocation(raw, node)); - } - if (isStringWithDoubleQuotes(node)) { - if (isMiniDisabled(node.start, miniDisableRanges)) { - return; - } - const { value } = node; - this.skip(); - emitMiniLocations && collectMiniLocations(value, node); - return this.replace(miniWithLocation(value, node)); - } - if (isSliderFunction(node)) { - emitWidgets && - widgets.push({ - from: node.arguments[0].start, - to: node.arguments[0].end, - value: node.arguments[0].raw, // don't use value! - min: node.arguments[1]?.value ?? 0, - max: node.arguments[2]?.value ?? 1, - step: node.arguments[3]?.value, - type: 'slider', - }); - return this.replace(sliderWithLocation(node)); - } - if (isWidgetMethod(node)) { - const type = node.callee.property.name; - const index = widgets.filter((w) => w.type === type).length; - const widgetConfig = { - to: node.end, - index, - type, - id: options.id, - }; - emitWidgets && widgets.push(widgetConfig); - return this.replace(widgetWithLocation(node, widgetConfig)); - } - if (isBareSamplesCall(node, parent)) { - return this.replace(withAwait(node)); + + for (const plugin of plugins) { + if (!plugin?.enter?.call(this, node, parent, prop, index)) continue; + return; } + 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) { - if (!isKabelCall(node)) return; - - let [expr, ...rest] = node.arguments; - if (!expr) throw new Error('K(...) requires an expression'); - - if (shouldCallKabelExpression(expr)) { - expr = { - type: 'CallExpression', - callee: expr, - arguments: [], - optional: false, - }; + for (const plugin of plugins) { + if (!plugin?.leave?.call(this, node, parent, prop, index)) continue; + return; } - - const { template, patternExprs } = extractPatternPlaceholders(expr); - if (patternExprs.length) { - const workletArgs = [{ type: 'Literal', value: template }, ...patternExprs, ...rest]; - - let callee = node.callee; - if (callee.type === 'ChainExpression') callee = callee.expression; - if (callee.type === 'MemberExpression') { - return this.replace({ - type: 'CallExpression', - callee: workletMemberAst(callee.object), - arguments: workletArgs, - optional: false, - }); - } - return this.replace({ - type: 'CallExpression', - callee: { type: 'Identifier', name: 'worklet' }, - arguments: workletArgs, - optional: false, - }); - } - - const kabelSrc = genExprSource(expr); - const workletArgs = [{ type: 'Literal', value: kabelSrc }, ...rest]; - - let callee = node.callee; - if (callee.type === 'ChainExpression') callee = callee.expression; - if (callee.type === 'MemberExpression') { - return this.replace({ - type: 'CallExpression', - callee: workletMemberAst(callee.object), - arguments: workletArgs, - optional: false, - }); - } - return this.replace({ - type: 'CallExpression', - callee: { type: 'Identifier', name: 'worklet' }, - arguments: workletArgs, - optional: false, - }); }, }); let { body } = ast; + const silenceExpression = { + type: 'ExpressionStatement', + expression: { + type: 'Identifier', + name: 'silence', + }, + }; + if (!body.length) { console.warn('empty body -> fallback to silence'); - body.push({ - type: 'ExpressionStatement', - expression: { - type: 'Identifier', - name: 'silence', - }, - }); + body.push(silenceExpression); } else if (!body?.[body.length - 1]?.expression) { - throw new Error('unexpected ast format without body expression'); + // Last statement is not an expression (e.g., VariableDeclaration, FunctionDeclaration) + body.push(silenceExpression); + } + + // For block-based eval, add scope assignments before the return statement + // This allows variables/functions defined in one block to be used in other blocks + if (blockBased && scopeDeclarations.length > 0) { + const scopeAssignments = scopeDeclarations.flatMap((name) => createScopeAssignment(name)); + // Insert scope assignments before the last statement (which will become the return) + body.splice(body.length - 1, 0, ...scopeAssignments); } // add return to last statement @@ -209,256 +164,15 @@ export function transpiler(input, options = {}) { if (!emitMiniLocations) { return { output }; } - return { output, miniLocations, widgets }; + + let pluginContext; + ({ options, input, miniDisableRanges, nodeOffset, ...pluginContext } = context); + + return { output, ...pluginContext }; } -function isKabelCall(node) { - if (node.type !== 'CallExpression') return false; - let callee = node.callee; - if (callee.type === 'ChainExpression') callee = callee.expression; - if (callee.type === 'MemberExpression') return !callee.computed && callee.property?.name === 'K'; - return callee.type === 'Identifier' && callee.name === 'K'; -} - -function shouldCallKabelExpression(expr) { - if (expr.type !== 'ArrowFunctionExpression' && expr.type !== 'FunctionExpression') { - return false; - } - if (expr.params.length) { - return false; - } - return expr.body?.type === 'BlockStatement'; -} - -function genExprSource(expr) { - return escodegen.generate(expr, { format: { semicolons: false } }); -} - -function extractPatternPlaceholders(expr) { - const templateExpr = cloneNode(expr); - const parents = new Map(); - const targets = []; - - walk(templateExpr, { - enter(node, parent, prop, index) { - parents.set(node, { parent, prop, index }); - const patternExpr = getStrudelPatternExpr(node); - if (patternExpr) { - targets.push({ node, patternExpr }); - this.skip(); - } - }, - }); - - if (!targets.length) { - return { template: genExprSource(templateExpr), patternExprs: [] }; - } - - targets.sort((a, b) => getPatternNodeOrder(a.node) - getPatternNodeOrder(b.node)); - - const patternExprs = targets.map(({ patternExpr }) => cloneNode(patternExpr)); - - let currentExpr = templateExpr; - targets.forEach(({ node }, index) => { - currentExpr = replaceNode(node, placeholderAst(index), parents, currentExpr); - }); - - const template = genExprSource(currentExpr); - return { template, patternExprs }; -} - -function getStrudelPatternExpr(node) { - if (isStrudelPatternWrap(node)) { - const arg = node.arguments?.[0]; - if (!arg) { - throw new Error('S(...) requires an argument'); - } - return arg; - } - if (isMiniCall(node)) { - return node; - } - return null; -} - -function isStrudelPatternWrap(node) { - if (node.type !== 'CallExpression') { - return false; - } - const callee = node.callee; - if (callee.type === 'Identifier') { - return callee.name === 'S'; - } - if (callee.type === 'MemberExpression' && !callee.computed) { - return callee.property?.name === 'S'; - } - return false; -} - -function getMinilangName() { - const minilang = languages.get('minilang'); - return minilang?.name || 'm'; -} - -// Used to identify transpiled `m(...)` calls for proper conversion -// to, say, kabelsalat placeholders -function isMiniCall(node) { - if (node.type !== 'CallExpression') { - return false; - } - const callee = node.callee; - if (callee.type !== 'Identifier') { - return false; - } - if (callee.name !== getMinilangName()) { - return false; - } - const firstArg = node.arguments?.[0]; - return firstArg?.type === 'Literal' && typeof firstArg.value === 'string'; -} - -// If `start` is available, we use it. If it's already been transpiled -// to `m(...)`, use the provided offset -function getPatternNodeOrder(node) { - if (typeof node.start === 'number') { - return node.start; - } - if (isMiniCall(node)) { - const offsetArg = node.arguments?.[1]; - if (offsetArg?.type === 'Literal' && typeof offsetArg.value === 'number') { - return offsetArg.value; - } - } - return 0; -} - -function placeholderAst(index) { - return { - type: 'MemberExpression', - object: { type: 'Identifier', name: 'pat' }, - property: { type: 'Literal', value: index }, - computed: true, - optional: false, - }; -} - -function replaceNode(node, replacement, parents, currentRoot) { - const info = parents.get(node); - if (!info || !info.parent) { - return replacement; - } - - const { parent, prop, index } = info; - if (Array.isArray(parent[prop])) { - parent[prop][index] = replacement; - } else { - parent[prop] = replacement; - } - parents.set(replacement, { parent, prop, index }); - return currentRoot; -} - -function cloneNode(node) { - return JSON.parse(JSON.stringify(node)); -} - -function workletMemberAst(objectExpr) { - return { - type: 'MemberExpression', - object: objectExpr, - property: { type: 'Identifier', name: 'worklet' }, - computed: false, - optional: false, - }; -} - -function isStringWithDoubleQuotes(node, locations, code) { - if (node.type !== 'Literal') { - return false; - } - return node.raw[0] === '"'; -} - -function isBackTickString(node, parent) { - return node.type === 'TemplateLiteral' && parent.type !== 'TaggedTemplateExpression'; -} - -function miniWithLocation(value, node) { - const { start: fromOffset } = node; - - const minilang = languages.get('minilang'); - let name = 'm'; - if (minilang && minilang.name) { - name = minilang.name; // name is expected to be exported from the package of the minilang - } - - return { - type: 'CallExpression', - callee: { - type: 'Identifier', - name, - }, - arguments: [ - { type: 'Literal', value }, - { type: 'Literal', value: fromOffset }, - ], - optional: false, - }; -} - -// these functions are connected to @strudel/codemirror -> slider.mjs -// maybe someday there will be pluggable transpiler functions, then move this there -function isSliderFunction(node) { - return node.type === 'CallExpression' && node.callee.name === 'slider'; -} - -function isWidgetMethod(node) { - return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name); -} - -function sliderWithLocation(node) { - const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id - // add loc as identifier to first argument - // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) - node.arguments.unshift({ - type: 'Literal', - value: id, - raw: id, - }); - node.callee.name = 'sliderWithID'; - return node; -} - -export function getWidgetID(widgetConfig) { - // the widget id is used as id for the dom element + as key for eventual resources - // for example, for each scope widget, a new analyser + buffer (large) is created - // that means, if we use the index index of line position as id, less garbage is generated - // return `widget_${widgetConfig.to}`; // more gargabe - //return `widget_${widgetConfig.index}_${widgetConfig.to}`; // also more garbage - return `${widgetConfig.id || ''}_widget_${widgetConfig.type}_${widgetConfig.index}`; // less garbage -} - -function widgetWithLocation(node, widgetConfig) { - const id = getWidgetID(widgetConfig); - // add loc as identifier to first argument - // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) - node.arguments.unshift({ - type: 'Literal', - value: id, - raw: id, - }); - return node; -} - -function isBareSamplesCall(node, parent) { - return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression'; -} - -function withAwait(node) { - return { - type: 'AwaitExpression', - argument: node, - }; +function isAllCall(node) { + return node.type === 'CallExpression' && node.callee.name === 'all'; } function isLabelStatement(node) { @@ -491,67 +205,126 @@ function labelToP(node) { }; } -function isLanguageLiteral(node) { - return node.type === 'TaggedTemplateExpression' && languages.has(node.tag.name); +// 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; } -// tidal highlighting -// this feels kind of stupid, when we also know the location inside the string op (tidal.mjs) -// but maybe it's the only way +function findVisualizerInSubtree(node) { + if (!node || typeof node !== 'object') return null; -function isTemplateLiteral(node, value) { - return node.type === 'TaggedTemplateExpression' && node.tag.name === value; -} + // Check if this node is a visualizer call + const viz = isVisualizerCall(node); + if (viz) return viz; -function collectHaskellMiniLocations(haskellCode, offset) { - return haskellCode - .split('') - .reduce((acc, char, i) => { - if (char !== '"') { - return acc; + // 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; } - if (!acc.length || acc[acc.length - 1].length > 1) { - acc.push([i + 1]); - } else { - acc[acc.length - 1].push(i); - } - return acc; - }, []) - .map(([start, end]) => { - const miniString = haskellCode.slice(start, end); - return getLeafLocations(`"${miniString}"`, offset + start - 1); - }) - .flat(); + } else if (child && typeof child === 'object' && child.type) { + const found = findVisualizerInSubtree(child); + if (found) return found; + } + } + return null; } -function tidalWithLocation(value, offset) { - return { - type: 'CallExpression', - callee: { - type: 'Identifier', - name: 'tidal', +// 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, + }, + ], + }, }, - arguments: [ - { type: 'Literal', value }, - { type: 'Literal', value: offset }, - ], - optional: false, - }; -} - -function languageWithLocation(name, value, offset) { - return { - type: 'CallExpression', - callee: { - type: 'Identifier', - name: 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, + }, + }, }, - arguments: [ - { type: 'Literal', value }, - { type: 'Literal', value: offset }, - ], - optional: false, - }; + // 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) { @@ -573,12 +346,3 @@ function findMiniDisableRanges(comments, codeEnd) { } return ranges; } - -function isMiniDisabled(offset, miniDisableRanges) { - for (const [start, end] of miniDisableRanges) { - if (offset >= start && offset < end) { - return true; - } - } - return false; -} diff --git a/packages/vite-plugin-bundle-audioworklet/package.json b/packages/vite-plugin-bundle-audioworklet/package.json index 198a4f9c..524af470 100644 --- a/packages/vite-plugin-bundle-audioworklet/package.json +++ b/packages/vite-plugin-bundle-audioworklet/package.json @@ -1,7 +1,7 @@ { "name": "vite-plugin-bundle-audioworklet", "main": "./vite-plugin-bundle-audioworklet.js", - "version": "0.1.1", + "version": "0.1.2", "description": "", "keywords": [ "vite", diff --git a/packages/web/package.json b/packages/web/package.json index f02d8613..cac1cfca 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/web", - "version": "1.2.6", + "version": "1.3.0", "description": "Easy to setup, opiniated bundle of Strudel for the browser.", "module": "web.mjs", "publishConfig": { @@ -34,6 +34,7 @@ "homepage": "https://codeberg.org/uzu/strudel#readme", "dependencies": { "@strudel/core": "workspace:*", + "@strudel/edo": "workspace:*", "@strudel/mini": "workspace:*", "@strudel/tonal": "workspace:*", "@strudel/transpiler": "workspace:*", diff --git a/packages/web/web.mjs b/packages/web/web.mjs index 2e2df84d..eebe6bdc 100644 --- a/packages/web/web.mjs +++ b/packages/web/web.mjs @@ -3,6 +3,7 @@ export * from '@strudel/webaudio'; //export * from '@strudel/soundfonts'; export * from '@strudel/transpiler'; export * from '@strudel/mini'; +export * from '@strudel/edo'; export * from '@strudel/tonal'; export * from '@strudel/webaudio'; import { Pattern, evalScope, setTime } from '@strudel/core'; @@ -17,6 +18,7 @@ export async function defaultPrebake() { evalScope, import('@strudel/core'), import('@strudel/mini'), + import('@strudel/edo'), import('@strudel/tonal'), import('@strudel/webaudio'), { hush, evaluate }, diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index f0bc4043..48b40470 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/webaudio", - "version": "1.2.6", + "version": "1.3.0", "description": "Web Audio helpers for Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/webaudio/scope.mjs b/packages/webaudio/scope.mjs index e423b20f..866e0a43 100644 --- a/packages/webaudio/scope.mjs +++ b/packages/webaudio/scope.mjs @@ -98,6 +98,7 @@ function clearScreen(smear = 0, smearRGB = `0,0,0`, ctx = getDrawContext()) { /** * Renders an oscilloscope for the frequency domain of the audio signal. * @name fscope + * @tags visualization * @param {string} color line color as hex or color name. defaults to white. * @param {number} scale scales the y-axis. Defaults to 0.25 * @param {number} pos y-position relative to screen height. 0 = top, 1 = bottom of screen @@ -122,6 +123,7 @@ Pattern.prototype.fscope = function (config = {}) { * Renders an oscilloscope for the time domain of the audio signal. * @name scope * @synonyms tscope + * @tags visualization * @param {object} config optional config with options: * @param {boolean} align if 1, the scope will be aligned to the first zero crossing. defaults to 1 * @param {string} color line color as hex or color name. defaults to white. diff --git a/packages/webaudio/spectrum.mjs b/packages/webaudio/spectrum.mjs index 2ddd214f..c67e321b 100644 --- a/packages/webaudio/spectrum.mjs +++ b/packages/webaudio/spectrum.mjs @@ -5,6 +5,7 @@ import { analysers, getAnalyzerData } from 'superdough'; /** * Renders a spectrum analyzer for the incoming audio signal. * @name spectrum + * @tags visualization * @param {object} config optional config with options: * @param {integer} thickness line thickness in px (default 3) * @param {integer} speed scroll speed (default 1) diff --git a/packages/xen/package.json b/packages/xen/package.json index c178c5de..e7d9d203 100644 --- a/packages/xen/package.json +++ b/packages/xen/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/xen", - "version": "1.2.5", + "version": "1.2.6", "description": "Xenharmonic API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/xen/tune.mjs b/packages/xen/tune.mjs index 01303bf5..a070515b 100644 --- a/packages/xen/tune.mjs +++ b/packages/xen/tune.mjs @@ -7,14 +7,51 @@ This program is free software: you can redistribute it and/or modify it under th import Tune from './tunejs.js'; import { register } from '@strudel/core'; +/** + * Assumes pattern contains numerical scale degrees on the `i` control (see examples below). Accepts a scale name or list of frequencies (see all available names at the link on the reference). Returns a new pattern with all values mapped to a frequency ratio. Similar to `xen`. + * @name tune + * @returns Pattern + * @memberof Pattern + * @param {(string | number[] )} scale + * @example + * i("0 1 2 3 4 5").tune("hexany15").mul("220").freq() + * @example + * // You can set your root to be a + * // particular note with getFreq: + * i("4 8 9 10 - - 5 7 9 11 - -").tune("tranh3") + * .mul(getFreq('c3')) + * .freq().clip(.5).room(1) + * @example + * // You can also give tune a list of + * // frequencies to use as the scale: + * i("0 1 2 3 4").tune([ + * 261.6255653006, + * 302.72962012827, + * 350.29154279212, + * 405.32593044476, + * 469.00678383895, + * 523.2511306012 + * ]).mul(220).freq(); + * + * @tags tonal + */ + +// Tune.scale seems to be in ratio format export const tune = register('tune', (scale, pat) => { const tune = new Tune(); if (!tune.isValidScale(scale)) { throw new Error('not a valid tune.js scale name: "' + scale + '". See http://abbernie.github.io/tune/scales.html'); } tune.loadScale(scale); + // if the tonic is a frequency, why are we putting in "1" tune.tonicize(1); return pat.withHap((hap) => { - return hap.withValue(() => tune.note(hap.value)); + if (typeof hap.value !== 'object') { + throw new Error(`Expected hap to have control 'i' set, but received ${hap.value.i}, try wrapping input in i()`); + } + // const { i, ...otherValues } = hap.value; + // hap.value = { ...otherValues, freq: tune.note(i)} + // return hap + return hap.withValue(() => tune.note(hap.value.i)); }); }); diff --git a/packages/xen/tunejs.js b/packages/xen/tunejs.js index 9f425370..e4e27251 100644 --- a/packages/xen/tunejs.js +++ b/packages/xen/tunejs.js @@ -78,14 +78,21 @@ Tune.prototype.frequency = function(stepIn, octaveIn) { } // which scale degree (0 - scale length) is our input + // 60 % 12 = 0 var scaleDegree = stepIn % this.scale.length + // what's this doing + // 0 scaleDegree = 12 + // seems to simply be for a negative result from above, maybe another way to do it, but ok for now while (scaleDegree < 0) { scaleDegree += this.scale.length } + // tonic is currently always 1 + // so this is 1*scale[scaledegree] var freq = this.tonic*this.scale[scaleDegree] + // map it to octave freq = freq*(Math.pow(2,octave)) // truncate irrational numbers @@ -137,10 +144,13 @@ Tune.prototype.MIDI = function(stepIn,octaveIn) { } /* Load a new scale */ +// sets .scale to ratios Tune.prototype.loadScale = function(scale){ /* load the scale */ + let name + if (typeof scale === 'string') name = scale; var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies this.scale = [] for (var i=0;i. */ -import { register, _mod, parseNumeral } from '@strudel/core'; +import { register, _mod, parseNumeral, removeUndefineds } from '@strudel/core'; +import Tune from './tunejs.js'; +// returns a list of frequency ratios for given edo scale export function edo(name) { if (!/^[1-9]+[0-9]*edo$/.test(name)) { throw new Error('not an edo scale: "' + name + '"'); @@ -18,23 +20,33 @@ const presets = { '12ji': [1 / 1, 16 / 15, 9 / 8, 6 / 5, 5 / 4, 4 / 3, 45 / 32, 3 / 2, 8 / 5, 5 / 3, 16 / 9, 15 / 8], }; -function withBase(freq, scale) { +// Given a base frequency such as 220 and an edo scale, returns +// an array of frequencies representing the given edo scale in that base +function _withBase(freq, scale) { return scale.map((r) => r * freq); } const defaultBase = 220; +const isEdo = (scale) => /^[1-9]+[0-9]*edo$/.test(scale); + +// Assumes a base of 220. Returns a filtered scale based on 'indices' +// NOTE: indices functionality is unused function getXenScale(scale, indices) { + let tune = new Tune(); if (typeof scale === 'string') { - if (/^[1-9]+[0-9]*edo$/.test(scale)) { + if (isEdo(scale)) { scale = edo(scale); } else if (presets[scale]) { scale = presets[scale]; + } else if (tune.isValidScale(scale)) { + tune.loadScale(scale); + scale = tune.scale; } else { throw new Error('unknown scale name: "' + scale + '"'); } } - scale = withBase(defaultBase, scale); + scale = _withBase(defaultBase, scale); if (!indices) { return scale; } @@ -47,16 +59,171 @@ function xenOffset(xenScale, offset, index = 0) { return xenScale[i] * Math.pow(2, oct); } +const trimFreq = (freq) => parseFloat(freq.toPrecision(10)); + +// accepts a scale name such as 31edo, and a pattern +// pattern expected to follow format such that a value can be mapped +// to an edostep within the scale. Returns the pattern with +// values mapped to the frequencies associated with the given edosteps // scaleNameOrRatios: string || number[], steps?: number + +/** + * Assumes a numerical pattern of scale steps, and a scale. Scales accepted are all preset scale names of `tune`, arbitrary edos such as 31edo, or an array of frequency ratios. Assumes scales repeat at octave (2/1). Returns a new pattern with all values mapped to their associated frequency, assuming a base frequency of 220hz. + * + * @name xen + * @returns Pattern + * @memberof Pattern + * @param {(string | number[] )} scaleNameOrRatios + * @tags tonal + * @example + * // A minor triad in 31edo: + * i("0 8 18").xen("31edo").piano() + * @example + * // You can also use xen with frequency ratios. + * // This is equivalent to the above: + * i("0 1 2").xen([ + * Math.pow(2, 0/31), + * Math.pow(2, 8/31), + * Math.pow(2, 18/31), + * ]).piano() + * @example + * // xen also supports all scale names that + * // tune does: + * i("0 1 2 3 4 5").xen("hexany15") + * // equiv to: + * // "0 1 2 3 4 5".tune("hexany15").mul("220").freq() + * @example + * i("0 1 2 3 4 5 6 7").xen("<5edo 10edo 15edo hexany15>") + */ + export const xen = register('xen', function (scaleNameOrRatios, pat) { - return pat.withHap((hap) => { - const scale = getXenScale(scaleNameOrRatios); - const frequency = xenOffset(scale, parseNumeral(hap.value)); - return hap.withValue(() => frequency); + return pat.withHaps((haps) => { + haps = haps.map((hap) => { + let hVal = hap.value; + const isObject = typeof hVal === 'object'; + if (!isObject) { + throw new Error(`Expected hap to have control 'i' set, but received ${hap.value.i}, try wrapping input in i()`); + } + const { i, ...otherValues } = hVal; + const scale = getXenScale(scaleNameOrRatios); + let freq = xenOffset(scale, parseNumeral(hVal.i)); + // 10 is somewhat arbitrary + freq = trimFreq(freq); + hap.value = { ...otherValues, freq }; + return isEdo(scaleNameOrRatios) + ? hap.setContext({ ...hap.context, edoSize: scaleNameOrRatios.match(/^([1-9]+[0-9]*)edo$/)[1] }) + : hap; + }); + return removeUndefineds(haps); }); }); -export const tuning = register('tuning', function (ratios, pat) { +/** + * Assumes pattern of frequencies tuned to some `base` frequency, such as the output of `xen` + * Because `xen` defaults to `220Hz`, so will `withBase`. + * but you can specify a different original base with the standard optional array syntax '`:`' + * @name withBase + * @param {number} base + * @param {number} (optional) originalBase + * @tags tonal + * + * @example + * i("[0 1 2 3] [3 4] [4 3 2 1]").xen("hexany23").withBase("<220 [300 200]>") + * @example + * mini([1 / 1, 16 / 15, 9 / 8, 6 / 5, 5 / 4].join(' ')).withBase("220:1") + * // mini([1 / 1, 16 / 15, 9 / 8, 6 / 5, 5 / 4].join(' ')).mul(220).freq() + * + * @returns Pattern + */ +export const withBase = register('withBase', (b, pat) => { + let base; + let originalBase = 220; + if (Array.isArray(b)) { + base = b[0]; + originalBase = b[1]; + } else { + base = b; + } + return pat.withHaps((haps) => { + haps = haps.map((hap) => { + let hVal = hap.value; + const isObject = typeof hVal === 'object'; + let freq = isObject ? hVal.freq : hVal; + freq = (freq * base) / originalBase; + hap.value = isObject ? { ...hap.value, freq } : { freq }; + return hap; + }); + return removeUndefineds(haps); + }); +}); + +/** + * Frequency transpose. Assumes pattern either has `freq` set, or has values that can be interpreted as frequencies + * amt has optional `edoSize` param, defaults to 12. + * If haps have edoSize param set, such as from the output of `xen("31edo")`, + * `ftrans` will fallback to that instead of 12 as the default. + * + * Transposes the frequency by `amt` edoSteps + * @name ftranspose + * @synonyms ftrans, fTrans, ftranspose, fTranspose + * @tags tonal + * @param {number} amt + * @param {number} edoSize (optional) + * @returns {Pattern} + * + * @example + * i("0 1 2").xen("12edo").ftrans("7") + * // n("0 1 2").scale("A:chromatic").trans("7") + * @example + * i("0 8 18").xen("31edo").ftrans("<8 -8>") + * @example + * // to transpose by steps of an edo, use "step:edo" : + * i("0 7 8 18").xen("31edo").ftrans("<0 1:31 1:12>") + * @example + * // it can also work with frequency values directly + * freq("200 300 400").ftrans("<0 7:31 7>") + */ + +/* f = frequency (Hz) + n = edo (steps per octave) + x = number of steps + if 0\n = f, then x\n = f * 2^(x/n) + example: 5edo, 0\5 = 220 Hz, then 2\5 = 220*2^(2/5) = 290.29 Hz */ + +export const { ftrans, fTrans, ftranspose, fTranspose } = register( + ['ftrans', 'fTrans', 'ftranspose', 'fTranspose'], + (amt, pat) => { + let edoSize; + let numSteps; + if (Array.isArray(amt)) { + edoSize = amt[1]; + numSteps = amt[0]; + } else { + numSteps = amt; + } + return pat.withHaps((haps) => { + haps = haps.map((hap) => { + let hVal = hap.value; + const isObject = typeof hVal === 'object'; + hVal = isObject ? hVal : { freq: hVal }; + let { freq, ...otherValues } = hVal; + if (edoSize == undefined && hap.context.edoSize != undefined) { + edoSize = hap.context.edoSize; + } else if (edoSize == undefined) { + edoSize = 12; + } + freq = freq * Math.pow(2, numSteps / edoSize); + freq = trimFreq(freq); + hap.value = isObject ? { ...otherValues, freq } : freq; + return hap.setContext({ ...hap.context, edoSize }); + }); + return removeUndefineds(haps); + }); + }, +); + +// not sure there's a point to having this and the above, seems like a proto version of the above. +const tuning = register('tuning', function (ratios, pat) { return pat.withHap((hap) => { const frequency = xenOffset(ratios, parseNumeral(hap.value)); return hap.withValue(() => frequency); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53437f8f..1b086a3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,30 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +catalogs: + default: + '@codemirror/autocomplete': + specifier: ^6.18.4 + version: 6.18.4 + '@codemirror/commands': + specifier: ^6.8.0 + version: 6.8.0 + '@codemirror/lang-javascript': + specifier: ^6.2.2 + version: 6.2.2 + '@codemirror/language': + specifier: ^6.10.8 + version: 6.10.8 + '@codemirror/search': + specifier: ^6.5.8 + version: 6.5.8 + '@codemirror/state': + specifier: ^6.5.1 + version: 6.5.1 + '@codemirror/view': + specifier: ^6.36.2 + version: 6.36.2 + importers: .: @@ -11,6 +35,9 @@ importers: '@strudel/core': specifier: workspace:* version: link:packages/core + '@strudel/edo': + specifier: workspace:* + version: link:packages/edo '@strudel/mini': specifier: workspace:* version: link:packages/mini @@ -93,6 +120,9 @@ importers: '@strudel/draw': specifier: workspace:* version: link:../../packages/draw + '@strudel/edo': + specifier: workspace:* + version: link:../../packages/edo '@strudel/mini': specifier: workspace:* version: link:../../packages/mini @@ -171,25 +201,25 @@ importers: packages/codemirror: dependencies: '@codemirror/autocomplete': - specifier: ^6.18.4 + specifier: 'catalog:' version: 6.18.4 '@codemirror/commands': - specifier: ^6.8.0 + specifier: 'catalog:' version: 6.8.0 '@codemirror/lang-javascript': - specifier: ^6.2.2 + specifier: 'catalog:' version: 6.2.2 '@codemirror/language': - specifier: ^6.10.8 + specifier: 'catalog:' version: 6.10.8 '@codemirror/search': - specifier: ^6.5.8 + specifier: 'catalog:' version: 6.5.8 '@codemirror/state': - specifier: ^6.5.1 + specifier: 'catalog:' version: 6.5.1 '@codemirror/view': - specifier: ^6.36.2 + specifier: 'catalog:' version: 6.36.2 '@lezer/highlight': specifier: ^1.2.1 @@ -221,6 +251,9 @@ 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 @@ -234,9 +267,6 @@ importers: packages/core: dependencies: - '@kabelsalat/web': - specifier: ^0.4.1 - version: 0.4.1 fraction.js: specifier: ^5.2.1 version: 5.2.1 @@ -283,6 +313,28 @@ importers: 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) + packages/edo: + dependencies: + '@strudel/core': + specifier: workspace:* + version: link:../core + '@tonaljs/tonal': + specifier: ^4.10.0 + version: 4.10.0 + chord-voicings: + specifier: ^0.0.1 + version: 0.0.1 + webmidi: + specifier: ^3.1.12 + version: 3.1.12 + 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) + vitest: + specifier: ^3.0.4 + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/embed: {} packages/gamepad: @@ -449,6 +501,9 @@ importers: '@strudel/draw': specifier: workspace:* version: link:../draw + '@strudel/edo': + specifier: workspace:* + version: link:../edo '@strudel/hydra': specifier: workspace:* version: link:../hydra @@ -524,6 +579,9 @@ importers: '@kabelsalat/lib': specifier: ^0.4.1 version: 0.4.1 + '@kabelsalat/web': + specifier: ^0.4.1 + version: 0.4.1 nanostores: specifier: ^0.11.3 version: 0.11.3 @@ -621,6 +679,9 @@ importers: '@strudel/core': specifier: workspace:* version: link:../core + '@strudel/edo': + specifier: workspace:* + version: link:../edo '@strudel/mini': specifier: workspace:* version: link:../mini @@ -702,6 +763,21 @@ importers: '@astrojs/tailwind': specifier: ^5.1.5 version: 5.1.5(astro@5.1.9(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@2.79.2)(terser@5.37.0)(typescript@5.7.3)(yaml@2.7.0))(tailwindcss@3.4.17) + '@codemirror/commands': + specifier: 'catalog:' + version: 6.8.0 + '@codemirror/lang-javascript': + specifier: 'catalog:' + version: 6.2.2 + '@codemirror/language': + specifier: 'catalog:' + version: 6.10.8 + '@codemirror/state': + specifier: 'catalog:' + version: 6.5.1 + '@codemirror/view': + specifier: 'catalog:' + version: 6.36.2 '@docsearch/css': specifier: ^3.8.3 version: 3.8.3 @@ -735,6 +811,9 @@ importers: '@strudel/draw': specifier: workspace:* version: link:../packages/draw + '@strudel/edo': + specifier: workspace:* + version: link:../packages/edo '@strudel/gamepad': specifier: workspace:* version: link:../packages/gamepad @@ -3551,6 +3630,15 @@ 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==} @@ -11280,6 +11368,14 @@ 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/pnpm-workspace.yaml b/pnpm-workspace.yaml index a4fca9ff..c8be1ac1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -10,3 +10,12 @@ onlyBuiltDependencies: - 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" diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index fa07db22..7ea844a1 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1037,6 +1037,43 @@ exports[`runs examples > example "anchor" example index 0 1`] = ` ] `; +exports[`runs examples > example "anchor" example index 1 1`] = ` +[ + "[ 0/1 → 1/8 | anchor:c4 note:60 ]", + "[ 1/8 → 1/4 | anchor:c4 note:62 ]", + "[ 1/4 → 3/8 | anchor:c4 note:64 ]", + "[ 3/8 → 1/2 | anchor:c4 note:65 ]", + "[ 1/2 → 5/8 | anchor:c4 note:67 ]", + "[ 5/8 → 3/4 | anchor:c4 note:69 ]", + "[ 3/4 → 7/8 | anchor:c4 note:71 ]", + "[ 7/8 → 1/1 | anchor:c4 note:72 ]", + "[ 1/1 → 9/8 | anchor:g4 note:67 ]", + "[ 9/8 → 5/4 | anchor:g4 note:68 ]", + "[ 5/4 → 11/8 | anchor:g4 note:70 ]", + "[ 11/8 → 3/2 | anchor:g4 note:72 ]", + "[ 3/2 → 13/8 | anchor:g4 note:73 ]", + "[ 13/8 → 7/4 | anchor:g4 note:75 ]", + "[ 7/4 → 15/8 | anchor:g4 note:77 ]", + "[ 15/8 → 2/1 | anchor:g4 note:79 ]", + "[ 2/1 → 17/8 | anchor:c5 note:72 ]", + "[ 17/8 → 9/4 | anchor:c5 note:74 ]", + "[ 9/4 → 19/8 | anchor:c5 note:76 ]", + "[ 19/8 → 5/2 | anchor:c5 note:77 ]", + "[ 5/2 → 21/8 | anchor:c5 note:79 ]", + "[ 21/8 → 11/4 | anchor:c5 note:81 ]", + "[ 11/4 → 23/8 | anchor:c5 note:83 ]", + "[ 23/8 → 3/1 | anchor:c5 note:84 ]", + "[ 3/1 → 25/8 | anchor:g5 note:79 ]", + "[ 25/8 → 13/4 | anchor:g5 note:80 ]", + "[ 13/4 → 27/8 | anchor:g5 note:82 ]", + "[ 27/8 → 7/2 | anchor:g5 note:84 ]", + "[ 7/2 → 29/8 | anchor:g5 note:85 ]", + "[ 29/8 → 15/4 | anchor:g5 note:87 ]", + "[ 15/4 → 31/8 | anchor:g5 note:89 ]", + "[ 31/8 → 4/1 | anchor:g5 note:91 ]", +] +`; + exports[`runs examples > example "apply" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:C3 ]", @@ -2975,15 +3012,6 @@ exports[`runs examples > example "delayfeedback" example index 0 1`] = ` ] `; -exports[`runs examples > example "delayfeedback" example index 0 2`] = ` -[ - "[ 0/1 → 1/1 | s:bd delay:0.25 delayfeedback:0.25 ]", - "[ 1/1 → 2/1 | s:bd delay:0.25 delayfeedback:0.5 ]", - "[ 2/1 → 3/1 | s:bd delay:0.25 delayfeedback:0.75 ]", - "[ 3/1 → 4/1 | s:bd delay:0.25 delayfeedback: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 ]", @@ -3034,6 +3062,43 @@ 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 ]", +] +`; + exports[`runs examples > example "density" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:crackle density:0.01 ]", @@ -3922,6 +3987,93 @@ exports[`runs examples > example "echoWith" example index 0 1`] = ` ] `; +exports[`runs examples > example "edoScale" example index 0 1`] = ` +[ + "[ 0/1 → 1/6 | degree:1 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:130.813 edo:12 ]", + "[ 1/6 → 1/3 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]", + "[ 1/3 → 1/2 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]", + "[ 1/2 → 2/3 | degree:7 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:246.942 edo:12 ]", + "[ 2/3 → 5/6 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]", + "[ 5/6 → 1/1 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]", + "[ 1/1 → 7/6 | degree:1 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:130.813 edo:12 ]", + "[ 7/6 → 4/3 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]", + "[ 4/3 → 3/2 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]", + "[ 3/2 → 5/3 | degree:7 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:246.942 edo:12 ]", + "[ 5/3 → 11/6 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]", + "[ 11/6 → 2/1 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]", + "[ 2/1 → 13/6 | degree:1 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:130.813 edo:12 ]", + "[ 13/6 → 7/3 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]", + "[ 7/3 → 5/2 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]", + "[ 5/2 → 8/3 | degree:7 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:246.942 edo:12 ]", + "[ 8/3 → 17/6 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]", + "[ 17/6 → 3/1 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]", + "[ 3/1 → 19/6 | degree:1 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:130.813 edo:12 ]", + "[ 19/6 → 10/3 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]", + "[ 10/3 → 7/2 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]", + "[ 7/2 → 11/3 | degree:7 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:246.942 edo:12 ]", + "[ 11/3 → 23/6 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]", + "[ 23/6 → 4/1 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]", +] +`; + +exports[`runs examples > example "edoScale" example index 1 1`] = ` +[ + "[ 0/1 → 1/4 | degree:1 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:97.999 edo:16 s:piano ]", + "[ 0/1 → 1/4 | degree:2 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]", + "[ 1/4 → 1/2 | degree:5 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:151.135 edo:16 s:piano ]", + "[ 1/2 → 3/4 | degree:3 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:127.089 edo:16 s:piano ]", + "[ 1/2 → 3/4 | degree:2 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]", + "[ 3/4 → 1/1 | degree:5 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:151.135 edo:16 s:piano ]", + "[ 1/1 → 5/4 | degree:1 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:97.999 edo:16 s:piano ]", + "[ 1/1 → 5/4 | degree:2 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]", + "[ 5/4 → 3/2 | degree:5 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:164.814 edo:16 s:piano ]", + "[ 3/2 → 7/4 | degree:3 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:127.089 edo:16 s:piano ]", + "[ 3/2 → 7/4 | degree:2 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]", + "[ 7/4 → 2/1 | degree:5 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:164.814 edo:16 s:piano ]", + "[ 2/1 → 9/4 | degree:1 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:97.999 edo:16 s:piano ]", + "[ 2/1 → 9/4 | degree:2 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]", + "[ 9/4 → 5/2 | degree:5 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:151.135 edo:16 s:piano ]", + "[ 5/2 → 11/4 | degree:3 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:127.089 edo:16 s:piano ]", + "[ 5/2 → 11/4 | degree:2 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]", + "[ 11/4 → 3/1 | degree:5 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:151.135 edo:16 s:piano ]", + "[ 3/1 → 13/4 | degree:1 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:97.999 edo:16 s:piano ]", + "[ 3/1 → 13/4 | degree:2 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]", + "[ 13/4 → 7/2 | degree:5 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:164.814 edo:16 s:piano ]", + "[ 7/2 → 15/4 | degree:3 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:127.089 edo:16 s:piano ]", + "[ 7/2 → 15/4 | degree:2 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]", + "[ 15/4 → 4/1 | degree:5 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:164.814 edo:16 s:piano ]", +] +`; + +exports[`runs examples > example "edoScale" example index 2 1`] = ` +[ + "[ 0/1 → 1/6 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:97.999 edo:13 s:piano ]", + "[ 1/6 → 1/3 | degree:5 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:167.025 edo:13 s:piano ]", + "[ 1/3 → 1/2 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:134.945 edo:13 s:piano ]", + "[ 1/2 → 2/3 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:114.998 edo:13 s:piano ]", + "[ 2/3 → 5/6 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:134.945 edo:13 s:piano ]", + "[ 5/6 → 1/1 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:97.999 edo:13 s:piano ]", + "[ 1/1 → 7/6 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:189.995 edo:13 s:piano ]", + "[ 7/6 → 4/3 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:153.504 edo:13 s:piano ]", + "[ 4/3 → 3/2 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:180.13 edo:13 s:piano ]", + "[ 3/2 → 5/3 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:189.995 edo:13 s:piano ]", + "[ 5/3 → 11/6 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:130.813 edo:13 s:piano ]", + "[ 11/6 → 2/1 | degree:5 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:222.952 edo:13 s:piano ]", + "[ 2/1 → 13/6 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:195.998 edo:13 s:piano ]", + "[ 13/6 → 7/3 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:142.336 edo:13 s:piano ]", + "[ 7/3 → 5/2 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:134.945 edo:13 s:piano ]", + "[ 5/2 → 8/3 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:134.945 edo:13 s:piano ]", + "[ 8/3 → 17/6 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:142.336 edo:13 s:piano ]", + "[ 17/6 → 3/1 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:114.998 edo:13 s:piano ]", + "[ 3/1 → 19/6 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:153.504 edo:13 s:piano ]", + "[ 19/6 → 10/3 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:153.504 edo:13 s:piano ]", + "[ 10/3 → 7/2 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:261.626 edo:13 s:piano ]", + "[ 7/2 → 11/3 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:180.13 edo:13 s:piano ]", + "[ 11/3 → 23/6 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:180.13 edo:13 s:piano ]", + "[ 23/6 → 4/1 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:189.995 edo:13 s:piano ]", +] +`; + exports[`runs examples > example "end" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:oh end:0.1 ]", @@ -4193,6 +4345,27 @@ exports[`runs examples > example "extend" example index 0 1`] = ` ] `; +exports[`runs examples > example "fadeTime" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:oh end:0.1 fadeTime:0 ]", + "[ 1/4 → 1/2 | s:oh end:0.1 fadeTime:0 ]", + "[ 1/2 → 3/4 | s:oh end:0.1 fadeTime:0 ]", + "[ 3/4 → 1/1 | s:oh end:0.1 fadeTime:0 ]", + "[ 1/1 → 5/4 | s:oh end:0.1 fadeTime:0.2 ]", + "[ 5/4 → 3/2 | s:oh end:0.1 fadeTime:0.2 ]", + "[ 3/2 → 7/4 | s:oh end:0.1 fadeTime:0.2 ]", + "[ 7/4 → 2/1 | s:oh end:0.1 fadeTime:0.2 ]", + "[ 2/1 → 9/4 | s:oh end:0.1 fadeTime:0.4 ]", + "[ 9/4 → 5/2 | s:oh end:0.1 fadeTime:0.4 ]", + "[ 5/2 → 11/4 | s:oh end:0.1 fadeTime:0.4 ]", + "[ 11/4 → 3/1 | s:oh end:0.1 fadeTime:0.4 ]", + "[ 3/1 → 13/4 | s:oh end:0.1 fadeTime:0.8 ]", + "[ 13/4 → 7/2 | s:oh end:0.1 fadeTime:0.8 ]", + "[ 7/2 → 15/4 | s:oh end:0.1 fadeTime:0.8 ]", + "[ 15/4 → 4/1 | s:oh end:0.1 fadeTime:0.8 ]", +] +`; + exports[`runs examples > example "fanchor" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:f s:sawtooth cutoff:1000 lpenv:8 fanchor:0 ]", @@ -4892,6 +5065,78 @@ exports[`runs examples > example "fscope" example index 0 1`] = ` ] `; +exports[`runs examples > example "ftranspose" example index 0 1`] = ` +[ + "[ 0/1 → 1/3 | freq:329.6275569 ]", + "[ 1/3 → 2/3 | freq:349.2282315 ]", + "[ 2/3 → 1/1 | freq:369.9944227 ]", + "[ 1/1 → 4/3 | freq:329.6275569 ]", + "[ 4/3 → 5/3 | freq:349.2282315 ]", + "[ 5/3 → 2/1 | freq:369.9944227 ]", + "[ 2/1 → 7/3 | freq:329.6275569 ]", + "[ 7/3 → 8/3 | freq:349.2282315 ]", + "[ 8/3 → 3/1 | freq:369.9944227 ]", + "[ 3/1 → 10/3 | freq:329.6275569 ]", + "[ 10/3 → 11/3 | freq:349.2282315 ]", + "[ 11/3 → 4/1 | freq:369.9944227 ]", +] +`; + +exports[`runs examples > example "ftranspose" example index 1 1`] = ` +[ + "[ 0/1 → 1/3 | freq:263.0921203 ]", + "[ 1/3 → 2/3 | freq:314.6248353 ]", + "[ 2/3 → 1/1 | freq:393.4589706 ]", + "[ 1/1 → 4/3 | freq:183.965981 ]", + "[ 4/3 → 5/3 | freq:220 ]", + "[ 5/3 → 2/1 | freq:275.1244143 ]", + "[ 2/1 → 7/3 | freq:263.0921203 ]", + "[ 7/3 → 8/3 | freq:314.6248353 ]", + "[ 8/3 → 3/1 | freq:393.4589706 ]", + "[ 3/1 → 10/3 | freq:183.965981 ]", + "[ 10/3 → 11/3 | freq:220 ]", + "[ 11/3 → 4/1 | freq:275.1244143 ]", +] +`; + +exports[`runs examples > example "ftranspose" example index 2 1`] = ` +[ + "[ 0/1 → 1/4 | freq:220 ]", + "[ 1/4 → 1/2 | freq:257.2747684 ]", + "[ 1/2 → 3/4 | freq:263.0921203 ]", + "[ 3/4 → 1/1 | freq:329.0139341 ]", + "[ 1/1 → 5/4 | freq:224.9745158 ]", + "[ 5/4 → 3/2 | freq:263.0921203 ]", + "[ 3/2 → 7/4 | freq:269.0410108 ]", + "[ 7/4 → 2/1 | freq:336.4534115 ]", + "[ 2/1 → 9/4 | freq:233.0818808 ]", + "[ 9/4 → 5/2 | freq:272.5731222 ]", + "[ 5/2 → 11/4 | freq:278.7363919 ]", + "[ 11/4 → 3/1 | freq:348.5781207 ]", + "[ 3/1 → 13/4 | freq:220 ]", + "[ 13/4 → 7/2 | freq:257.2747684 ]", + "[ 7/2 → 15/4 | freq:263.0921203 ]", + "[ 15/4 → 4/1 | freq:329.0139341 ]", +] +`; + +exports[`runs examples > example "ftranspose" example index 3 1`] = ` +[ + "[ 0/1 → 1/3 | freq:200 ]", + "[ 1/3 → 2/3 | freq:300 ]", + "[ 2/3 → 1/1 | freq:400 ]", + "[ 1/1 → 4/3 | freq:233.8861531 ]", + "[ 4/3 → 5/3 | freq:350.8292297 ]", + "[ 5/3 → 2/1 | freq:467.7723062 ]", + "[ 2/1 → 7/3 | freq:299.6614154 ]", + "[ 7/3 → 8/3 | freq:449.4921231 ]", + "[ 8/3 → 3/1 | freq:599.3228308 ]", + "[ 3/1 → 10/3 | freq:200 ]", + "[ 10/3 → 11/3 | freq:300 ]", + "[ 11/3 → 4/1 | freq:400 ]", +] +`; + exports[`runs examples > example "ftype" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:f s:sawtooth lpenv:4 cutoff:500 ftype:0 resonance:1 ]", @@ -5061,6 +5306,15 @@ exports[`runs examples > example "gain" example index 0 1`] = ` exports[`runs examples > example "gap" example index 0 1`] = `[]`; +exports[`runs examples > example "getDur" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | s:sax cps:null ]", + "[ 1/1 → 2/1 | s:sax cps:null ]", + "[ 2/1 → 3/1 | s:sax cps:null ]", + "[ 3/1 → 4/1 | s:sax cps:null ]", +] +`; + exports[`runs examples > example "grow" example index 0 1`] = ` [ "[ 0/1 → 1/10 | s:tha bank:mridangam ]", @@ -5576,6 +5830,43 @@ exports[`runs examples > example "hush" example index 0 1`] = ` ] `; +exports[`runs examples > example "i" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | freq:220 ]", + "[ 1/8 → 1/4 | freq:252.7136381 ]", + "[ 1/4 → 3/8 | freq:290.2917404 ]", + "[ 3/8 → 1/2 | freq:333.4576446 ]", + "[ 1/2 → 5/8 | freq:383.0422479 ]", + "[ 5/8 → 3/4 | freq:440 ]", + "[ 3/4 → 7/8 | freq:505.4272762 ]", + "[ 7/8 → 1/1 | freq:580.5834807 ]", + "[ 1/1 → 9/8 | freq:220 ]", + "[ 9/8 → 5/4 | freq:235.7901618 ]", + "[ 5/4 → 11/8 | freq:252.7136381 ]", + "[ 11/8 → 3/2 | freq:270.8517709 ]", + "[ 3/2 → 13/8 | freq:290.2917404 ]", + "[ 13/8 → 7/4 | freq:311.1269837 ]", + "[ 7/4 → 15/8 | freq:333.4576446 ]", + "[ 15/8 → 2/1 | freq:357.3910544 ]", + "[ 2/1 → 17/8 | freq:220 ]", + "[ 17/8 → 9/4 | freq:230.404707 ]", + "[ 9/4 → 19/8 | freq:241.3014955 ]", + "[ 19/8 → 5/2 | freq:252.7136381 ]", + "[ 5/2 → 21/8 | freq:264.6655079 ]", + "[ 21/8 → 11/4 | freq:277.182631 ]", + "[ 11/4 → 23/8 | freq:290.2917404 ]", + "[ 23/8 → 3/1 | freq:304.0208336 ]", + "[ 3/1 → 25/8 | freq:220 ]", + "[ 25/8 → 13/4 | freq:275 ]", + "[ 13/4 → 27/8 | freq:293.3333333 ]", + "[ 27/8 → 7/2 | freq:330 ]", + "[ 7/2 → 29/8 | freq:352 ]", + "[ 29/8 → 15/4 | freq:440 ]", + "[ 15/4 → 31/8 | freq:550 ]", + "[ 31/8 → 4/1 | freq:586.6666667 ]", +] +`; + exports[`runs examples > example "inhabit" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:bd ]", @@ -6236,6 +6527,27 @@ exports[`runs examples > example "juxBy" example index 0 1`] = ` ] `; +exports[`runs examples > example "keep" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:c s:piano ]", + "[ 1/4 → 1/2 | note:a s:piano ]", + "[ 1/2 → 3/4 | note:f s:piano ]", + "[ 3/4 → 1/1 | note:e s:piano ]", + "[ 1/1 → 5/4 | note:c s:piano ]", + "[ 5/4 → 3/2 | note:a s:piano ]", + "[ 3/2 → 7/4 | note:f s:piano ]", + "[ 7/4 → 2/1 | note:e s:piano ]", + "[ 2/1 → 9/4 | note:c s:piano ]", + "[ 9/4 → 5/2 | note:a s:piano ]", + "[ 5/2 → 11/4 | note:f s:piano ]", + "[ 11/4 → 3/1 | note:e s:piano ]", + "[ 3/1 → 13/4 | note:c s:piano ]", + "[ 13/4 → 7/2 | note:a s:piano ]", + "[ 7/2 → 15/4 | note:f s:piano ]", + "[ 15/4 → 4/1 | note:e s:piano ]", +] +`; + exports[`runs examples > example "keyDown" example index 0 1`] = `[]`; exports[`runs examples > example "lastOf" example index 0 1`] = ` @@ -8099,6 +8411,48 @@ exports[`runs examples > example "panchor" example index 0 1`] = ` ] `; +exports[`runs examples > example "panspan" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd pan:0.5 panspan:0 ]", + "[ 1/4 → 1/2 | s:hh pan:0.5 panspan:0 ]", + "[ 1/2 → 3/4 | s:bd pan:0.5 panspan:0 ]", + "[ 3/4 → 1/1 | s:hh pan:0.5 panspan:0 ]", + "[ 1/1 → 5/4 | s:bd pan:1 panspan:0.5 ]", + "[ 5/4 → 3/2 | s:hh pan:1 panspan:0.5 ]", + "[ 3/2 → 7/4 | s:bd pan:1 panspan:0.5 ]", + "[ 7/4 → 2/1 | s:hh pan:1 panspan:0.5 ]", + "[ 2/1 → 9/4 | s:bd pan:0.5 panspan:1 ]", + "[ 9/4 → 5/2 | s:hh pan:0.5 panspan:1 ]", + "[ 5/2 → 11/4 | s:bd pan:0.5 panspan:1 ]", + "[ 11/4 → 3/1 | s:hh pan:0.5 panspan:1 ]", + "[ 3/1 → 13/4 | s:bd pan:0 panspan:0 ]", + "[ 13/4 → 7/2 | s:hh pan:0 panspan:0 ]", + "[ 7/2 → 15/4 | s:bd pan:0 panspan:0 ]", + "[ 15/4 → 4/1 | s:hh pan:0 panspan:0 ]", +] +`; + +exports[`runs examples > example "pansplay" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd pan:0.5 pansplay:0 ]", + "[ 1/4 → 1/2 | s:hh pan:0.5 pansplay:0 ]", + "[ 1/2 → 3/4 | s:bd pan:0.5 pansplay:0 ]", + "[ 3/4 → 1/1 | s:hh pan:0.5 pansplay:0 ]", + "[ 1/1 → 5/4 | s:bd pan:1 pansplay:0.5 ]", + "[ 5/4 → 3/2 | s:hh pan:1 pansplay:0.5 ]", + "[ 3/2 → 7/4 | s:bd pan:1 pansplay:0.5 ]", + "[ 7/4 → 2/1 | s:hh pan:1 pansplay:0.5 ]", + "[ 2/1 → 9/4 | s:bd pan:0.5 pansplay:1 ]", + "[ 9/4 → 5/2 | s:hh pan:0.5 pansplay:1 ]", + "[ 5/2 → 11/4 | s:bd pan:0.5 pansplay:1 ]", + "[ 11/4 → 3/1 | s:hh pan:0.5 pansplay:1 ]", + "[ 3/1 → 13/4 | s:bd pan:0 pansplay:0 ]", + "[ 13/4 → 7/2 | s:hh pan:0 pansplay:0 ]", + "[ 7/2 → 15/4 | s:bd pan:0 pansplay:0 ]", + "[ 15/4 → 4/1 | s:hh pan:0 pansplay:0 ]", +] +`; + exports[`runs examples > example "partials" example index 0 1`] = ` [ "[ 0/1 → 1/16 | s:user note:A3 partials:[1 0 1 0 0 1] ]", @@ -8723,6 +9077,33 @@ exports[`runs examples > example "pickF" example index 1 1`] = ` ] `; +exports[`runs examples > example "pickF" example index 2 1`] = ` +[ + "[ 0/1 → 1/8 | note:c2 s:square pan:0 ]", + "[ 1/8 → 1/4 | note:c2 s:square pan:1 ]", + "[ 3/8 → 1/2 | note:c2 s:square pan:0 ]", + "[ 1/2 → 9/16 | note:d2 s:square ]", + "[ 11/16 → 3/4 | note:d2 s:square ]", + "[ 7/8 → 15/16 | note:d2 s:square ]", + "[ 1/1 → 9/8 | note:d2 s:square cutoff:800 ]", + "[ 11/8 → 3/2 | note:d2 s:square cutoff:800 ]", + "[ 3/2 → 25/16 | note:d2 s:square ]", + "[ 27/16 → 7/4 | note:d2 s:square ]", + "[ 15/8 → 31/16 | note:d2 s:square ]", + "[ 2/1 → 17/8 | note:c2 s:square pan:0 ]", + "[ 17/8 → 9/4 | note:c2 s:square pan:1 ]", + "[ 19/8 → 5/2 | note:c2 s:square pan:0 ]", + "[ 5/2 → 41/16 | note:d2 s:square ]", + "[ 43/16 → 11/4 | note:d2 s:square ]", + "[ 23/8 → 47/16 | note:d2 s:square ]", + "[ 3/1 → 25/8 | note:d2 s:square cutoff:800 ]", + "[ 27/8 → 7/2 | note:d2 s:square cutoff:800 ]", + "[ 7/2 → 57/16 | note:d2 s:square ]", + "[ 59/16 → 15/4 | note:d2 s:square ]", + "[ 31/8 → 63/16 | note:d2 s:square ]", +] +`; + exports[`runs examples > example "pickmodRestart" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C3 s:piano ]", @@ -10874,6 +11255,56 @@ exports[`runs examples > example "seqPLoop" example index 0 1`] = ` ] `; +exports[`runs examples > example "set" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:c s:triangle ]", + "[ 1/4 → 1/2 | note:a s:triangle ]", + "[ 1/2 → 3/4 | note:f s:triangle ]", + "[ 3/4 → 1/1 | note:e s:triangle ]", + "[ 1/1 → 5/4 | note:c s:triangle ]", + "[ 5/4 → 3/2 | note:a s:triangle ]", + "[ 3/2 → 7/4 | note:f s:triangle ]", + "[ 7/4 → 2/1 | note:e s:triangle ]", + "[ 2/1 → 9/4 | note:c s:triangle ]", + "[ 9/4 → 5/2 | note:a s:triangle ]", + "[ 5/2 → 11/4 | note:f s:triangle ]", + "[ 11/4 → 3/1 | note:e s:triangle ]", + "[ 3/1 → 13/4 | note:c s:triangle ]", + "[ 13/4 → 7/2 | note:a s:triangle ]", + "[ 7/2 → 15/4 | note:f s:triangle ]", + "[ 15/4 → 4/1 | note:e s:triangle ]", +] +`; + +exports[`runs examples > example "setDefaultJoin" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:saw velocity:1 note:F delay:0 ]", + "[ 1/4 → 1/3 | s:saw velocity:1 note:A delay:0 ]", + "[ 1/3 → 1/2 | s:saw velocity:1 note:A delay:0.2 ]", + "[ 1/2 → 2/3 | s:saw velocity:0.5 note:C delay:0.2 ]", + "[ 2/3 → 3/4 | s:saw velocity:0.5 note:C delay:0.3 ]", + "[ 3/4 → 1/1 | s:saw velocity:0.5 note:E delay:0.3 ]", + "[ 1/1 → 5/4 | s:saw velocity:1 note:F delay:0 ]", + "[ 5/4 → 4/3 | s:saw velocity:1 note:A delay:0 ]", + "[ 4/3 → 3/2 | s:saw velocity:1 note:A delay:0.2 ]", + "[ 3/2 → 5/3 | s:saw velocity:0.5 note:C delay:0.2 ]", + "[ 5/3 → 7/4 | s:saw velocity:0.5 note:C delay:0.3 ]", + "[ 7/4 → 2/1 | s:saw velocity:0.5 note:E delay:0.3 ]", + "[ 2/1 → 9/4 | s:saw velocity:1 note:F delay:0 ]", + "[ 9/4 → 7/3 | s:saw velocity:1 note:A delay:0 ]", + "[ 7/3 → 5/2 | s:saw velocity:1 note:A delay:0.2 ]", + "[ 5/2 → 8/3 | s:saw velocity:0.5 note:C delay:0.2 ]", + "[ 8/3 → 11/4 | s:saw velocity:0.5 note:C delay:0.3 ]", + "[ 11/4 → 3/1 | s:saw velocity:0.5 note:E delay:0.3 ]", + "[ 3/1 → 13/4 | s:saw velocity:1 note:F delay:0 ]", + "[ 13/4 → 10/3 | s:saw velocity:1 note:A delay:0 ]", + "[ 10/3 → 7/2 | s:saw velocity:1 note:A delay:0.2 ]", + "[ 7/2 → 11/3 | s:saw velocity:0.5 note:C delay:0.2 ]", + "[ 11/3 → 15/4 | s:saw velocity:0.5 note:C delay:0.3 ]", + "[ 15/4 → 4/1 | s:saw velocity:0.5 note:E delay:0.3 ]", +] +`; + exports[`runs examples > example "setGainCurve" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:bd gain:0.5 ]", @@ -13051,6 +13482,97 @@ exports[`runs examples > example "tri" example index 0 1`] = ` ] `; +exports[`runs examples > example "tune" example index 0 1`] = ` +[ + "[ 0/1 → 1/6 | freq:{i:0} ]", + "[ 1/6 → 1/3 | freq:{i:1} ]", + "[ 1/3 → 1/2 | freq:{i:2} ]", + "[ 1/2 → 2/3 | freq:{i:3} ]", + "[ 2/3 → 5/6 | freq:{i:4} ]", + "[ 5/6 → 1/1 | freq:{i:5} ]", + "[ 1/1 → 7/6 | freq:{i:0} ]", + "[ 7/6 → 4/3 | freq:{i:1} ]", + "[ 4/3 → 3/2 | freq:{i:2} ]", + "[ 3/2 → 5/3 | freq:{i:3} ]", + "[ 5/3 → 11/6 | freq:{i:4} ]", + "[ 11/6 → 2/1 | freq:{i:5} ]", + "[ 2/1 → 13/6 | freq:{i:0} ]", + "[ 13/6 → 7/3 | freq:{i:1} ]", + "[ 7/3 → 5/2 | freq:{i:2} ]", + "[ 5/2 → 8/3 | freq:{i:3} ]", + "[ 8/3 → 17/6 | freq:{i:4} ]", + "[ 17/6 → 3/1 | freq:{i:5} ]", + "[ 3/1 → 19/6 | freq:{i:0} ]", + "[ 19/6 → 10/3 | freq:{i:1} ]", + "[ 10/3 → 7/2 | freq:{i:2} ]", + "[ 7/2 → 11/3 | freq:{i:3} ]", + "[ 11/3 → 23/6 | freq:{i:4} ]", + "[ 23/6 → 4/1 | freq:{i:5} ]", +] +`; + +exports[`runs examples > example "tune" example index 1 1`] = ` +[ + "[ 0/1 → 1/12 | freq:{i:4} clip:0.5 room:1 ]", + "[ 1/12 → 1/6 | freq:{i:8} clip:0.5 room:1 ]", + "[ 1/6 → 1/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 1/4 → 1/3 | freq:{i:10} clip:0.5 room:1 ]", + "[ 1/2 → 7/12 | freq:{i:5} clip:0.5 room:1 ]", + "[ 7/12 → 2/3 | freq:{i:7} clip:0.5 room:1 ]", + "[ 2/3 → 3/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 3/4 → 5/6 | freq:{i:11} clip:0.5 room:1 ]", + "[ 1/1 → 13/12 | freq:{i:4} clip:0.5 room:1 ]", + "[ 13/12 → 7/6 | freq:{i:8} clip:0.5 room:1 ]", + "[ 7/6 → 5/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 5/4 → 4/3 | freq:{i:10} clip:0.5 room:1 ]", + "[ 3/2 → 19/12 | freq:{i:5} clip:0.5 room:1 ]", + "[ 19/12 → 5/3 | freq:{i:7} clip:0.5 room:1 ]", + "[ 5/3 → 7/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 7/4 → 11/6 | freq:{i:11} clip:0.5 room:1 ]", + "[ 2/1 → 25/12 | freq:{i:4} clip:0.5 room:1 ]", + "[ 25/12 → 13/6 | freq:{i:8} clip:0.5 room:1 ]", + "[ 13/6 → 9/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 9/4 → 7/3 | freq:{i:10} clip:0.5 room:1 ]", + "[ 5/2 → 31/12 | freq:{i:5} clip:0.5 room:1 ]", + "[ 31/12 → 8/3 | freq:{i:7} clip:0.5 room:1 ]", + "[ 8/3 → 11/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 11/4 → 17/6 | freq:{i:11} clip:0.5 room:1 ]", + "[ 3/1 → 37/12 | freq:{i:4} clip:0.5 room:1 ]", + "[ 37/12 → 19/6 | freq:{i:8} clip:0.5 room:1 ]", + "[ 19/6 → 13/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 13/4 → 10/3 | freq:{i:10} clip:0.5 room:1 ]", + "[ 7/2 → 43/12 | freq:{i:5} clip:0.5 room:1 ]", + "[ 43/12 → 11/3 | freq:{i:7} clip:0.5 room:1 ]", + "[ 11/3 → 15/4 | freq:{i:9} clip:0.5 room:1 ]", + "[ 15/4 → 23/6 | freq:{i:11} clip:0.5 room:1 ]", +] +`; + +exports[`runs examples > example "tune" example index 2 1`] = ` +[ + "[ 0/1 → 1/5 | freq:{i:0} ]", + "[ 1/5 → 2/5 | freq:{i:1} ]", + "[ 2/5 → 3/5 | freq:{i:2} ]", + "[ 3/5 → 4/5 | freq:{i:3} ]", + "[ 4/5 → 1/1 | freq:{i:4} ]", + "[ 1/1 → 6/5 | freq:{i:0} ]", + "[ 6/5 → 7/5 | freq:{i:1} ]", + "[ 7/5 → 8/5 | freq:{i:2} ]", + "[ 8/5 → 9/5 | freq:{i:3} ]", + "[ 9/5 → 2/1 | freq:{i:4} ]", + "[ 2/1 → 11/5 | freq:{i:0} ]", + "[ 11/5 → 12/5 | freq:{i:1} ]", + "[ 12/5 → 13/5 | freq:{i:2} ]", + "[ 13/5 → 14/5 | freq:{i:3} ]", + "[ 14/5 → 3/1 | freq:{i:4} ]", + "[ 3/1 → 16/5 | freq:{i:0} ]", + "[ 16/5 → 17/5 | freq:{i:1} ]", + "[ 17/5 → 18/5 | freq:{i:2} ]", + "[ 18/5 → 19/5 | freq:{i:3} ]", + "[ 19/5 → 4/1 | freq:{i:4} ]", +] +`; + exports[`runs examples > example "undegrade" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh ]", @@ -13824,6 +14346,76 @@ exports[`runs examples > example "when" example index 0 1`] = ` exports[`runs examples > example "whenKey" example index 0 1`] = `[]`; +exports[`runs examples > example "withBase" example index 0 1`] = ` +[ + "[ 0/1 → 1/12 | freq:220 ]", + "[ 1/12 → 1/6 | freq:293.3333333 ]", + "[ 1/6 → 1/4 | freq:302.5 ]", + "[ 1/4 → 1/3 | freq:320 ]", + "[ 1/3 → 1/2 | freq:320 ]", + "[ 1/2 → 2/3 | freq:330 ]", + "[ 2/3 → 3/4 | freq:330 ]", + "[ 3/4 → 5/6 | freq:320 ]", + "[ 5/6 → 11/12 | freq:302.5 ]", + "[ 11/12 → 1/1 | freq:293.3333333 ]", + "[ 1/1 → 13/12 | freq:300 ]", + "[ 13/12 → 7/6 | freq:399.99999995454544 ]", + "[ 7/6 → 5/4 | freq:412.5 ]", + "[ 5/4 → 4/3 | freq:436.3636363636364 ]", + "[ 4/3 → 3/2 | freq:436.3636363636364 ]", + "[ 3/2 → 5/3 | freq:300 ]", + "[ 5/3 → 7/4 | freq:300 ]", + "[ 7/4 → 11/6 | freq:290.90909090909093 ]", + "[ 11/6 → 23/12 | freq:275 ]", + "[ 23/12 → 2/1 | freq:266.66666663636363 ]", + "[ 2/1 → 25/12 | freq:220 ]", + "[ 25/12 → 13/6 | freq:293.3333333 ]", + "[ 13/6 → 9/4 | freq:302.5 ]", + "[ 9/4 → 7/3 | freq:320 ]", + "[ 7/3 → 5/2 | freq:320 ]", + "[ 5/2 → 8/3 | freq:330 ]", + "[ 8/3 → 11/4 | freq:330 ]", + "[ 11/4 → 17/6 | freq:320 ]", + "[ 17/6 → 35/12 | freq:302.5 ]", + "[ 35/12 → 3/1 | freq:293.3333333 ]", + "[ 3/1 → 37/12 | freq:300 ]", + "[ 37/12 → 19/6 | freq:399.99999995454544 ]", + "[ 19/6 → 13/4 | freq:412.5 ]", + "[ 13/4 → 10/3 | freq:436.3636363636364 ]", + "[ 10/3 → 7/2 | freq:436.3636363636364 ]", + "[ 7/2 → 11/3 | freq:300 ]", + "[ 11/3 → 15/4 | freq:300 ]", + "[ 15/4 → 23/6 | freq:290.90909090909093 ]", + "[ 23/6 → 47/12 | freq:275 ]", + "[ 47/12 → 4/1 | freq:266.66666663636363 ]", +] +`; + +exports[`runs examples > example "withBase" example index 1 1`] = ` +[ + "[ 0/1 → 1/5 | freq:220 ]", + "[ 1/5 → 2/5 | freq:234.66666666666666 ]", + "[ 2/5 → 3/5 | freq:247.5 ]", + "[ 3/5 → 4/5 | freq:264 ]", + "[ 4/5 → 1/1 | freq:275 ]", + "[ 1/1 → 6/5 | freq:220 ]", + "[ 6/5 → 7/5 | freq:234.66666666666666 ]", + "[ 7/5 → 8/5 | freq:247.5 ]", + "[ 8/5 → 9/5 | freq:264 ]", + "[ 9/5 → 2/1 | freq:275 ]", + "[ 2/1 → 11/5 | freq:220 ]", + "[ 11/5 → 12/5 | freq:234.66666666666666 ]", + "[ 12/5 → 13/5 | freq:247.5 ]", + "[ 13/5 → 14/5 | freq:264 ]", + "[ 14/5 → 3/1 | freq:275 ]", + "[ 3/1 → 16/5 | freq:220 ]", + "[ 16/5 → 17/5 | freq:234.66666666666666 ]", + "[ 17/5 → 18/5 | freq:247.5 ]", + "[ 18/5 → 19/5 | freq:264 ]", + "[ 19/5 → 4/1 | freq:275 ]", +] +`; + exports[`runs examples > example "withValue" example index 0 1`] = ` [ "[ 0/1 → 1/3 | 10 ]", @@ -13963,6 +14555,106 @@ exports[`runs examples > example "wtphaserand" example index 0 1`] = ` ] `; +exports[`runs examples > example "xen" example index 0 1`] = ` +[ + "[ 0/1 → 1/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", + "[ 1/3 → 2/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 2/3 → 1/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 1/1 → 4/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", + "[ 4/3 → 5/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 5/3 → 2/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 2/1 → 7/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", + "[ 7/3 → 8/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 8/3 → 3/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 3/1 → 10/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", + "[ 10/3 → 11/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 11/3 → 4/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", +] +`; + +exports[`runs examples > example "xen" example index 1 1`] = ` +[ + "[ 0/1 → 1/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", + "[ 1/3 → 2/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 2/3 → 1/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 1/1 → 4/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", + "[ 4/3 → 5/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 5/3 → 2/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 2/1 → 7/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", + "[ 7/3 → 8/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 8/3 → 3/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", + "[ 3/1 → 10/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]", + "[ 10/3 → 11/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]", + "[ 11/3 → 4/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]", +] +`; + +exports[`runs examples > example "xen" example index 2 1`] = ` +[ + "[ 0/1 → 1/6 | freq:220 ]", + "[ 1/6 → 1/3 | freq:275 ]", + "[ 1/3 → 1/2 | freq:293.3333333 ]", + "[ 1/2 → 2/3 | freq:330 ]", + "[ 2/3 → 5/6 | freq:352 ]", + "[ 5/6 → 1/1 | freq:440 ]", + "[ 1/1 → 7/6 | freq:220 ]", + "[ 7/6 → 4/3 | freq:275 ]", + "[ 4/3 → 3/2 | freq:293.3333333 ]", + "[ 3/2 → 5/3 | freq:330 ]", + "[ 5/3 → 11/6 | freq:352 ]", + "[ 11/6 → 2/1 | freq:440 ]", + "[ 2/1 → 13/6 | freq:220 ]", + "[ 13/6 → 7/3 | freq:275 ]", + "[ 7/3 → 5/2 | freq:293.3333333 ]", + "[ 5/2 → 8/3 | freq:330 ]", + "[ 8/3 → 17/6 | freq:352 ]", + "[ 17/6 → 3/1 | freq:440 ]", + "[ 3/1 → 19/6 | freq:220 ]", + "[ 19/6 → 10/3 | freq:275 ]", + "[ 10/3 → 7/2 | freq:293.3333333 ]", + "[ 7/2 → 11/3 | freq:330 ]", + "[ 11/3 → 23/6 | freq:352 ]", + "[ 23/6 → 4/1 | freq:440 ]", +] +`; + +exports[`runs examples > example "xen" example index 3 1`] = ` +[ + "[ 0/1 → 1/8 | freq:220 ]", + "[ 1/8 → 1/4 | freq:252.7136381 ]", + "[ 1/4 → 3/8 | freq:290.2917404 ]", + "[ 3/8 → 1/2 | freq:333.4576446 ]", + "[ 1/2 → 5/8 | freq:383.0422479 ]", + "[ 5/8 → 3/4 | freq:440 ]", + "[ 3/4 → 7/8 | freq:505.4272762 ]", + "[ 7/8 → 1/1 | freq:580.5834807 ]", + "[ 1/1 → 9/8 | freq:220 ]", + "[ 9/8 → 5/4 | freq:235.7901618 ]", + "[ 5/4 → 11/8 | freq:252.7136381 ]", + "[ 11/8 → 3/2 | freq:270.8517709 ]", + "[ 3/2 → 13/8 | freq:290.2917404 ]", + "[ 13/8 → 7/4 | freq:311.1269837 ]", + "[ 7/4 → 15/8 | freq:333.4576446 ]", + "[ 15/8 → 2/1 | freq:357.3910544 ]", + "[ 2/1 → 17/8 | freq:220 ]", + "[ 17/8 → 9/4 | freq:230.404707 ]", + "[ 9/4 → 19/8 | freq:241.3014955 ]", + "[ 19/8 → 5/2 | freq:252.7136381 ]", + "[ 5/2 → 21/8 | freq:264.6655079 ]", + "[ 21/8 → 11/4 | freq:277.182631 ]", + "[ 11/4 → 23/8 | freq:290.2917404 ]", + "[ 23/8 → 3/1 | freq:304.0208336 ]", + "[ 3/1 → 25/8 | freq:220 ]", + "[ 25/8 → 13/4 | freq:275 ]", + "[ 13/4 → 27/8 | freq:293.3333333 ]", + "[ 27/8 → 7/2 | freq:330 ]", + "[ 7/2 → 29/8 | freq:352 ]", + "[ 29/8 → 15/4 | freq:440 ]", + "[ 15/4 → 31/8 | freq:550 ]", + "[ 31/8 → 4/1 | freq:586.6666667 ]", +] +`; + exports[`runs examples > example "xfade" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh gain:0 ]", diff --git a/test/examples.test.mjs b/test/examples.test.mjs index ebec2252..5ee1348a 100644 --- a/test/examples.test.mjs +++ b/test/examples.test.mjs @@ -20,6 +20,7 @@ const skippedExamples = [ 'accelerationX', 'defaultmidimap', 'midimaps', + 'clearScope', 'bmod', ]; diff --git a/test/runtime.mjs b/test/runtime.mjs index 5a64913c..e06f9162 100644 --- a/test/runtime.mjs +++ b/test/runtime.mjs @@ -13,6 +13,7 @@ import { mini, m } from '@strudel/mini/mini.mjs'; // import euclid from '@strudel/core/euclid.mjs'; //import '@strudel/midi/midi.mjs'; import * as tonalHelpers from '@strudel/tonal'; +import * as edoHelpers from '@strudel/edo'; import '@strudel/xen/xen.mjs'; // import '@strudel/xen/tune.mjs'; // import '@strudel/core/euclid.mjs'; @@ -126,6 +127,7 @@ const loadSoundfont = () => {}; const loadCsound = () => {}; const loadCSound = () => {}; const loadcsound = () => {}; +const getDuration = () => {}; const midin = () => { return (ccNum) => strudel.ref(() => 0); // returns ref with default value 0 @@ -144,6 +146,7 @@ evalScope( toneHelpersMocked, uiHelpersMocked, webaudio, + edoHelpers, tonalHelpers, gamepadHelpers, /* @@ -168,6 +171,7 @@ evalScope( loadCSound, loadCsound, loadcsound, + getDuration, setcps: id, setcpm: id, Clock: {}, // whatever diff --git a/vitest.setup.mjs b/vitest.setup.mjs index ea965df6..7b582a45 100644 --- a/vitest.setup.mjs +++ b/vitest.setup.mjs @@ -1,7 +1,9 @@ import { afterEach } from 'vitest'; import { useRNG } from './packages/core/signal.mjs'; +import { setDefaultJoin } from './packages/core/pattern.mjs'; afterEach(() => { // Avoid bleed between tests useRNG('legacy'); + setDefaultJoin('in'); }); diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 37904cb8..86763588 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -10,8 +10,11 @@ import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet'; import tailwind from '@astrojs/tailwind'; import AstroPWA from '@vite-pwa/astro'; -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 +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 baseNoTrailing = base.endsWith('/') ? base.slice(0, -1) : base; // this rehype plugin fixes relative links diff --git a/website/package.json b/website/package.json index 6f5539dd..7258f150 100644 --- a/website/package.json +++ b/website/package.json @@ -17,6 +17,11 @@ "@astrojs/react": "^4.1.6", "@astrojs/rss": "^4.0.11", "@astrojs/tailwind": "^5.1.5", + "@codemirror/commands": "catalog:", + "@codemirror/language": "catalog:", + "@codemirror/lang-javascript": "catalog:", + "@codemirror/state": "catalog:", + "@codemirror/view": "catalog:", "@docsearch/css": "^3.8.3", "@docsearch/react": "^3.8.3", "@headlessui/react": "^2.2.0", @@ -28,6 +33,7 @@ "@strudel/csound": "workspace:*", "@strudel/desktopbridge": "workspace:*", "@strudel/draw": "workspace:*", + "@strudel/edo": "workspace:*", "@strudel/gamepad": "workspace:*", "@strudel/hydra": "workspace:*", "@strudel/midi": "workspace:*", diff --git a/website/src/components/HeadCommon.astro b/website/src/components/HeadCommon.astro index fe8d34bb..550222e4 100644 --- a/website/src/components/HeadCommon.astro +++ b/website/src/components/HeadCommon.astro @@ -33,6 +33,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL