mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 13:13:10 -04:00
Compare commits
176 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e15ce7d402 | |||
| ef5ddc90c2 | |||
| 28af99914f | |||
| 4a801ab668 | |||
| 8f61a0aa90 | |||
| 1e2705c07f | |||
| 3cefbf80bf | |||
| 81342fb469 | |||
| 761e2e347e | |||
| 3911142443 | |||
| bc8517ade7 | |||
| a926346d89 | |||
| 0f4e6af49d | |||
| c20a6673af | |||
| 6b00837a4d | |||
| 807cce521c | |||
| aa49d9dcc2 | |||
| 60da901952 | |||
| 30cc92e863 | |||
| 91203233c9 | |||
| c52e8f3b38 | |||
| d5dbc5c428 | |||
| 161d2ce056 | |||
| 7cb8a151a4 | |||
| 6f108f7f28 | |||
| 7c607c8d06 | |||
| be0b3c0d8b | |||
| d3e9b9c020 | |||
| c16b300144 | |||
| 29a3543918 | |||
| 1e502c7e8c | |||
| 27a22d7609 | |||
| c515ba89a0 | |||
| 7e3063038e | |||
| 9bc560b17f | |||
| bdeafbaf75 | |||
| 8984055ea6 | |||
| 04dcbda9a3 | |||
| d7067f3562 | |||
| a39c9e6096 | |||
| 9690f78c30 | |||
| cf845ddbab | |||
| 3f72b9b97a | |||
| a347bd607f | |||
| 3966659f18 | |||
| 6d9b899ae4 | |||
| d802907dac | |||
| 2bea102dc1 | |||
| d9e5c5b0c8 | |||
| 7c1304690f | |||
| 3c366b37ae | |||
| 52a206ceca | |||
| 23f6ff9bc4 | |||
| 3e1d02cb3d | |||
| 8a72f21257 | |||
| 2755a7eebf | |||
| deb69ced9f | |||
| d5a9019220 | |||
| d0d2459e14 | |||
| a60a54147c | |||
| 6707c7893b | |||
| 1d0e2bd0c7 | |||
| b5ef12a2b1 | |||
| e3dd4ab126 | |||
| 751f924a4d | |||
| 7b58bbf12b | |||
| b2cfd3dc3d | |||
| 9f2237e11d | |||
| 6d99a7f399 | |||
| 5921bec5f4 | |||
| b4e171f5b4 | |||
| 90ad12300a | |||
| ffc6d12d87 | |||
| 1334276ec0 | |||
| cbf472fe0a | |||
| 4439599117 | |||
| 32754ef97a | |||
| 67bce82a62 | |||
| 5da3ae8df4 | |||
| eb3818eebb | |||
| e6b824ebff | |||
| c74dd95bd0 | |||
| 897a750f70 | |||
| 81d02cf5ae | |||
| a6dc6bb831 | |||
| 00a3c1acc0 | |||
| 8ff51d4468 | |||
| 2e041e8647 | |||
| 55095ba4e8 | |||
| 530f45a7bb | |||
| 99295275ec | |||
| c2e45eb36c | |||
| 35e32a32e8 | |||
| 8def3f6af6 | |||
| aa3698c4f4 | |||
| 6b525b4819 | |||
| 77ced19144 | |||
| 14cc5d989b | |||
| 47ad4bb171 | |||
| e3ad6b1f0a | |||
| 1e771d6150 | |||
| 3ac4649536 | |||
| 14be1ddf7f | |||
| 207ce9cbec | |||
| 3abcc59874 | |||
| 7fc8f1c5c9 | |||
| 016315afbc | |||
| b3c0d9179f | |||
| 993dc53e92 | |||
| 60345db1bd | |||
| a09349e457 | |||
| 71f538b115 | |||
| 70be560a7a | |||
| 269758ebdb | |||
| 98d613fbdf | |||
| f5e21a220b | |||
| f6e6df78a8 | |||
| 05ae80b0ca | |||
| 3bbaf77b20 | |||
| a9975a2b41 | |||
| 2b3f719484 | |||
| 4051fd5241 | |||
| 55ffb59b9c | |||
| 93cf9fcb6a | |||
| a5b867f1b1 | |||
| d8645e9a71 | |||
| b824ad16ae | |||
| 03b221a2a2 | |||
| 28db6d1fd4 | |||
| dc9cadc979 | |||
| 0001d9c27b | |||
| d79d122f68 | |||
| 6e61f3cdb6 | |||
| 7f2860eb14 | |||
| b684e183a0 | |||
| 6105d5027d | |||
| c96b1c2faf | |||
| 87e723649e | |||
| 62bf9abed3 | |||
| e35a2000de | |||
| 4b8d80016b | |||
| d2f2c6df02 | |||
| 9bcaaa3ac4 | |||
| 2e7ec9ea27 | |||
| bc9e97b9ac | |||
| 00f0ef56bc | |||
| 44ad05fb20 | |||
| 18cc1e4466 | |||
| 055f009357 | |||
| 2fe1712dae | |||
| 0ef30d5e8d | |||
| 160ef84b47 | |||
| 1a7f464998 | |||
| ee326c709e | |||
| a0291e8a42 | |||
| ecada58edc | |||
| bb7f587025 | |||
| dab1a018e4 | |||
| 0c01a997ac | |||
| b8dc50267e | |||
| 1aaa04ab50 | |||
| 1860d31624 | |||
| 47557cf06f | |||
| 213edfb211 | |||
| 84e59645fd | |||
| 4df19f9ff8 | |||
| ba7721f53c | |||
| fd1e3d248a | |||
| bad81e3850 | |||
| a409149dae | |||
| a6b3b9ef5e | |||
| ec73c48e2e | |||
| 65dd79e374 | |||
| ad331c8c2e | |||
| 43293dc80f | |||
| 73965a3a39 |
@@ -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
|
||||||
@@ -29,6 +29,7 @@ const editor = new StrudelMirror({
|
|||||||
import('@strudel/core'),
|
import('@strudel/core'),
|
||||||
import('@strudel/draw'),
|
import('@strudel/draw'),
|
||||||
import('@strudel/mini'),
|
import('@strudel/mini'),
|
||||||
|
import('@strudel/edo'),
|
||||||
import('@strudel/tonal'),
|
import('@strudel/tonal'),
|
||||||
import('@strudel/webaudio'),
|
import('@strudel/webaudio'),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@strudel/codemirror": "workspace:*",
|
"@strudel/codemirror": "workspace:*",
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
|
"@strudel/edo": "workspace:*",
|
||||||
"@strudel/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
"@strudel/soundfonts": "workspace:*",
|
"@strudel/soundfonts": "workspace:*",
|
||||||
"@strudel/tonal": "workspace:*",
|
"@strudel/tonal": "workspace:*",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export * from './packages/core/index.mjs';
|
|||||||
export * from './packages/csound/index.mjs';
|
export * from './packages/csound/index.mjs';
|
||||||
export * from './packages/desktopbridge/index.mjs';
|
export * from './packages/desktopbridge/index.mjs';
|
||||||
export * from './packages/draw/index.mjs';
|
export * from './packages/draw/index.mjs';
|
||||||
|
export * from './packages/edo/index.mjs';
|
||||||
export * from './packages/embed/index.mjs';
|
export * from './packages/embed/index.mjs';
|
||||||
export * from './packages/hydra/index.mjs';
|
export * from './packages/hydra/index.mjs';
|
||||||
export * from './packages/midi/index.mjs';
|
export * from './packages/midi/index.mjs';
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"homepage": "https://strudel.cc",
|
"homepage": "https://strudel.cc",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/edo": "workspace:*",
|
||||||
"@strudel/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
"@strudel/tonal": "workspace:*",
|
"@strudel/tonal": "workspace:*",
|
||||||
"@strudel/transpiler": "workspace:*",
|
"@strudel/transpiler": "workspace:*",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -13,18 +13,18 @@ import {
|
|||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { persistentAtom } from '@nanostores/persistent';
|
import { persistentAtom } from '@nanostores/persistent';
|
||||||
import { logger, registerControl, repl } from '@strudel/core';
|
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 { isAutoCompletionEnabled } from './autocomplete.mjs';
|
||||||
import { basicSetup } from './basicSetup.mjs';
|
import { basicSetup } from './basicSetup.mjs';
|
||||||
|
import { evalBlock } from './block_utilities.mjs';
|
||||||
import { flash, isFlashEnabled } from './flash.mjs';
|
import { flash, isFlashEnabled } from './flash.mjs';
|
||||||
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
||||||
import { keybindings } from './keybindings.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 { activateTheme, initTheme, theme } from './themes.mjs';
|
||||||
import { isTooltipEnabled } from './tooltip.mjs';
|
import { isTooltipEnabled } from './tooltip.mjs';
|
||||||
import { updateWidgets, widgetPlugin } from './widget.mjs';
|
import { getActiveWidgets, updateWidgets, widgetPlugin } from './widget.mjs';
|
||||||
import { jumpToCharacter } from './labelJump.mjs';
|
|
||||||
|
|
||||||
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
||||||
|
|
||||||
@@ -64,6 +64,7 @@ export const defaultSettings = {
|
|||||||
isLineWrappingEnabled: false,
|
isLineWrappingEnabled: false,
|
||||||
isTabIndentationEnabled: false,
|
isTabIndentationEnabled: false,
|
||||||
isMultiCursorEnabled: false,
|
isMultiCursorEnabled: false,
|
||||||
|
isBlockBasedEvalEnabled: false,
|
||||||
theme: 'strudelTheme',
|
theme: 'strudelTheme',
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
@@ -75,7 +76,7 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS
|
|||||||
});
|
});
|
||||||
|
|
||||||
// https://codemirror.net/docs/guide/
|
// 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 settings = codemirrorSettings.get();
|
||||||
const initialSettings = Object.keys(compartments).map((key) =>
|
const initialSettings = Object.keys(compartments).map((key) =>
|
||||||
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
||||||
@@ -105,11 +106,26 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
|||||||
keymap.of([
|
keymap.of([
|
||||||
{
|
{
|
||||||
key: 'Ctrl-Enter',
|
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',
|
key: 'Alt-Enter',
|
||||||
run: () => onEvaluate?.(),
|
run: () => {
|
||||||
|
if (strudelMirror?.isBlockBasedEvalEnabled) {
|
||||||
|
evalBlock(strudelMirror);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return onEvaluate?.();
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Ctrl-.',
|
key: 'Ctrl-.',
|
||||||
@@ -171,6 +187,7 @@ export class StrudelMirror {
|
|||||||
this.onDraw = onDraw || this.draw;
|
this.onDraw = onDraw || this.draw;
|
||||||
this.id = id || s4();
|
this.id = id || s4();
|
||||||
this.solo = solo;
|
this.solo = solo;
|
||||||
|
this.isBlockBasedEvalEnabled = false; // Will be updated via updateSettings()
|
||||||
|
|
||||||
this.drawer = new Drawer((haps, time, _, painters) => {
|
this.drawer = new Drawer((haps, time, _, painters) => {
|
||||||
const currentFrame = haps.filter((hap) => hap.isActive(time));
|
const currentFrame = haps.filter((hap) => hap.isActive(time));
|
||||||
@@ -201,20 +218,28 @@ export class StrudelMirror {
|
|||||||
cleanupDraw(true, id);
|
cleanupDraw(true, id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeEval: async () => {
|
beforeEval: async ({ blockBased } = {}) => {
|
||||||
cleanupDraw(true, id);
|
// 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 this.prebaked;
|
||||||
await replOptions?.beforeEval?.();
|
await replOptions?.beforeEval?.();
|
||||||
},
|
},
|
||||||
afterEval: (options) => {
|
afterEval: (options) => {
|
||||||
// remember for when highlighting is toggled on
|
// remember for when highlighting is toggled on
|
||||||
this.miniLocations = options.meta?.miniLocations;
|
this.miniLocations = options.meta?.miniLocations || [];
|
||||||
this.widgets = options.meta?.widgets;
|
this.widgets = options.meta?.widgets || [];
|
||||||
|
|
||||||
const sliders = this.widgets.filter((w) => w.type === 'slider');
|
const sliders = this.widgets.filter((w) => w.type === 'slider');
|
||||||
updateSliderWidgets(this.editor, sliders);
|
|
||||||
const widgets = this.widgets.filter((w) => w.type !== 'slider');
|
const widgets = this.widgets.filter((w) => w.type !== 'slider');
|
||||||
updateWidgets(this.editor, widgets);
|
// range-aware update for block-based evaluation
|
||||||
updateMiniLocations(this.editor, this.miniLocations);
|
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);
|
replOptions?.afterEval?.(options);
|
||||||
// if no painters are set (.onPaint was not called), then we only need
|
// if no painters are set (.onPaint was not called), then we only need
|
||||||
// the present moment (for highlighting)
|
// the present moment (for highlighting)
|
||||||
@@ -222,8 +247,14 @@ export class StrudelMirror {
|
|||||||
this.drawer.setDrawTime(drawTime);
|
this.drawer.setDrawTime(drawTime);
|
||||||
// invalidate drawer after we've set the appropriate drawTime
|
// invalidate drawer after we've set the appropriate drawTime
|
||||||
this.drawer.invalidate(this.repl.scheduler);
|
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({
|
this.editor = initEditor({
|
||||||
root,
|
root,
|
||||||
initialCode,
|
initialCode,
|
||||||
@@ -236,7 +267,9 @@ export class StrudelMirror {
|
|||||||
onEvaluate: () => this.evaluate(),
|
onEvaluate: () => this.evaluate(),
|
||||||
onStop: () => this.stop(),
|
onStop: () => this.stop(),
|
||||||
mondo: replOptions.mondo,
|
mondo: replOptions.mondo,
|
||||||
|
strudelMirror: this,
|
||||||
});
|
});
|
||||||
|
|
||||||
const cmEditor = this.root.querySelector('.cm-editor');
|
const cmEditor = this.root.querySelector('.cm-editor');
|
||||||
if (cmEditor) {
|
if (cmEditor) {
|
||||||
this.root.style.display = 'block';
|
this.root.style.display = 'block';
|
||||||
@@ -260,6 +293,9 @@ export class StrudelMirror {
|
|||||||
// Handle global evaluation requests (e.g., from Vim :w)
|
// Handle global evaluation requests (e.g., from Vim :w)
|
||||||
this.onEvaluateRequest = (e) => {
|
this.onEvaluateRequest = (e) => {
|
||||||
try {
|
try {
|
||||||
|
if (e.detail.view !== this.editor) {
|
||||||
|
return; // ignore events from other editors
|
||||||
|
}
|
||||||
// Evaluate current editor on repl-evaluate
|
// Evaluate current editor on repl-evaluate
|
||||||
logger('[repl] evaluate via event');
|
logger('[repl] evaluate via event');
|
||||||
this.evaluate();
|
this.evaluate();
|
||||||
@@ -274,6 +310,9 @@ export class StrudelMirror {
|
|||||||
// Toggle comments requested from Vim (gc)
|
// Toggle comments requested from Vim (gc)
|
||||||
this.onToggleComment = (e) => {
|
this.onToggleComment = (e) => {
|
||||||
try {
|
try {
|
||||||
|
if (e.detail.view !== this.editor) {
|
||||||
|
return; // ignore events from other editors
|
||||||
|
}
|
||||||
// Honor selections; toggleLineComment handles both selections and
|
// Honor selections; toggleLineComment handles both selections and
|
||||||
// single line
|
// single line
|
||||||
toggleLineComment(this.editor);
|
toggleLineComment(this.editor);
|
||||||
@@ -306,13 +345,17 @@ export class StrudelMirror {
|
|||||||
this.flash();
|
this.flash();
|
||||||
await this.repl.evaluate(this.code, autostart);
|
await this.repl.evaluate(this.code, autostart);
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop() {
|
async stop() {
|
||||||
this.repl.scheduler.stop();
|
this.repl.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for global stop requests (e.g., from Vim :q)
|
// Listen for global stop requests (e.g., from Vim :q)
|
||||||
onStopRequest = (e) => {
|
onStopRequest = (e) => {
|
||||||
try {
|
try {
|
||||||
|
if (e.detail.view !== this.editor) {
|
||||||
|
return; // ignore events from other editors
|
||||||
|
}
|
||||||
this.stop();
|
this.stop();
|
||||||
e?.cancelable && e.preventDefault?.();
|
e?.cancelable && e.preventDefault?.();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -326,8 +369,8 @@ export class StrudelMirror {
|
|||||||
this.evaluate();
|
this.evaluate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
flash(ms) {
|
flash(ms, range) {
|
||||||
flash(this.editor, ms);
|
flash(this.editor, ms, range);
|
||||||
}
|
}
|
||||||
highlight(haps, time) {
|
highlight(haps, time) {
|
||||||
highlightMiniLocations(this.editor, time, haps);
|
highlightMiniLocations(this.editor, time, haps);
|
||||||
@@ -359,6 +402,10 @@ export class StrudelMirror {
|
|||||||
setLineWrappingEnabled(enabled) {
|
setLineWrappingEnabled(enabled) {
|
||||||
this.reconfigureExtension('isLineWrappingEnabled', enabled);
|
this.reconfigureExtension('isLineWrappingEnabled', enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setBlockBasedEvalEnabled(enabled) {
|
||||||
|
this.reconfigureExtension('isBlockBasedEvalEnabled', enabled);
|
||||||
|
}
|
||||||
setBracketMatchingEnabled(enabled) {
|
setBracketMatchingEnabled(enabled) {
|
||||||
this.reconfigureExtension('isBracketMatchingEnabled', enabled);
|
this.reconfigureExtension('isBracketMatchingEnabled', enabled);
|
||||||
}
|
}
|
||||||
@@ -380,6 +427,10 @@ export class StrudelMirror {
|
|||||||
for (let key in extensions) {
|
for (let key in extensions) {
|
||||||
this.reconfigureExtension(key, settings[key]);
|
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 };
|
const updated = { ...codemirrorSettings.get(), ...settings };
|
||||||
codemirrorSettings.set(updated);
|
codemirrorSettings.set(updated);
|
||||||
}
|
}
|
||||||
@@ -393,14 +444,26 @@ export class StrudelMirror {
|
|||||||
this.setFontSize(value);
|
this.setFontSize(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setCode(code) {
|
replaceCode(code, from, to) {
|
||||||
const changes = {
|
const changes = { from, to, insert: code };
|
||||||
from: 0,
|
|
||||||
to: this.editor.state.doc.length,
|
|
||||||
insert: code,
|
|
||||||
};
|
|
||||||
this.editor.dispatch({ changes });
|
this.editor.dispatch({ changes });
|
||||||
}
|
}
|
||||||
|
insertCode(code, position) {
|
||||||
|
this.replaceCode(code, position, position);
|
||||||
|
}
|
||||||
|
setCode(code) {
|
||||||
|
this.replaceCode(code, 0, this.editor.state.doc.length);
|
||||||
|
}
|
||||||
|
// used for debugging but could serve other purposes
|
||||||
|
getActiveWidgets() {
|
||||||
|
return getActiveWidgets(this.editor);
|
||||||
|
}
|
||||||
|
getSliderWidgets() {
|
||||||
|
return getSliderWidgets(this.editor);
|
||||||
|
}
|
||||||
|
getMiniLocations() {
|
||||||
|
return this.miniLocations;
|
||||||
|
}
|
||||||
clear() {
|
clear() {
|
||||||
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
|
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
|
||||||
this.onEvaluateRequest && document.removeEventListener('repl-evaluate', this.onEvaluateRequest);
|
this.onEvaluateRequest && document.removeEventListener('repl-evaluate', this.onEvaluateRequest);
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export const flashField = StateField.define({
|
|||||||
const mark = Decoration.mark({
|
const mark = Decoration.mark({
|
||||||
attributes: { style: `background-color: rgba(255,255,255, .4); filter: invert(10%)` },
|
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 {
|
} else {
|
||||||
flash = Decoration.set([]);
|
flash = Decoration.set([]);
|
||||||
}
|
}
|
||||||
@@ -29,8 +30,9 @@ export const flashField = StateField.define({
|
|||||||
provide: (f) => EditorView.decorations.from(f),
|
provide: (f) => EditorView.decorations.from(f),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const flash = (view, ms = 200) => {
|
export const flash = (view, ms = 200, range) => {
|
||||||
view.dispatch({ effects: setFlash.of(true) });
|
const flashData = range ? { range } : true;
|
||||||
|
view.dispatch({ effects: setFlash.of(flashData) });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
view.dispatch({ effects: setFlash.of(false) });
|
view.dispatch({ effects: setFlash.of(false) });
|
||||||
}, ms);
|
}, ms);
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { Decoration, EditorView } from '@codemirror/view';
|
|||||||
|
|
||||||
export const setMiniLocations = StateEffect.define();
|
export const setMiniLocations = StateEffect.define();
|
||||||
export const showMiniLocations = StateEffect.define();
|
export const showMiniLocations = StateEffect.define();
|
||||||
export const updateMiniLocations = (view, locations) => {
|
export const displayMiniLocations = StateEffect.define();
|
||||||
view.dispatch({ effects: setMiniLocations.of(locations) });
|
export const updateMiniLocations = (view, locations, range = null) => {
|
||||||
|
view.dispatch({ effects: setMiniLocations.of({ locations, range }) });
|
||||||
};
|
};
|
||||||
export const highlightMiniLocations = (view, atTime, haps) => {
|
export const highlightMiniLocations = (view, atTime, haps) => {
|
||||||
view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) });
|
view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) });
|
||||||
@@ -21,23 +22,54 @@ const miniLocations = StateField.define({
|
|||||||
|
|
||||||
for (let e of tr.effects) {
|
for (let e of tr.effects) {
|
||||||
if (e.is(setMiniLocations)) {
|
if (e.is(setMiniLocations)) {
|
||||||
// this is called on eval, with the mini locations obtained from the transpiler
|
//block-based eval case
|
||||||
// codemirror will automatically remap the marks when the document is edited
|
if (e.value.range) {
|
||||||
// create a mark for each mini location, adding the range to the spec to find it later
|
const stateMiniLocations = getMiniLocationsFromDecorations(locations);
|
||||||
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
|
|
||||||
);
|
|
||||||
|
|
||||||
locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet<Decoration>
|
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<Decoration>
|
||||||
|
} 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<Decoration>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,12 +107,83 @@ const visibleMiniLocations = StateField.define({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// // Derive the set of decorations from the miniLocations and visibleLocations
|
const displayMiniLocationsState = StateField.define({
|
||||||
const miniLocationHighlights = EditorView.decorations.compute([miniLocations, visibleMiniLocations], (state) => {
|
create() {
|
||||||
const iterator = state.field(miniLocations).iter();
|
return true; // default to showing miniLocations
|
||||||
const { haps } = state.field(visibleMiniLocations);
|
},
|
||||||
const builder = new RangeSetBuilder();
|
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) {
|
while (iterator.value) {
|
||||||
const {
|
const {
|
||||||
from,
|
from,
|
||||||
@@ -89,50 +192,48 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
|
|||||||
spec: { id },
|
spec: { id },
|
||||||
},
|
},
|
||||||
} = iterator;
|
} = iterator;
|
||||||
|
miniLocationsArray.push({
|
||||||
if (haps.has(id)) {
|
from,
|
||||||
const hap = haps.get(id);
|
to,
|
||||||
const color = hap.value?.color ?? 'var(--foreground)';
|
id,
|
||||||
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();
|
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) => {
|
export const isPatternHighlightingEnabled = (on, config) => {
|
||||||
on &&
|
// NOTE:
|
||||||
config &&
|
// Modified this function to always return the highlightExtension, and instead just toggle whether or not the miniLocations are displayed.
|
||||||
setTimeout(() => {
|
// This is because block based evaluation only updates regions of miniLocations, and those updates need to be kept track of constantly.
|
||||||
updateMiniLocations(config.editor, config.miniLocations);
|
// The setTimeout was also removed because it conflicted with the range-specific updates required by
|
||||||
}, 100);
|
// block based evaluation.
|
||||||
return on ? Prec.highest(highlightExtension) : [];
|
// 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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ export * from './slider.mjs';
|
|||||||
export * from './themes.mjs';
|
export * from './themes.mjs';
|
||||||
export * from './widget.mjs';
|
export * from './widget.mjs';
|
||||||
export { Vim } from './keybindings.mjs';
|
export { Vim } from './keybindings.mjs';
|
||||||
|
export * from './basicSetup.mjs';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { emacs } from '@replit/codemirror-emacs';
|
|||||||
import { vim, Vim } from '@replit/codemirror-vim';
|
import { vim, Vim } from '@replit/codemirror-vim';
|
||||||
// import { vim } from './vim_test.mjs';
|
// import { vim } from './vim_test.mjs';
|
||||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||||
|
import { helix, commands } from 'codemirror-helix';
|
||||||
import { logger } from '@strudel/core';
|
import { logger } from '@strudel/core';
|
||||||
|
|
||||||
const vscodePlugin = ViewPlugin.fromClass(
|
const vscodePlugin = ViewPlugin.fromClass(
|
||||||
@@ -20,6 +21,70 @@ const vscodePlugin = ViewPlugin.fromClass(
|
|||||||
);
|
);
|
||||||
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
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
|
// 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
|
// 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
|
// 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.
|
// internal actions and works with current selections/visual mode.
|
||||||
try {
|
try {
|
||||||
Vim.defineAction('strudelToggleComment', (cm) => {
|
Vim.defineAction('strudelToggleComment', (cm) => {
|
||||||
const view = cm?.view || cm;
|
const view = cm.cm6;
|
||||||
try {
|
try {
|
||||||
const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
|
const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
|
||||||
document.dispatchEvent(ev);
|
document.dispatchEvent(ev);
|
||||||
@@ -47,74 +112,23 @@ try {
|
|||||||
|
|
||||||
// :q to pause/stop
|
// :q to pause/stop
|
||||||
Vim.defineEx('quit', 'q', (cm) => {
|
Vim.defineEx('quit', 'q', (cm) => {
|
||||||
try {
|
const view = cm.cm6;
|
||||||
const view = cm?.view || cm;
|
logger('[vim] :q — stopping repl');
|
||||||
// First try dispatching our custom stop event, then fallback to Alt+.
|
replStop(view);
|
||||||
let handled = false;
|
|
||||||
try {
|
|
||||||
const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true });
|
|
||||||
handled = document.dispatchEvent(ev) === false;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error dispatching repl-stop event', e);
|
|
||||||
}
|
|
||||||
if (!handled) {
|
|
||||||
const altDot = new KeyboardEvent('keydown', {
|
|
||||||
key: '.',
|
|
||||||
code: 'Period',
|
|
||||||
altKey: true,
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
});
|
|
||||||
view?.dom?.dispatchEvent?.(altDot);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error dispatching :q stop event', e);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// :w to evaluate
|
// :w to evaluate
|
||||||
Vim.defineEx('write', 'w', (cm) => {
|
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 {
|
try {
|
||||||
view?.focus?.();
|
view.focus?.();
|
||||||
// Let the app know this came from Vim :w
|
// Let the app know this came from Vim :w
|
||||||
try {
|
try {
|
||||||
logger('[vim] :w — evaluating code');
|
logger('[vim] :w — evaluating code');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error logging Vim :w evaluation', e);
|
console.error('Error logging Vim :w evaluation', e);
|
||||||
}
|
}
|
||||||
// Dispatch a dedicated evaluate event first
|
replEval(view);
|
||||||
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) {
|
} catch (e) {
|
||||||
console.error('Error dispatching :w evaluation event', 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);
|
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 = {
|
const keymaps = {
|
||||||
vim,
|
vim,
|
||||||
emacs,
|
emacs,
|
||||||
codemirror: () => keymap.of(defaultKeymap),
|
codemirror: () => keymap.of(defaultKeymap),
|
||||||
vscode: vscodeExtension,
|
vscode: vscodeExtension,
|
||||||
|
helix: () => [helix(), helixCommands],
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Vim } from '@replit/codemirror-vim';
|
export { Vim } from '@replit/codemirror-vim';
|
||||||
|
|||||||
@@ -32,18 +32,19 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.18.4",
|
"@codemirror/autocomplete": "catalog:",
|
||||||
"@codemirror/commands": "^6.8.0",
|
"@codemirror/commands": "catalog:",
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "catalog:",
|
||||||
"@codemirror/language": "^6.10.8",
|
"@codemirror/language": "catalog:",
|
||||||
"@codemirror/search": "^6.5.8",
|
"@codemirror/search": "catalog:",
|
||||||
"@codemirror/state": "^6.5.1",
|
"@codemirror/state": "catalog:",
|
||||||
"@codemirror/view": "^6.36.2",
|
"@codemirror/view": "catalog:",
|
||||||
"@lezer/highlight": "^1.2.1",
|
"@lezer/highlight": "^1.2.1",
|
||||||
"@nanostores/persistent": "^0.10.2",
|
"@nanostores/persistent": "^0.10.2",
|
||||||
"@replit/codemirror-emacs": "^6.1.0",
|
"@replit/codemirror-emacs": "^6.1.0",
|
||||||
"@replit/codemirror-vim": "^6.3.0",
|
"@replit/codemirror-vim": "^6.3.0",
|
||||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||||
|
"codemirror-helix": "^0.5.0",
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
"@strudel/tonal": "workspace:*",
|
"@strudel/tonal": "workspace:*",
|
||||||
|
|||||||
+106
-17
@@ -2,11 +2,11 @@ import { ref, pure } from '@strudel/core';
|
|||||||
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
|
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
|
||||||
import { StateEffect } from '@codemirror/state';
|
import { StateEffect } from '@codemirror/state';
|
||||||
|
|
||||||
|
// Global state storage for all widget types
|
||||||
export let sliderValues = {};
|
export let sliderValues = {};
|
||||||
const getSliderID = (from) => `slider_${from}`;
|
|
||||||
|
|
||||||
export class SliderWidget extends WidgetType {
|
export class SliderWidget extends WidgetType {
|
||||||
constructor(value, min, max, from, to, step, view) {
|
constructor(value, min, max, from, to, step, view, id) {
|
||||||
super();
|
super();
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.min = min;
|
this.min = min;
|
||||||
@@ -16,10 +16,21 @@ export class SliderWidget extends WidgetType {
|
|||||||
this.to = to;
|
this.to = to;
|
||||||
this.step = step;
|
this.step = step;
|
||||||
this.view = view;
|
this.view = view;
|
||||||
|
this.id = id || `${from}:${to}`; // Range-based ID for stability
|
||||||
}
|
}
|
||||||
|
|
||||||
eq() {
|
eq(other) {
|
||||||
return false;
|
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() {
|
toDOM() {
|
||||||
@@ -38,6 +49,7 @@ export class SliderWidget extends WidgetType {
|
|||||||
slider.from = this.from;
|
slider.from = this.from;
|
||||||
slider.originalFrom = this.originalFrom;
|
slider.originalFrom = this.originalFrom;
|
||||||
slider.to = this.to;
|
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)';
|
slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)';
|
||||||
this.slider = slider;
|
this.slider = slider;
|
||||||
slider.addEventListener('input', (e) => {
|
slider.addEventListener('input', (e) => {
|
||||||
@@ -49,7 +61,7 @@ export class SliderWidget extends WidgetType {
|
|||||||
slider.originalValue = insert;
|
slider.originalValue = insert;
|
||||||
slider.value = insert;
|
slider.value = insert;
|
||||||
this.view.dispatch({ changes: change });
|
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 });
|
window.postMessage({ type: 'cm-slider', value: Number(next), id });
|
||||||
});
|
});
|
||||||
return wrap;
|
return wrap;
|
||||||
@@ -62,19 +74,60 @@ export class SliderWidget extends WidgetType {
|
|||||||
|
|
||||||
export const setSliderWidgets = StateEffect.define();
|
export const setSliderWidgets = StateEffect.define();
|
||||||
|
|
||||||
export const updateSliderWidgets = (view, widgets) => {
|
export const setSliderWidgetsInRange = StateEffect.define();
|
||||||
view.dispatch({ effects: setSliderWidgets.of(widgets) });
|
|
||||||
|
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) {
|
function getSliders(widgetConfigs, view) {
|
||||||
return widgetConfigs
|
return (
|
||||||
.filter((w) => w.type === 'slider')
|
widgetConfigs
|
||||||
.map(({ from, to, value, min, max, step }) => {
|
.filter((w) => w.type === 'slider')
|
||||||
return Decoration.widget({
|
// Deduplicate sliders that might appear multiple times (e.g., during paste operations)
|
||||||
widget: new SliderWidget(value, min, max, from, to, step, view),
|
.filter((slider, index, self) => index === self.findIndex((s) => s.from === slider.from && s.to === slider.to))
|
||||||
side: 0,
|
.sort((a, b) => a.from - b.from)
|
||||||
}).range(from /* , to */);
|
.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(
|
export const sliderPlugin = ViewPlugin.fromClass(
|
||||||
@@ -101,7 +154,42 @@ export const sliderPlugin = ViewPlugin.fromClass(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let e of tr.effects) {
|
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));
|
this.decorations = Decoration.set(getSliders(e.value, update.view));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,6 +220,7 @@ export let sliderWithID = (id, value, min, max) => {
|
|||||||
sliderValues[id] = value; // sync state at eval time (code -> state)
|
sliderValues[id] = value; // sync state at eval time (code -> state)
|
||||||
return ref(() => sliderValues[id]); // use state at query time
|
return ref(() => sliderValues[id]); // use state at query time
|
||||||
};
|
};
|
||||||
|
|
||||||
// update state when sliders are moved
|
// update state when sliders are moved
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.addEventListener('message', (e) => {
|
window.addEventListener('message', (e) => {
|
||||||
@@ -140,7 +229,7 @@ if (typeof window !== 'undefined') {
|
|||||||
// update state when slider is moved
|
// update state when slider is moved
|
||||||
sliderValues[e.data.id] = e.data.value;
|
sliderValues[e.data.id] = e.data.value;
|
||||||
} else {
|
} 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)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+194
-46
@@ -1,55 +1,111 @@
|
|||||||
import { StateEffect, StateField } from '@codemirror/state';
|
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 { getWidgetID, registerWidgetType } from '@strudel/transpiler';
|
||||||
import { Pattern } from '@strudel/core';
|
import { Pattern } from '@strudel/core';
|
||||||
|
|
||||||
export const addWidget = StateEffect.define({
|
export const setWidgets = StateEffect.define();
|
||||||
map: ({ from, to }, change) => {
|
|
||||||
return { from: change.mapPos(from), to: change.mapPos(to) };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateWidgets = (view, widgets) => {
|
export const setWidgetsInRange = StateEffect.define();
|
||||||
view.dispatch({ effects: addWidget.of(widgets) });
|
|
||||||
|
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) {
|
function getWidgets(widgetConfigs, view) {
|
||||||
return (
|
const filtered = widgetConfigs
|
||||||
widgetConfigs
|
// Filter to widget configs only (exclude sliders)
|
||||||
// codemirror throws an error if we don't sort
|
.filter((w) => w && w.type && w.type !== 'slider')
|
||||||
.sort((a, b) => a.to - b.to)
|
// Deduplicate widgets by ID, matching slider behavior for stable widget identity
|
||||||
.map((widgetConfig) => {
|
.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({
|
return Decoration.widget({
|
||||||
widget: new BlockWidget(widgetConfig),
|
widget: new BlockWidget(widgetConfig, view),
|
||||||
side: 0,
|
side: 0,
|
||||||
block: true,
|
}).range(widgetConfig.to || widgetConfig.from || 0);
|
||||||
}).range(widgetConfig.to);
|
} catch (error) {
|
||||||
})
|
console.error('error creating widget', error);
|
||||||
);
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean); // Remove any null results from failed creations
|
||||||
}
|
}
|
||||||
|
|
||||||
const widgetField = StateField.define(
|
export const widgetPlugin = ViewPlugin.fromClass(
|
||||||
/* <DecorationSet> */ {
|
class {
|
||||||
create() {
|
decorations; //: DecorationSet
|
||||||
return Decoration.none;
|
|
||||||
},
|
constructor(view /* : EditorView */) {
|
||||||
update(widgets, tr) {
|
this.decorations = Decoration.set([]);
|
||||||
widgets = widgets.map(tr.changes);
|
}
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(addWidget)) {
|
update(update /* : ViewUpdate */) {
|
||||||
try {
|
update.transactions.forEach((tr) => {
|
||||||
widgets = widgets.update({
|
if (tr.docChanged) {
|
||||||
filter: () => false,
|
this.decorations = this.decorations.map(tr.changes);
|
||||||
add: getWidgets(e.value),
|
const iterator = this.decorations.iter();
|
||||||
});
|
// Apply changes to iterator.from and iterator.to if docChanged
|
||||||
} catch (error) {
|
while (iterator.value) {
|
||||||
console.log('err', error);
|
// when the widgets are moved, we need to tell the dom node the current position
|
||||||
|
// this is important because the widget functions have to work with the dom node
|
||||||
|
if (iterator.value?.widget instanceof BlockWidget) {
|
||||||
|
iterator.value.widget.from = iterator.from;
|
||||||
|
iterator.value.widget.to = iterator.to;
|
||||||
|
}
|
||||||
|
iterator.next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
for (let e of tr.effects) {
|
||||||
return widgets;
|
if (e.is(setWidgetsInRange)) {
|
||||||
},
|
// Block-aware widget update logic
|
||||||
provide: (f) => EditorView.decorations.from(f),
|
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 {
|
export class BlockWidget extends WidgetType {
|
||||||
constructor(widgetConfig) {
|
constructor(widgetConfig, view) {
|
||||||
super();
|
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;
|
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() {
|
toDOM() {
|
||||||
const id = getWidgetID(this.widgetConfig);
|
let wrap = document.createElement('span');
|
||||||
const el = widgetElements[id];
|
wrap.setAttribute('aria-hidden', 'true');
|
||||||
return el;
|
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) {
|
ignoreEvent(e) {
|
||||||
return true;
|
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
|
// widget implementer API to create a new widget type
|
||||||
export function registerWidget(type, fn) {
|
export function registerWidget(type, fn) {
|
||||||
|
|||||||
+265
-239
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,34 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const strudelScope = {};
|
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) => {
|
export const evalScope = async (...args) => {
|
||||||
const results = await Promise.allSettled(args);
|
const results = await Promise.allSettled(args);
|
||||||
|
|||||||
+221
-148
@@ -818,7 +818,6 @@ export class Pattern {
|
|||||||
* @name layer
|
* @name layer
|
||||||
* @tags combiners
|
* @tags combiners
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @synonyms apply
|
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*8"
|
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*8"
|
||||||
@@ -1055,91 +1054,135 @@ function _composeOp(a, b, func) {
|
|||||||
return func(a, b);
|
return func(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make composers
|
// pattern composers
|
||||||
(function () {
|
const COMPOSERS = {
|
||||||
// pattern composers
|
/**
|
||||||
const composers = {
|
* When called on a pattern `a`, with a input pattern `b` (`a.set(b)`),
|
||||||
set: [(a, b) => b],
|
* combines `a` and `b` such that anything defined in `b`
|
||||||
keep: [(a) => a],
|
* and anything defined in `a` that is *not* defined in `b`
|
||||||
keepif: [(a, b) => (b ? a : undefined)],
|
* 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
|
// numerical functions
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Assumes a pattern of numbers. Adds the given number to each item in the pattern.
|
* Assumes a pattern of numbers. Adds the given number to each item in the pattern.
|
||||||
* @name add
|
* @name add
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @tags math
|
* @tags math
|
||||||
* @example
|
* @example
|
||||||
* // Here, the triad 0, 2, 4 is shifted by different amounts
|
* // Here, the triad 0, 2, 4 is shifted by different amounts
|
||||||
* n("0 2 4".add("<0 3 4 0>")).scale("C:major")
|
* n("0 2 4".add("<0 3 4 0>")).scale("C:major")
|
||||||
* // Without add, the equivalent would be:
|
* // Without add, the equivalent would be:
|
||||||
* // n("<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>").scale("C:major")
|
* // n("<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>").scale("C:major")
|
||||||
* @example
|
* @example
|
||||||
* // You can also use add with notes:
|
* // You can also use add with notes:
|
||||||
* note("c3 e3 g3".add("<0 5 7 0>"))
|
* note("c3 e3 g3".add("<0 5 7 0>"))
|
||||||
* // Behind the scenes, the notes are converted to midi numbers:
|
* // Behind the scenes, the notes are converted to midi numbers:
|
||||||
* // note("48 52 55".add("<0 5 7 0>"))
|
* // note("48 52 55".add("<0 5 7 0>"))
|
||||||
*/
|
*/
|
||||||
add: [numeralArgs((a, b) => a + b)], // support string concatenation
|
add: [numeralArgs((a, b) => a + b)], // support string concatenation
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Like add, but the given numbers are subtracted.
|
* Like add, but the given numbers are subtracted.
|
||||||
* @name sub
|
* @name sub
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @tags math
|
* @tags math
|
||||||
* @example
|
* @example
|
||||||
* n("0 2 4".sub("<0 1 2 3>")).scale("C4:minor")
|
* n("0 2 4".sub("<0 1 2 3>")).scale("C4:minor")
|
||||||
* // See add for more information.
|
* // See add for more information.
|
||||||
*/
|
*/
|
||||||
sub: [numeralArgs((a, b) => a - b)],
|
sub: [numeralArgs((a, b) => a - b)],
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Multiplies each number by the given factor.
|
* Multiplies each number by the given factor.
|
||||||
* @name mul
|
* @name mul
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @tags math
|
* @tags math
|
||||||
* @example
|
* @example
|
||||||
* "<1 1.5 [1.66, <2 2.33>]>*4".mul(150).freq()
|
* "<1 1.5 [1.66, <2 2.33>]>*4".mul(150).freq()
|
||||||
*/
|
*/
|
||||||
mul: [numeralArgs((a, b) => a * b)],
|
mul: [numeralArgs((a, b) => a * b)],
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Divides each number by the given factor.
|
* Divides each number by the given factor.
|
||||||
* @name div
|
* @name div
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @tags math
|
* @tags math
|
||||||
*/
|
*/
|
||||||
div: [numeralArgs((a, b) => a / b)],
|
div: [numeralArgs((a, b) => a / b)],
|
||||||
mod: [numeralArgs(_mod)],
|
mod: [numeralArgs(_mod)],
|
||||||
pow: [numeralArgs(Math.pow)],
|
pow: [numeralArgs(Math.pow)],
|
||||||
log2: [numeralArgs(Math.log2)],
|
band: [numeralArgs((a, b) => a & b)],
|
||||||
band: [numeralArgs((a, b) => a & b)],
|
bor: [numeralArgs((a, b) => a | b)],
|
||||||
bor: [numeralArgs((a, b) => a | b)],
|
bxor: [numeralArgs((a, b) => a ^ b)],
|
||||||
bxor: [numeralArgs((a, b) => a ^ b)],
|
blshift: [numeralArgs((a, b) => a << b)],
|
||||||
blshift: [numeralArgs((a, b) => a << b)],
|
brshift: [numeralArgs((a, b) => a >> b)],
|
||||||
brshift: [numeralArgs((a, b) => a >> b)],
|
|
||||||
|
|
||||||
// TODO - force numerical comparison if both look like numbers?
|
// TODO - force numerical comparison if both look like numbers?
|
||||||
lt: [(a, b) => a < b],
|
lt: [(a, b) => a < b],
|
||||||
gt: [(a, b) => a > b],
|
gt: [(a, b) => a > b],
|
||||||
lte: [(a, b) => a <= b],
|
lte: [(a, b) => a <= b],
|
||||||
gte: [(a, b) => a >= b],
|
gte: [(a, b) => a >= b],
|
||||||
eq: [(a, b) => a == b],
|
eq: [(a, b) => a == b],
|
||||||
eqt: [(a, b) => a === b],
|
eqt: [(a, b) => a === b],
|
||||||
ne: [(a, b) => a != b],
|
ne: [(a, b) => a != b],
|
||||||
net: [(a, b) => a !== b],
|
net: [(a, b) => a !== b],
|
||||||
and: [(a, b) => a && b],
|
and: [(a, b) => a && b],
|
||||||
or: [(a, b) => a || b],
|
or: [(a, b) => a || b],
|
||||||
|
|
||||||
// bitwise ops
|
// bitwise ops
|
||||||
func: [(a, b) => b(a)],
|
func: [(a, b) => b(a)],
|
||||||
};
|
};
|
||||||
|
|
||||||
const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart', 'Poly'];
|
|
||||||
|
|
||||||
|
const _setupAlignments = () => {
|
||||||
// generate methods to do what and how
|
// 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
|
// make plain version, e.g. pat._add(value) adds that plain value
|
||||||
// to all the values in pat
|
// to all the values in pat
|
||||||
Pattern.prototype['_' + what] = function (value) {
|
Pattern.prototype['_' + what] = function (value) {
|
||||||
@@ -1148,16 +1191,18 @@ function _composeOp(a, b, func) {
|
|||||||
|
|
||||||
// make patternified monster version
|
// make patternified monster version
|
||||||
Object.defineProperty(Pattern.prototype, what, {
|
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
|
// a getter that returns a function, so 'pat' can be
|
||||||
// accessed by closures that are methods of that function..
|
// accessed by closures that are methods of that function..
|
||||||
get: function () {
|
get: function () {
|
||||||
const pat = this;
|
const pat = this;
|
||||||
|
|
||||||
// wrap the 'in' function as default behaviour
|
// 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
|
// add methods to that function for each behaviour
|
||||||
for (const how of hows) {
|
for (const how of ALIGNMENTS) {
|
||||||
wrapper[how.toLowerCase()] = function (...other) {
|
wrapper[how.toLowerCase()] = function (...other) {
|
||||||
var howpat = pat;
|
var howpat = pat;
|
||||||
other = sequence(other);
|
other = sequence(other);
|
||||||
@@ -1182,15 +1227,23 @@ function _composeOp(a, b, func) {
|
|||||||
return wrapper;
|
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
|
// binary composers
|
||||||
/**
|
/**
|
||||||
* Applies the given structure to the pattern:
|
* Applies the given structure to the pattern:
|
||||||
@@ -1249,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
|
// aliases
|
||||||
export const polyrhythm = stack;
|
export const polyrhythm = stack;
|
||||||
export const pr = stack;
|
export const pr = stack;
|
||||||
@@ -1536,10 +1611,10 @@ export function arrange(...sections) {
|
|||||||
* @tags combiners
|
* @tags combiners
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
seqPLoop([0, 2, "bd(3,8)"],
|
seqPLoop(
|
||||||
[1, 3, "cp(3,8)"]
|
[0, 2, "bd(3,8)"],
|
||||||
)
|
[1, 3, "cp(3,8)"]
|
||||||
.sound()
|
).sound()
|
||||||
*/
|
*/
|
||||||
export function seqPLoop(...parts) {
|
export function seqPLoop(...parts) {
|
||||||
let total = Fraction(0);
|
let total = Fraction(0);
|
||||||
@@ -1582,7 +1657,7 @@ export function sequence(...pats) {
|
|||||||
|
|
||||||
/** Like **cat**, but the items are crammed into one cycle.
|
/** Like **cat**, but the items are crammed into one cycle.
|
||||||
* @tags combiners
|
* @tags combiners
|
||||||
* @synonyms seq, fastcat
|
* @synonyms fastcat
|
||||||
* @example
|
* @example
|
||||||
* seq("e5", "b4", ["d5", "c5"]).note()
|
* seq("e5", "b4", ["d5", "c5"]).note()
|
||||||
* // "e5 b4 [d5 c5]".note()
|
* // "e5 b4 [d5 c5]".note()
|
||||||
@@ -1788,6 +1863,8 @@ export const floor = register('floor', function (pat) {
|
|||||||
return pat.asNumber().fmap((v) => Math.floor(v));
|
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
|
* 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`
|
* their mathematical ceiling. E.g. `3.2` replaced with `4`, and `-4.2`
|
||||||
@@ -2109,13 +2186,12 @@ 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 temporal
|
* @tags combiners
|
||||||
* @name apply
|
* @name apply
|
||||||
* @example
|
* @example
|
||||||
* "<c3 eb3 g3>".scale('C minor').apply(scaleTranspose("0,2,4")).note()
|
* "<c3 eb3 g3>".scale('C minor').apply(scaleTranspose("0,2,4")).note()
|
||||||
*/
|
*/
|
||||||
// TODO: remove or dedupe with layer?
|
|
||||||
export const apply = register('apply', function (func, pat) {
|
export const apply = register('apply', function (func, pat) {
|
||||||
return func(pat);
|
return func(pat);
|
||||||
});
|
});
|
||||||
@@ -2472,7 +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.
|
* The jux function creates strange stereo effects, by applying a function to a pattern, but only in the right-hand channel.
|
||||||
* @tags temporal
|
* @tags temporal, superdough
|
||||||
* @example
|
* @example
|
||||||
* s("bd lt [~ ht] mt cp ~ bd hh").jux(rev)
|
* s("bd lt [~ ht] mt cp ~ bd hh").jux(rev)
|
||||||
* @example
|
* @example
|
||||||
@@ -2486,7 +2562,7 @@ export const jux = register('jux', function (func, pat) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Superimpose and offset multiple times, applying the given function each time.
|
* Superimpose and offset multiple times, applying the given function each time.
|
||||||
* @tags temporal
|
* @tags temporal, functional
|
||||||
* @name echoWith
|
* @name echoWith
|
||||||
* @synonyms echowith, stutWith, stutwith
|
* @synonyms echowith, stutWith, stutwith
|
||||||
* @param {number} times how many times to repeat
|
* @param {number} times how many times to repeat
|
||||||
@@ -2660,7 +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).
|
* Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle).
|
||||||
* @tags temporal
|
* @tags temporal, functional
|
||||||
* @name chunk
|
* @name chunk
|
||||||
* @synonyms slowChunk, slowchunk
|
* @synonyms slowChunk, slowchunk
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
@@ -2824,7 +2900,7 @@ Pattern.prototype.tag = function (tag) {
|
|||||||
/**
|
/**
|
||||||
* Filters haps using the given function
|
* Filters haps using the given function
|
||||||
* @name filter
|
* @name filter
|
||||||
* @tags temporal
|
* @tags temporal, functional
|
||||||
* @param {Function} test function to test Hap
|
* @param {Function} test function to test Hap
|
||||||
* @example
|
* @example
|
||||||
* s("hh!7 oh").filter(hap => hap.value.s === 'hh')
|
* s("hh!7 oh").filter(hap => hap.value.s === 'hh')
|
||||||
@@ -2834,7 +2910,7 @@ export const filter = register('filter', (test, pat) => pat.withHaps((haps) => h
|
|||||||
/**
|
/**
|
||||||
* Filters haps by their begin time
|
* Filters haps by their begin time
|
||||||
* @name filterWhen
|
* @name filterWhen
|
||||||
* @tags temporal
|
* @tags temporal, functional
|
||||||
* @param {Function} test function to test Hap.whole.begin
|
* @param {Function} test function to test Hap.whole.begin
|
||||||
* @example
|
* @example
|
||||||
* oneCycle: s("bd*4").filterWhen((t) => t < 1)
|
* oneCycle: s("bd*4").filterWhen((t) => t < 1)
|
||||||
@@ -2844,7 +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.
|
* Use within to apply a function to only a part of a pattern.
|
||||||
* @name within
|
* @name within
|
||||||
* @tags temporal
|
* @tags temporal, functional
|
||||||
* @param {number} start start within cycle (0 - 1)
|
* @param {number} start start within cycle (0 - 1)
|
||||||
* @param {number} end end within cycle (0 - 1). Must be > start
|
* @param {number} end end within cycle (0 - 1). Must be > start
|
||||||
* @param {Function} func function to be applied to the sub-pattern
|
* @param {Function} func function to be applied to the sub-pattern
|
||||||
@@ -2919,7 +2995,7 @@ export function _match(span, hap_p) {
|
|||||||
* *Experimental*
|
* *Experimental*
|
||||||
*
|
*
|
||||||
* Speeds a pattern up or down, to fit to the given number of steps per cycle.
|
* Speeds a pattern up or down, to fit to the given number of steps per cycle.
|
||||||
* @tags temporal
|
* @tags stepwise
|
||||||
* @example
|
* @example
|
||||||
* sound("bd sd cp").pace(4)
|
* sound("bd sd cp").pace(4)
|
||||||
* // The same as sound("{bd sd cp}%4") or sound("<bd sd cp>*4")
|
* // The same as sound("{bd sd cp}%4") or sound("<bd sd cp>*4")
|
||||||
@@ -2961,7 +3037,7 @@ export function _polymeterListSteps(steps, ...args) {
|
|||||||
* *Experimental*
|
* *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.
|
* Aligns the steps of the patterns, creating polymeters. The patterns are repeated until they all fit the cycle. For example, in the below the first pattern is repeated twice, and the second is repeated three times, to fit the lowest common multiple of six steps.
|
||||||
* @tags temporal
|
* @tags stepwise
|
||||||
* @synonyms pm
|
* @synonyms pm
|
||||||
* @example
|
* @example
|
||||||
* // The same as note("{c eb g, c2 g2}%6")
|
* // The same as note("{c eb g, c2 g2}%6")
|
||||||
@@ -2994,7 +3070,7 @@ export function polymeter(...args) {
|
|||||||
* The steps can either be inferred from the pattern, or provided as a [length, pattern] pair.
|
* The steps can either be inferred from the pattern, or provided as a [length, pattern] pair.
|
||||||
* Has the alias `timecat`.
|
* Has the alias `timecat`.
|
||||||
* @name stepcat
|
* @name stepcat
|
||||||
* @tags combiners
|
* @tags stepwise
|
||||||
* @synonyms timeCat, timecat
|
* @synonyms timeCat, timecat
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
@@ -3052,7 +3128,7 @@ export function stepcat(...timepats) {
|
|||||||
* Concatenates patterns stepwise, according to an inferred 'steps per cycle'.
|
* 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.
|
* Similar to `stepcat`, but if an argument is a list, the whole pattern will alternate between the elements in the list.
|
||||||
*
|
*
|
||||||
* @tags combiners
|
* @tags stepwise
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* stepalt(["bd cp", "mt"], "bd").sound()
|
* stepalt(["bd cp", "mt"], "bd").sound()
|
||||||
@@ -3079,7 +3155,7 @@ export function stepalt(...groups) {
|
|||||||
*
|
*
|
||||||
* Takes the given number of steps from a pattern (dropping the rest).
|
* 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.
|
* A positive number will take steps from the start of a pattern, and a negative number from the end.
|
||||||
* @tags temporal
|
* @tags stepwise
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* "bd cp ht mt".take("2").sound()
|
* "bd cp ht mt".take("2").sound()
|
||||||
@@ -3124,7 +3200,7 @@ export const take = stepRegister('take', function (i, pat) {
|
|||||||
*
|
*
|
||||||
* Drops the given number of steps from a pattern.
|
* 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.
|
* A positive number will drop steps from the start of a pattern, and a negative number from the end.
|
||||||
* @tags temporal
|
* @tags stepwise
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* "tha dhi thom nam".drop("1").sound().bank("mridangam")
|
* "tha dhi thom nam".drop("1").sound().bank("mridangam")
|
||||||
@@ -3153,7 +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
|
* `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
|
* 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"`.
|
* `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`.
|
||||||
* @tags temporal
|
* @tags stepwise
|
||||||
* @example
|
* @example
|
||||||
* stepcat(
|
* stepcat(
|
||||||
* sound("bd bd - cp").extend(2),
|
* sound("bd bd - cp").extend(2),
|
||||||
@@ -3172,7 +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"`.
|
* `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
|
* TODO: find out how this function differs from extend
|
||||||
* @tags temporal
|
* @tags stepwise
|
||||||
* @example
|
* @example
|
||||||
* stepcat(
|
* stepcat(
|
||||||
* sound("bd bd - cp").replicate(2),
|
* sound("bd bd - cp").replicate(2),
|
||||||
@@ -3187,7 +3263,7 @@ export const replicate = stepRegister('replicate', function (factor, pat) {
|
|||||||
* *Experimental*
|
* *Experimental*
|
||||||
*
|
*
|
||||||
* Expands the step size of the pattern by the given factor.
|
* Expands the step size of the pattern by the given factor.
|
||||||
* @tags temporal
|
* @tags stepwise
|
||||||
* @example
|
* @example
|
||||||
* sound("tha dhi thom nam").bank("mridangam").expand("3 2 1 1 2 3").pace(8)
|
* sound("tha dhi thom nam").bank("mridangam").expand("3 2 1 1 2 3").pace(8)
|
||||||
*/
|
*/
|
||||||
@@ -3199,7 +3275,7 @@ export const expand = stepRegister('expand', function (factor, pat) {
|
|||||||
* *Experimental*
|
* *Experimental*
|
||||||
*
|
*
|
||||||
* Contracts the step size of the pattern by the given factor. See also `expand`.
|
* Contracts the step size of the pattern by the given factor. See also `expand`.
|
||||||
* @tags temporal
|
* @tags stepwise
|
||||||
* @example
|
* @example
|
||||||
* sound("tha dhi thom nam").bank("mridangam").contract("3 2 1 1 2 3").pace(8)
|
* sound("tha dhi thom nam").bank("mridangam").contract("3 2 1 1 2 3").pace(8)
|
||||||
*/
|
*/
|
||||||
@@ -3254,7 +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 `:`),
|
* 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.
|
* that number of times.
|
||||||
* A positive number will progressively drop steps from the start of a pattern, and a negative number from the end.
|
* A positive number will progressively drop steps from the start of a pattern, and a negative number from the end.
|
||||||
* @tags temporal
|
* @tags stepwise
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* "tha dhi thom nam".shrink("1").sound()
|
* "tha dhi thom nam".shrink("1").sound()
|
||||||
@@ -3294,7 +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 `:`),
|
* 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.
|
* that number of times.
|
||||||
* A positive number will progressively grow steps from the start of a pattern, and a negative number from the end.
|
* A positive number will progressively grow steps from the start of a pattern, and a negative number from the end.
|
||||||
* @tags temporal
|
* @tags stepwise
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* "tha dhi thom nam".grow("1").sound()
|
* "tha dhi thom nam".grow("1").sound()
|
||||||
@@ -3335,7 +3411,7 @@ export const grow = register(
|
|||||||
* on successive repetitions. The patterns are added together stepwise, with all repetitions taking place over a single cycle. Using `pace` to set the
|
* 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.
|
* number of steps per cycle is therefore usually recommended.
|
||||||
*
|
*
|
||||||
* @tags combiners
|
* @tags stepwise
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* "[c g]".tour("e f", "e f g", "g f e c").note()
|
* "[c g]".tour("e f", "e f g", "g f e c").note()
|
||||||
@@ -3362,7 +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.
|
* '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.
|
* Using `pace` to set the number of steps per cycle is therefore usually recommended.
|
||||||
*
|
*
|
||||||
* @tags combiners
|
* @tags stepwise
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* zip("e f", "e f g", "g [f e] a f4 c").note()
|
* zip("e f", "e f g", "g [f e] a f4 c").note()
|
||||||
@@ -3414,7 +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'.
|
* 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.
|
* It turns a pattern of samples into a pattern of parts of samples.
|
||||||
* @name chop
|
* @name chop
|
||||||
* @tags temporal
|
* @tags samples
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
@@ -3445,7 +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.
|
* Cuts each sample into the given number of parts, triggering progressive portions of each sample at each loop.
|
||||||
* @name striate
|
* @name striate
|
||||||
* @tags temporal
|
* @tags samples
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
@@ -3464,14 +3540,13 @@ export const striate = register('striate', function (n, pat) {
|
|||||||
/**
|
/**
|
||||||
* Makes the sample fit the given number of cycles by changing the speed.
|
* Makes the sample fit the given number of cycles by changing the speed.
|
||||||
* @name loopAt
|
* @name loopAt
|
||||||
* @tags temporal
|
* @tags samples, pitch
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
|
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
|
||||||
* s("rhodes").loopAt(2)
|
* s("rhodes").loopAt(2)
|
||||||
*/
|
*/
|
||||||
// TODO - global cps clock
|
|
||||||
const _loopAt = function (factor, pat, cps = 0.5) {
|
const _loopAt = function (factor, pat, cps = 0.5) {
|
||||||
return pat
|
return pat
|
||||||
.speed((1 / factor) * cps)
|
.speed((1 / factor) * cps)
|
||||||
@@ -3479,11 +3554,16 @@ const _loopAt = function (factor, pat, cps = 0.5) {
|
|||||||
.slow(factor);
|
.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.
|
* 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.
|
* Instead of a number, it also accepts a list of numbers from 0 to 1 to slice at specific points.
|
||||||
* @name slice
|
* @name slice
|
||||||
* @tags temporal
|
* @tags samples
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
@@ -3537,7 +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.
|
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
|
||||||
* @name splice
|
* @name splice
|
||||||
* @tags temporal
|
* @tags samples, pitch
|
||||||
* @example
|
* @example
|
||||||
* samples('github:tidalcycles/dirt-samples')
|
* samples('github:tidalcycles/dirt-samples')
|
||||||
* s("breaks165")
|
* s("breaks165")
|
||||||
@@ -3566,16 +3646,11 @@ export const splice = register(
|
|||||||
false, // turns off auto-patternification
|
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.
|
* Makes the sample fit its event duration. Good for rhythmical loops like drum breaks.
|
||||||
* Similar to `loopAt`.
|
* Similar to `loopAt`.
|
||||||
* @name fit
|
* @name fit
|
||||||
* @tags temporal
|
* @tags samples, pitch
|
||||||
* @example
|
* @example
|
||||||
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
|
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
|
||||||
* s("rhodes/2").fit()
|
* s("rhodes/2").fit()
|
||||||
@@ -3597,18 +3672,16 @@ export const fit = register('fit', (pat) =>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes the sample fit the given number of cycles and cps value, by
|
* 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
|
* changing the speed. deprecated: use loopAt or fit instead, together with setCps / setCpm.
|
||||||
* given by a global clock and this function will be
|
|
||||||
* deprecated/removed.
|
|
||||||
* @name loopAtCps
|
* @name loopAtCps
|
||||||
* @tags temporal
|
* @tags samples, pitch
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
|
* @deprecated
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
|
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
|
||||||
* s("rhodes").loopAtCps(4,1.5).cps(1.5)
|
* s("rhodes").loopAtCps(4,1.5).cps(1.5)
|
||||||
*/
|
*/
|
||||||
// TODO - global cps clock
|
|
||||||
export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
|
export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
|
||||||
return _loopAt(factor, pat, cps);
|
return _loopAt(factor, pat, cps);
|
||||||
});
|
});
|
||||||
@@ -3630,7 +3703,7 @@ let fadeGain = (p) => (p < 0.5 ? 1 : 1 - (p - 0.5) / 0.5);
|
|||||||
* - 1 = (no left, full right)
|
* - 1 = (no left, full right)
|
||||||
*
|
*
|
||||||
* @name xfade
|
* @name xfade
|
||||||
* @tags combiners
|
* @tags amplitude
|
||||||
* @example
|
* @example
|
||||||
* xfade(s("bd*2"), "<0 .25 .5 .75 1>", s("hh*8"))
|
* xfade(s("bd*2"), "<0 .25 .5 .75 1>", s("hh*8"))
|
||||||
*/
|
*/
|
||||||
@@ -3756,7 +3829,7 @@ const _distortWithAlg = function (name) {
|
|||||||
* Soft-clipping distortion
|
* Soft-clipping distortion
|
||||||
*
|
*
|
||||||
* @name soft
|
* @name soft
|
||||||
* @tags fx
|
* @tags distortion, superdough
|
||||||
* @param {number | Pattern} distortion amount of distortion to apply
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
*
|
*
|
||||||
@@ -3767,7 +3840,7 @@ export const soft = _distortWithAlg('soft');
|
|||||||
* Hard-clipping distortion
|
* Hard-clipping distortion
|
||||||
*
|
*
|
||||||
* @name hard
|
* @name hard
|
||||||
* @tags fx
|
* @tags distortion, superdough
|
||||||
* @param {number | Pattern} distortion amount of distortion to apply
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
*
|
*
|
||||||
@@ -3778,7 +3851,7 @@ export const hard = _distortWithAlg('hard');
|
|||||||
* Cubic polynomial distortion
|
* Cubic polynomial distortion
|
||||||
*
|
*
|
||||||
* @name cubic
|
* @name cubic
|
||||||
* @tags fx
|
* @tags distortion, superdough
|
||||||
* @param {number | Pattern} distortion amount of distortion to apply
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
*
|
*
|
||||||
@@ -3789,7 +3862,7 @@ export const cubic = _distortWithAlg('cubic');
|
|||||||
* Diode-emulating distortion
|
* Diode-emulating distortion
|
||||||
*
|
*
|
||||||
* @name diode
|
* @name diode
|
||||||
* @tags fx
|
* @tags distortion, superdough
|
||||||
* @param {number | Pattern} distortion amount of distortion to apply
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
*
|
*
|
||||||
@@ -3800,7 +3873,7 @@ export const diode = _distortWithAlg('diode');
|
|||||||
* Asymmetrical diode distortion
|
* Asymmetrical diode distortion
|
||||||
*
|
*
|
||||||
* @name asym
|
* @name asym
|
||||||
* @tags fx
|
* @tags distortion, superdough
|
||||||
* @param {number | Pattern} distortion amount of distortion to apply
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
*
|
*
|
||||||
@@ -3811,7 +3884,7 @@ export const asym = _distortWithAlg('asym');
|
|||||||
* Wavefolding distortion
|
* Wavefolding distortion
|
||||||
*
|
*
|
||||||
* @name fold
|
* @name fold
|
||||||
* @tags fx
|
* @tags distortion, superdough
|
||||||
* @param {number | Pattern} distortion amount of distortion to apply
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
*
|
*
|
||||||
@@ -3822,7 +3895,7 @@ export const fold = _distortWithAlg('fold');
|
|||||||
* Wavefolding distortion composed with sinusoid
|
* Wavefolding distortion composed with sinusoid
|
||||||
*
|
*
|
||||||
* @name sinefold
|
* @name sinefold
|
||||||
* @tags fx
|
* @tags distortion, superdough
|
||||||
* @param {number | Pattern} distortion amount of distortion to apply
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
*
|
*
|
||||||
@@ -3833,7 +3906,7 @@ export const sinefold = _distortWithAlg('sinefold');
|
|||||||
* Distortion via Chebyshev polynomials
|
* Distortion via Chebyshev polynomials
|
||||||
*
|
*
|
||||||
* @name chebyshev
|
* @name chebyshev
|
||||||
* @tags fx
|
* @tags distortion, superdough
|
||||||
* @param {number | Pattern} distortion amount of distortion to apply
|
* @param {number | Pattern} distortion amount of distortion to apply
|
||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
*
|
*
|
||||||
@@ -3867,7 +3940,7 @@ const _ensureListPattern = (list) => {
|
|||||||
* Can also be used to create a new synth via `s('user').partials(...)`
|
* Can also be used to create a new synth via `s('user').partials(...)`
|
||||||
*
|
*
|
||||||
* @name partials
|
* @name partials
|
||||||
* @tags fx, superdough
|
* @tags superdough
|
||||||
* @param {number[] | Pattern} magnitudes List of [0, 1] magnitudes for partials. 0th entry is the fundamental harmonic (i.e. DC offset is skipped)
|
* @param {number[] | Pattern} magnitudes List of [0, 1] magnitudes for partials. 0th entry is the fundamental harmonic (i.e. DC offset is skipped)
|
||||||
* @example
|
* @example
|
||||||
* s("user").seg(16).n(irand(8)).scale("A:major")
|
* s("user").seg(16).n(irand(8)).scale("A:major")
|
||||||
@@ -3889,7 +3962,7 @@ export const partials = (list) => {
|
|||||||
* Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases
|
* Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases
|
||||||
*
|
*
|
||||||
* @name phases
|
* @name phases
|
||||||
* @tags fx, superdough
|
* @tags superdough
|
||||||
* @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the fundamental phase (i.e. DC offset is skipped)
|
* @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the fundamental phase (i.e. DC offset is skipped)
|
||||||
* @example
|
* @example
|
||||||
* // Phase cancellation
|
* // Phase cancellation
|
||||||
@@ -3911,7 +3984,7 @@ export const phases = (list) => {
|
|||||||
* calls and/or in a single .FX(fx1, fx2, ..) call. The fx1, .. are _patterns_ which
|
* calls and/or in a single .FX(fx1, fx2, ..) call. The fx1, .. are _patterns_ which
|
||||||
* establish the controls of the given effect. See examples.
|
* establish the controls of the given effect. See examples.
|
||||||
* @name FX
|
* @name FX
|
||||||
* @tags fx, superdough
|
* @tags superdough
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
@@ -3962,7 +4035,7 @@ const _asArrayPattern = (pats) => {
|
|||||||
* by wrapping them inside a function in K (see example).
|
* by wrapping them inside a function in K (see example).
|
||||||
*
|
*
|
||||||
* @name K
|
* @name K
|
||||||
* @tags generators, fx, superdough
|
* @tags generators, superdough
|
||||||
* @param {KabelsalatExpression | Function} expr Kabelsalat graph definition
|
* @param {KabelsalatExpression | Function} expr Kabelsalat graph definition
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
|||||||
+28
-23
@@ -71,29 +71,32 @@ export const pickmod = register('pickmod', function (lookup, pat) {
|
|||||||
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
|
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
|
||||||
* @tags combiners, functional
|
* @tags combiners, functional
|
||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {Pattern} lookup a pattern of indices
|
* @param {Pattern} lookup a pattern of indices or names
|
||||||
* @param {function[]} funcs the array of functions from which to pull
|
* @param {function[] | object} lookup the array or lookup object of functions from which to pull
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
|
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
|
||||||
* @example
|
* @example
|
||||||
* note("<c2 d2>(3,8)").s("square")
|
* note("<c2 d2>(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("<c2 d2>(3,8)").s("square")
|
||||||
|
* .pickF("<jr l> f", { jr:jux(rev), f:fast(2), l:x=>x.lpf(800) })
|
||||||
*/
|
*/
|
||||||
export const pickF = register('pickF', function (lookup, funcs, pat) {
|
export const pickF = register('pickF', function (pickPattern, lookup, pat) {
|
||||||
return pat.apply(pick(lookup, funcs));
|
return pat.apply(pick(lookup, pickPattern));
|
||||||
});
|
});
|
||||||
|
|
||||||
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
|
/** * 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.
|
* it wraps around, rather than sticking at the maximum value.
|
||||||
* @tags combiners
|
* @tags combiners
|
||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {Pattern} lookup a pattern of indices
|
* @param {Pattern} lookup a pattern of indices or names
|
||||||
* @param {function[]} funcs the array of functions from which to pull
|
* @param {function[] | object} lookup the array or lookup object of functions from which to pull
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
*/
|
*/
|
||||||
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
|
export const pickmodF = register('pickmodF', function (pickPattern, lookup, pat) {
|
||||||
return pat.apply(pickmod(lookup, funcs));
|
return pat.apply(pickmod(lookup, pickPattern));
|
||||||
});
|
});
|
||||||
|
|
||||||
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
|
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
|
||||||
@@ -167,20 +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).
|
/** 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.
|
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
|
||||||
* @name inhabit
|
* @name inhabit
|
||||||
* @tags combiners
|
* @tags combiners
|
||||||
* @synonyms pickSqueeze
|
* @synonyms pickSqueeze
|
||||||
* @param {Pattern} pat
|
* @param {Pattern} pat
|
||||||
* @param {*} xs
|
* @param {*} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
|
* let a = s("bd(3,8)")
|
||||||
b: s("cp sd")
|
* let b = s("cp sd")
|
||||||
})
|
* "<a b [a,b]>".inhabit({ a, b })
|
||||||
* @example
|
* @example
|
||||||
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
|
* 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) {
|
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
|
||||||
return _pick(lookup, pat, false).squeezeJoin();
|
return _pick(lookup, pat, false).squeezeJoin();
|
||||||
});
|
});
|
||||||
|
|||||||
+302
-39
@@ -38,6 +38,7 @@ export function repl({
|
|||||||
pattern: undefined,
|
pattern: undefined,
|
||||||
miniLocations: [],
|
miniLocations: [],
|
||||||
widgets: [],
|
widgets: [],
|
||||||
|
sliders: [],
|
||||||
pending: false,
|
pending: false,
|
||||||
started: false,
|
started: false,
|
||||||
};
|
};
|
||||||
@@ -79,11 +80,132 @@ export function repl({
|
|||||||
let allTransform;
|
let allTransform;
|
||||||
let eachTransform;
|
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 () {
|
const hush = function () {
|
||||||
pPatterns = {};
|
pPatterns = {};
|
||||||
anonymousIndex = 0;
|
anonymousIndex = 0;
|
||||||
allTransform = undefined;
|
allTransform = undefined;
|
||||||
eachTransform = undefined;
|
eachTransform = undefined;
|
||||||
|
codeBlocks = {};
|
||||||
|
blockPatterns.clear();
|
||||||
|
lastActiveVisualizerLabel = null; // Reset 'all' visualizer tracking
|
||||||
return silence;
|
return silence;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,7 +225,60 @@ export function repl({
|
|||||||
};
|
};
|
||||||
setTime(() => scheduler.now()); // TODO: refactor?
|
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 start = () => scheduler.start();
|
||||||
const pause = () => scheduler.pause();
|
const pause = () => scheduler.pause();
|
||||||
const toggle = () => scheduler.toggle();
|
const toggle = () => scheduler.toggle();
|
||||||
@@ -217,7 +392,7 @@ export function repl({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const evaluate = async (code, autostart = true, shouldHush = true) => {
|
const evaluate = async (code, autostart = true) => {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error('no code to evaluate');
|
throw new Error('no code to evaluate');
|
||||||
}
|
}
|
||||||
@@ -225,59 +400,34 @@ export function repl({
|
|||||||
updateState({ code, pending: true });
|
updateState({ code, pending: true });
|
||||||
await injectPatternMethods();
|
await injectPatternMethods();
|
||||||
setTime(() => scheduler.now()); // TODO: refactor?
|
setTime(() => scheduler.now()); // TODO: refactor?
|
||||||
await beforeEval?.({ code });
|
await beforeEval?.({ code, blockBased: false });
|
||||||
allTransforms = []; // reset all transforms
|
allTransforms = []; // reset all transforms
|
||||||
shouldHush && hush();
|
|
||||||
|
codeBlocks = {};
|
||||||
|
hush();
|
||||||
|
|
||||||
if (mondo) {
|
if (mondo) {
|
||||||
code = `mondolang\`${code}\``;
|
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)) {
|
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
||||||
pattern = silence;
|
|
||||||
}
|
pattern = applyPatternTransforms(pattern);
|
||||||
|
|
||||||
logger(`[eval] code updated`);
|
logger(`[eval] code updated`);
|
||||||
pattern = await setPattern(pattern, autostart);
|
pattern = await setPattern(pattern, autostart);
|
||||||
updateState({
|
updateState({
|
||||||
miniLocations: meta?.miniLocations || [],
|
miniLocations: meta?.miniLocations || [],
|
||||||
widgets: meta?.widgets || [],
|
widgets: meta?.widgets || [],
|
||||||
|
sliders: meta?.sliders || [],
|
||||||
activeCode: code,
|
activeCode: code,
|
||||||
pattern,
|
pattern,
|
||||||
evalError: undefined,
|
evalError: undefined,
|
||||||
schedulerError: undefined,
|
schedulerError: undefined,
|
||||||
pending: false,
|
pending: false,
|
||||||
});
|
});
|
||||||
afterEval?.({ code, pattern, meta });
|
|
||||||
|
afterEval?.({ code, pattern, meta, range: undefined, widgetRemoved: false });
|
||||||
return pattern;
|
return pattern;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger(`[eval] error: ${err.message}`, 'error');
|
logger(`[eval] error: ${err.message}`, 'error');
|
||||||
@@ -286,8 +436,121 @@ export function repl({
|
|||||||
onEvalError?.(err);
|
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 });
|
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 =
|
export const getTrigger =
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ export const run = (n) => saw.range(0, n).round().segment(n);
|
|||||||
* // "hh".s().struct("1 0 1")
|
* // "hh".s().struct("1 0 1")
|
||||||
*/
|
*/
|
||||||
export const binary = (n) => {
|
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);
|
return binaryN(n, nBits);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -348,7 +348,7 @@ export const binaryN = (n, nBits = 16) => {
|
|||||||
* .partials(binaryL(irand(4096).add(1)))
|
* .partials(binaryL(irand(4096).add(1)))
|
||||||
*/
|
*/
|
||||||
export const binaryL = (n) => {
|
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);
|
return binaryNL(n, nBits);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -389,7 +389,9 @@ export const randL = (n) => {
|
|||||||
export const randrun = (n) => {
|
export const randrun = (n) => {
|
||||||
return signal((t, controls) => {
|
return signal((t, controls) => {
|
||||||
// Without adding 0.5, the first cycle is always 0,1,2,3,...
|
// 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
|
const nums = rands
|
||||||
.map((n, i) => [n, i])
|
.map((n, i) => [n, i])
|
||||||
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
|
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Fraction from 'fraction.js';
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
|
||||||
import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs';
|
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 st = (begin, end) => new State(ts(begin, end));
|
||||||
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(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());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -339,8 +339,13 @@ export function uniqsortr(a) {
|
|||||||
|
|
||||||
export function unicodeToBase64(text) {
|
export function unicodeToBase64(text) {
|
||||||
const utf8Bytes = new TextEncoder().encode(text);
|
const utf8Bytes = new TextEncoder().encode(text);
|
||||||
const base64String = btoa(String.fromCharCode(...utf8Bytes));
|
let binaryString = '';
|
||||||
return base64String;
|
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) {
|
export function base64ToUnicode(base64String) {
|
||||||
|
|||||||
@@ -78,6 +78,16 @@ export const cleanupDraw = (clearScreen = true, id) => {
|
|||||||
stopAllAnimations(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) {
|
Pattern.prototype.onPaint = function (painter) {
|
||||||
return this.withState((state) => {
|
return this.withState((state) => {
|
||||||
if (!state.controls.painters) {
|
if (!state.controls.painters) {
|
||||||
|
|||||||
@@ -54,11 +54,42 @@ export function pitchwheel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (edo) {
|
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) => {
|
Array.from({ length: edo }, (_, i) => {
|
||||||
const angle = freq2angle(root * Math.pow(2, i / edo), root);
|
const angle = freq2angle(root * Math.pow(2, i / edo), root);
|
||||||
|
|
||||||
const [x, y] = circlePos(centerX, centerY, radius, angle);
|
const [x, y] = circlePos(centerX, centerY, radius, angle);
|
||||||
ctx.beginPath();
|
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.fill();
|
||||||
});
|
});
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
]
|
||||||
|
```
|
||||||
@@ -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 <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/edo.mjs>
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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:<LLsLLL LLLLsL>:3:1")
|
||||||
|
* .s("piano")._pitchwheel()
|
||||||
|
* @example
|
||||||
|
* n(rand.range(0,5).segment(6))
|
||||||
|
* .edoScale("<G2 C3>: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
|
||||||
|
);
|
||||||
@@ -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 <https://github.com/robmckinnon/pitfalls/blob/main/lib/Scale.lua>
|
||||||
|
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/edoscale.mjs>
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import './edo.mjs';
|
||||||
|
|
||||||
|
export * from './edo.mjs';
|
||||||
|
|
||||||
|
export const packageName = '@strudel/edo';
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
intervals.mjs - defines Intervals for equal division of the octave (EDO) scale
|
||||||
|
- Port of pitfalls/lib/Intervals.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/Intervals.lua>
|
||||||
|
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/intervals.mjs>
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <rob@movingflow>",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
pitches.mjs - defines Pitches for equal division of the octave (EDO) scale
|
||||||
|
- Port of pitfalls/lib/Pitches.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/Pitches.lua>
|
||||||
|
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/pitches.mjs>
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
ratios.mjs - lists whole number ratios for pitch intervals
|
||||||
|
- Port of pitfalls/lib/ratios.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/ratios.lua>
|
||||||
|
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/ratios.mjs>
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
edo.test.mjs - tests of edo.mjs
|
||||||
|
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tonal/test/tonal.test.mjs>
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
edoscale.test.mjs - tests of EdoScale from edoscale.mjs
|
||||||
|
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/test/edoscale.test.mjs>
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
ratios.test.mjs - tests of ratios.mjs
|
||||||
|
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/test/ratios.test.mjs>
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
/*
|
||||||
|
input.mjs - MIDI input wrapper
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/midi/midi.mjs>
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
-67
@@ -23,6 +23,8 @@ import { noteToMidi, getControlName } from '@strudel/core';
|
|||||||
import { Note } from 'webmidi';
|
import { Note } from 'webmidi';
|
||||||
import { getAudioContext } from '@strudel/webaudio';
|
import { getAudioContext } from '@strudel/webaudio';
|
||||||
import { scheduleAtTime } from '../superdough/helpers.mjs';
|
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:
|
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||||
export const { WebMidi } = _WebMidi;
|
export const { WebMidi } = _WebMidi;
|
||||||
@@ -31,10 +33,6 @@ function supportsMidi() {
|
|||||||
return typeof navigator.requestMIDIAccess === 'function';
|
return typeof navigator.requestMIDIAccess === 'function';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMidiDeviceNamesString(devices) {
|
|
||||||
return devices.map((o) => `'${o.name}'`).join(' | ');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function enableWebMidi(options = {}) {
|
export function enableWebMidi(options = {}) {
|
||||||
const { onReady, onConnected, onDisconnected, onEnabled } = options;
|
const { onReady, onConnected, onDisconnected, onEnabled } = options;
|
||||||
if (WebMidi.enabled) {
|
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
|
// send start/stop messages to outputs when repl starts/stops
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.addEventListener('message', (e) => {
|
window.addEventListener('message', (e) => {
|
||||||
@@ -138,7 +113,7 @@ function githubPath(base, subpath = '') {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* configures the default midimap, which is used when no "midimap" port is set
|
* configures the default midimap, which is used when no "midimap" port is set
|
||||||
* @tags external_io
|
* @tags external_io, midi
|
||||||
* @example
|
* @example
|
||||||
* defaultmidimap({ lpf: 74 })
|
* defaultmidimap({ lpf: 74 })
|
||||||
* $: note("c a f e").midi();
|
* $: note("c a f e").midi();
|
||||||
@@ -152,7 +127,7 @@ let loadCache = {};
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds midimaps to the registry. Inside each midimap, control names (e.g. lpf) are mapped to cc numbers.
|
* Adds midimaps to the registry. Inside each midimap, control names (e.g. lpf) are mapped to cc numbers.
|
||||||
* @tags external_io
|
* @tags external_io, midi
|
||||||
* @example
|
* @example
|
||||||
* midimaps({ mymap: { lpf: 74 } })
|
* midimaps({ mymap: { lpf: 74 } })
|
||||||
* $: note("c a f e")
|
* $: note("c a f e")
|
||||||
@@ -495,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)) {
|
if (isPattern(input)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[midi] Midi input cannot be a pattern. Make sure to pass device name with single quotes. Example: midin('${
|
`[midi] Midi input cannot be a pattern. Make sure to pass device name with single quotes. Example: midin('${
|
||||||
@@ -505,31 +480,38 @@ async function _initialize(input) {
|
|||||||
}')`,
|
}')`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const initial = await enableWebMidi(); // only returns on first init
|
const initial = await enableWebMidi(); // only returns on first init
|
||||||
const device = getDevice(input, WebMidi.inputs);
|
|
||||||
if (!device) {
|
const instance = midiInputs[input] || new MidiInput(input);
|
||||||
throw new Error(
|
midiInputs[input] = instance;
|
||||||
`[midi] Midi device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (initial) {
|
if (initial) {
|
||||||
|
const device = instance.initialDevice;
|
||||||
|
|
||||||
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
|
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
|
||||||
logger(
|
logger(
|
||||||
`[midi] Midi enabled! Using "${device.name}". ${
|
device
|
||||||
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
|
? `[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.
|
* 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
|
* The output is a function that accepts a midi cc value to query as well as (optionally) a midi channel
|
||||||
*
|
*
|
||||||
* @name midin
|
* @name midin
|
||||||
* @tags external_io
|
* @tags external_io, midi
|
||||||
* @param {string | number} input MIDI device name or index defaulting to 0
|
* @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.
|
* @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)
|
* When queried, the pattern will produces the most recently received midi value (normalized to 0 to 1)
|
||||||
@@ -543,31 +525,10 @@ async function _initialize(input) {
|
|||||||
* note("c a f e").s("saw")
|
* note("c a f e").s("saw")
|
||||||
* .when(cc(0).gt(0), x => x.postgain(0))
|
* .when(cc(0).gt(0), x => x.postgain(0))
|
||||||
*/
|
*/
|
||||||
let listeners = {};
|
|
||||||
const refs = {};
|
|
||||||
const refsByChan = {};
|
|
||||||
export async function midin(input) {
|
export async function midin(input) {
|
||||||
const device = await _initialize(input);
|
const instance = await _initializeInput(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);
|
|
||||||
};
|
|
||||||
|
|
||||||
listeners[input] && device.removeListener('midimessage', listeners[input]);
|
return instance.createCC.bind(instance);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -577,7 +538,7 @@ export async function midin(input) {
|
|||||||
* note durations
|
* note durations
|
||||||
*
|
*
|
||||||
* @name midikeys
|
* @name midikeys
|
||||||
* @tags external_io
|
* @tags external_io, midi
|
||||||
* @param {string | number} input MIDI device name or index defaulting to 0
|
* @param {string | number} input MIDI device name or index defaulting to 0
|
||||||
* @returns {function((number | Pattern)=): Pattern} A function that produces a pattern.
|
* @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,
|
* When queried, the pattern will produces the most recently played midi notes and velocities,
|
||||||
@@ -622,7 +583,16 @@ function _triggerKeyboard(input, cps, now, latencyCycles) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
export async function midikeys(input) {
|
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]) {
|
if (!kHaps[input]) {
|
||||||
kHaps[input] = [];
|
kHaps[input] = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
util.mjs - MIDI utility functions
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/midi/midi.mjs>
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
"@strudel/codemirror": "workspace:*",
|
"@strudel/codemirror": "workspace:*",
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
|
"@strudel/edo": "workspace:*",
|
||||||
"@strudel/hydra": "workspace:*",
|
"@strudel/hydra": "workspace:*",
|
||||||
"@strudel/midi": "workspace:*",
|
"@strudel/midi": "workspace:*",
|
||||||
"@strudel/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export async function prebake() {
|
|||||||
core,
|
core,
|
||||||
import('@strudel/draw'),
|
import('@strudel/draw'),
|
||||||
import('@strudel/mini'),
|
import('@strudel/mini'),
|
||||||
|
import('@strudel/edo'),
|
||||||
import('@strudel/tonal'),
|
import('@strudel/tonal'),
|
||||||
import('@strudel/webaudio'),
|
import('@strudel/webaudio'),
|
||||||
import('@strudel/codemirror'),
|
import('@strudel/codemirror'),
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export const connectLFO = (id, params, nodeTracker) => {
|
|||||||
fxi = 'main',
|
fxi = 'main',
|
||||||
depth = 1,
|
depth = 1,
|
||||||
depthabs,
|
depthabs,
|
||||||
|
retrig = 0,
|
||||||
...filteredParams
|
...filteredParams
|
||||||
} = params;
|
} = params;
|
||||||
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl);
|
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl);
|
||||||
@@ -109,7 +110,7 @@ export const connectLFO = (id, params, nodeTracker) => {
|
|||||||
const modParams = {
|
const modParams = {
|
||||||
...filteredParams,
|
...filteredParams,
|
||||||
frequency: sync !== undefined ? sync * cps : rate,
|
frequency: sync !== undefined ? sync * cps : rate,
|
||||||
time: cycle / cps,
|
time: retrig > 0.5 ? 0 : cycle / cps,
|
||||||
depth: depthValue,
|
depth: depthValue,
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
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 { getAudioContext } from './audioContext.mjs';
|
||||||
import {
|
import {
|
||||||
getADSRValues,
|
getADSRValues,
|
||||||
@@ -30,6 +30,29 @@ function humanFileSize(bytes, si) {
|
|||||||
return bytes.toFixed(1) + ' ' + units[u];
|
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) {
|
export function getSampleInfo(hapValue, bank) {
|
||||||
const { speed = 1.0 } = hapValue;
|
const { speed = 1.0 } = hapValue;
|
||||||
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
|
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
import { map } from 'nanostores';
|
import { map } from 'nanostores';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { connectLFO, connectEnvelope, connectBusModulator } from './modulators.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 { getAudioContext } from './audioContext.mjs';
|
||||||
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
||||||
import { resetSeenKeys } from './wavetable.mjs';
|
import { resetSeenKeys } from './wavetable.mjs';
|
||||||
@@ -40,7 +40,7 @@ export let maxPolyphony = DEFAULT_MAX_POLYPHONY;
|
|||||||
* start to die out in first-in-first-out order once the max polyphony has been hit
|
* start to die out in first-in-first-out order once the max polyphony has been hit
|
||||||
*
|
*
|
||||||
* @name setMaxPolyphony
|
* @name setMaxPolyphony
|
||||||
* @tags fx, superdough
|
* @tags superdough
|
||||||
* @param {number} Max polyphony. Defaults to 128
|
* @param {number} Max polyphony. Defaults to 128
|
||||||
* @example
|
* @example
|
||||||
* setMaxPolyphony(4)
|
* setMaxPolyphony(4)
|
||||||
@@ -74,7 +74,7 @@ export function applyGainCurve(val) {
|
|||||||
* quadratic, exponential, etc. rather than linear
|
* quadratic, exponential, etc. rather than linear
|
||||||
*
|
*
|
||||||
* @name setGainCurve
|
* @name setGainCurve
|
||||||
* @tags fx, superdough
|
* @tags amplitude, superdough
|
||||||
* @param {Function} function to apply to all gain values
|
* @param {Function} function to apply to all gain values
|
||||||
* @example
|
* @example
|
||||||
* setGainCurve((x) => x * x) // quadratic gain
|
* setGainCurve((x) => x * x) // quadratic gain
|
||||||
|
|||||||
@@ -485,8 +485,10 @@ export function waveformN(partials, phases, type) {
|
|||||||
if (phase !== 0) {
|
if (phase !== 0) {
|
||||||
const c = Math.cos(PI2 * phase);
|
const c = Math.cos(PI2 * phase);
|
||||||
const s = Math.sin(PI2 * phase);
|
const s = Math.sin(PI2 * phase);
|
||||||
R = c * R - s * I;
|
const R0 = R;
|
||||||
I = s * R + c * I;
|
const I0 = I;
|
||||||
|
R = c * R0 - s * I0;
|
||||||
|
I = s * R0 + c * I0;
|
||||||
}
|
}
|
||||||
real[n + 1] = R;
|
real[n + 1] = R;
|
||||||
imag[n + 1] = I;
|
imag[n + 1] = I;
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ export function registerWaveTable(key, tables, params) {
|
|||||||
* Loads a collection of wavetables to use with `s`
|
* Loads a collection of wavetables to use with `s`
|
||||||
*
|
*
|
||||||
* @name tables
|
* @name tables
|
||||||
* @tags fx
|
* @tags wavetable
|
||||||
*/
|
*/
|
||||||
export const tables = async (url, frameLen, json, options = {}) => {
|
export const tables = async (url, frameLen, json, options = {}) => {
|
||||||
if (json !== undefined) return _processTables(json, url, frameLen);
|
if (json !== undefined) return _processTables(json, url, frameLen);
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ class CoarseProcessor extends AudioWorkletProcessor {
|
|||||||
coarse = Math.max(1, coarse);
|
coarse = Math.max(1, coarse);
|
||||||
for (let n = 0; n < blockSize; n++) {
|
for (let n = 0; n < blockSize; n++) {
|
||||||
for (let i = 0; i < input.length; i++) {
|
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;
|
return true;
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ function scaleOffset(scale, offset, note) {
|
|||||||
* - 5P = perfect fifth
|
* - 5P = perfect fifth
|
||||||
* - 5d = diminished fifth
|
* - 5d = diminished fifth
|
||||||
*
|
*
|
||||||
* @tags music_theory
|
* @tags tonal
|
||||||
* @param {string | number} amount Either number of semitones or interval string.
|
* @param {string | number} amount Either number of semitones or interval string.
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
@@ -155,7 +155,7 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
|
|||||||
*
|
*
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @name scaleTranspose
|
* @name scaleTranspose
|
||||||
* @tags music_theory
|
* @tags tonal
|
||||||
* @param {offset} offset number of steps inside the scale
|
* @param {offset} offset number of steps inside the scale
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @synonyms scaleTrans, strans
|
* @synonyms scaleTrans, strans
|
||||||
@@ -246,7 +246,7 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) {
|
|||||||
* The root note defaults to octave 3, if no octave number is given.
|
* The root note defaults to octave 3, if no octave number is given.
|
||||||
*
|
*
|
||||||
* @name scale
|
* @name scale
|
||||||
* @tags music_theory
|
* @tags tonal
|
||||||
* @param {string} scale Name of scale
|
* @param {string} scale Name of scale
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistr
|
|||||||
* Adds a new custom voicing dictionary.
|
* Adds a new custom voicing dictionary.
|
||||||
*
|
*
|
||||||
* @name addVoicings
|
* @name addVoicings
|
||||||
* @tags music_theory
|
* @tags tonal
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @param {string} name identifier for the voicing dictionary
|
* @param {string} name identifier for the voicing dictionary
|
||||||
* @param {Object} dictionary maps chord symbol to possible voicings
|
* @param {Object} dictionary maps chord symbol to possible voicings
|
||||||
@@ -134,7 +134,7 @@ const getVoicing = (chord, dictionaryName, lastVoicing) => {
|
|||||||
* Uses [chord-voicings package](https://github.com/felixroos/chord-voicings#chord-voicings).
|
* Uses [chord-voicings package](https://github.com/felixroos/chord-voicings#chord-voicings).
|
||||||
*
|
*
|
||||||
* @name voicings
|
* @name voicings
|
||||||
* @tags music_theory
|
* @tags tonal
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @param {string} dictionary which voicing dictionary to use.
|
* @param {string} dictionary which voicing dictionary to use.
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
@@ -159,7 +159,7 @@ export const voicings = register('voicings', function (dictionary, pat) {
|
|||||||
* Maps the chords of the incoming pattern to root notes in the given octave.
|
* Maps the chords of the incoming pattern to root notes in the given octave.
|
||||||
*
|
*
|
||||||
* @name rootNotes
|
* @name rootNotes
|
||||||
* @tags music_theory
|
* @tags tonal
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @param {octave} octave octave to use
|
* @param {octave} octave octave to use
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
@@ -192,7 +192,7 @@ export const rootNotes = register('rootNotes', function (octave, pat) {
|
|||||||
* If you pass a pattern of strings to voicing, they will be interpreted as chords.
|
* If you pass a pattern of strings to voicing, they will be interpreted as chords.
|
||||||
*
|
*
|
||||||
* @name voicing
|
* @name voicing
|
||||||
* @tags music_theory
|
* @tags tonal
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* n("0 1 2 3").chord("<C Am F G>").voicing()
|
* n("0 1 2 3").chord("<C Am F G>").voicing()
|
||||||
|
|||||||
@@ -1,10 +1,31 @@
|
|||||||
# @strudel/transpiler
|
# @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
|
- adds return statement to the last expression
|
||||||
|
- handles label capturing for block-based eval
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
|
/*
|
||||||
|
index.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
import { evaluate as _evaluate } from '@strudel/core';
|
import { evaluate as _evaluate } from '@strudel/core';
|
||||||
import { transpiler } from './transpiler.mjs';
|
import { transpiler } from './transpiler.mjs';
|
||||||
|
|
||||||
export * 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);
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
/*
|
||||||
|
plugin-kabelsalat.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
/*
|
||||||
|
plugin-mini.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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]);
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
plugin-sample.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
plugin-widgets.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -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 { describe, it, expect } from 'vitest';
|
||||||
import { transpiler } from '../transpiler.mjs';
|
import { transpiler } from '../index.mjs';
|
||||||
|
|
||||||
const simple = { wrapAsync: false, addReturn: false, simpleLocs: true };
|
const simple = { wrapAsync: false, addReturn: false, simpleLocs: true };
|
||||||
|
|
||||||
|
|||||||
+223
-459
@@ -1,13 +1,12 @@
|
|||||||
import { getLeafLocations } from '@strudel/mini';
|
/*
|
||||||
|
transpiler.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
import { parse } from 'acorn';
|
import { parse } from 'acorn';
|
||||||
import escodegen from 'escodegen';
|
import escodegen from 'escodegen';
|
||||||
import { walk } from 'estree-walker';
|
import { walk } from 'estree-walker';
|
||||||
|
|
||||||
let widgetMethods = [];
|
|
||||||
export function registerWidgetType(type) {
|
|
||||||
widgetMethods.push(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
let languages = new Map();
|
let languages = new Map();
|
||||||
// config = { getLocations: (code: string, offset?: number) => number[][] }
|
// config = { getLocations: (code: string, offset?: number) => number[][] }
|
||||||
// see mondough.mjs for example use
|
// see mondough.mjs for example use
|
||||||
@@ -17,9 +16,31 @@ let languages = new Map();
|
|||||||
export function registerLanguage(type, config) {
|
export function registerLanguage(type, config) {
|
||||||
languages.set(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 = {}) {
|
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 = [];
|
const comments = [];
|
||||||
let ast = parse(input, {
|
let ast = parse(input, {
|
||||||
@@ -29,169 +50,103 @@ export function transpiler(input, options = {}) {
|
|||||||
onComment: comments,
|
onComment: comments,
|
||||||
});
|
});
|
||||||
|
|
||||||
const miniDisableRanges = findMiniDisableRanges(comments, input.length);
|
let miniDisableRanges = findMiniDisableRanges(comments, input.length);
|
||||||
let miniLocations = [];
|
|
||||||
const collectMiniLocations = (value, node) => {
|
// Position offset for block-based evaluation
|
||||||
const minilang = languages.get('minilang');
|
let nodeOffset = range && range.length > 0 ? range[0] : 0;
|
||||||
if (minilang) {
|
|
||||||
const code = `[${value}]`;
|
// Track declarations to add to strudelScope for block-based eval
|
||||||
const locs = minilang.getLocations(code, node.start);
|
let scopeDeclarations = [];
|
||||||
miniLocations = miniLocations.concat(locs);
|
|
||||||
} else {
|
let labels = [];
|
||||||
const leafLocs = getLeafLocations(`"${value}"`, node.start, input);
|
|
||||||
miniLocations = miniLocations.concat(leafLocs);
|
const context = { options, input, nodeOffset, miniDisableRanges, labels };
|
||||||
}
|
const plugins = getPlugins().map((plugin) => plugin.walk?.(context));
|
||||||
};
|
|
||||||
let widgets = [];
|
|
||||||
|
|
||||||
walk(ast, {
|
walk(ast, {
|
||||||
enter(node, parent /* , prop, index */) {
|
enter(node, parent, prop, index) {
|
||||||
if (isLanguageLiteral(node)) {
|
// Apply position offset for block-based evaluation
|
||||||
const { name } = node.tag;
|
if (blockBased && node.start !== undefined) {
|
||||||
const language = languages.get(name);
|
node.start = node.start + nodeOffset;
|
||||||
const code = node.quasi.quasis[0].value.raw;
|
node.end = node.end + nodeOffset;
|
||||||
const offset = node.quasi.start + 1;
|
}
|
||||||
if (emitMiniLocations) {
|
// Collect variable and function declarations for strudelScope (block-based eval)
|
||||||
const locs = language.getLocations(code, offset);
|
if (blockBased && parent?.type === 'Program') {
|
||||||
miniLocations = miniLocations.concat(locs);
|
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;
|
for (const plugin of plugins) {
|
||||||
const offset = node.quasi.start + 1;
|
if (!plugin?.enter?.call(this, node, parent, prop, index)) continue;
|
||||||
if (emitMiniLocations) {
|
return;
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLabelStatement(node)) {
|
if (isLabelStatement(node)) {
|
||||||
|
// Collect label info for block-based evaluation
|
||||||
|
// Store positions WITHOUT offset so repl can slice the transpiler output correctly
|
||||||
|
if (blockBased) {
|
||||||
|
labels.push({
|
||||||
|
name: node.label.name,
|
||||||
|
index: node.start - nodeOffset,
|
||||||
|
end: node.label.end - nodeOffset,
|
||||||
|
fullMatch: input.slice(node.start - nodeOffset, node.label.end - nodeOffset),
|
||||||
|
activeVisualizer: findVisualizerInSubtree(node.body),
|
||||||
|
});
|
||||||
|
}
|
||||||
return this.replace(labelToP(node));
|
return this.replace(labelToP(node));
|
||||||
}
|
}
|
||||||
|
// Detect all() calls as special labels for block management
|
||||||
|
// Store positions WITHOUT offset so repl can slice the transpiler output correctly
|
||||||
|
if (blockBased && isAllCall(node)) {
|
||||||
|
labels.push({
|
||||||
|
name: 'all',
|
||||||
|
index: node.start - nodeOffset,
|
||||||
|
end: node.end - nodeOffset,
|
||||||
|
fullMatch: input.slice(node.start - nodeOffset, node.end - nodeOffset),
|
||||||
|
activeVisualizer: node.arguments[0] ? findVisualizerInSubtree(node.arguments[0]) : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
leave(node, parent, prop, index) {
|
leave(node, parent, prop, index) {
|
||||||
if (!isKabelCall(node)) return;
|
for (const plugin of plugins) {
|
||||||
|
if (!plugin?.leave?.call(this, node, parent, prop, index)) continue;
|
||||||
let [expr, ...rest] = node.arguments;
|
return;
|
||||||
if (!expr) throw new Error('K(...) requires an expression');
|
|
||||||
|
|
||||||
if (shouldCallKabelExpression(expr)) {
|
|
||||||
expr = {
|
|
||||||
type: 'CallExpression',
|
|
||||||
callee: expr,
|
|
||||||
arguments: [],
|
|
||||||
optional: false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
let { body } = ast;
|
||||||
|
|
||||||
|
const silenceExpression = {
|
||||||
|
type: 'ExpressionStatement',
|
||||||
|
expression: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name: 'silence',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
if (!body.length) {
|
if (!body.length) {
|
||||||
console.warn('empty body -> fallback to silence');
|
console.warn('empty body -> fallback to silence');
|
||||||
body.push({
|
body.push(silenceExpression);
|
||||||
type: 'ExpressionStatement',
|
|
||||||
expression: {
|
|
||||||
type: 'Identifier',
|
|
||||||
name: 'silence',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else if (!body?.[body.length - 1]?.expression) {
|
} 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
|
// add return to last statement
|
||||||
@@ -209,256 +164,15 @@ export function transpiler(input, options = {}) {
|
|||||||
if (!emitMiniLocations) {
|
if (!emitMiniLocations) {
|
||||||
return { output };
|
return { output };
|
||||||
}
|
}
|
||||||
return { output, miniLocations, widgets };
|
|
||||||
|
let pluginContext;
|
||||||
|
({ options, input, miniDisableRanges, nodeOffset, ...pluginContext } = context);
|
||||||
|
|
||||||
|
return { output, ...pluginContext };
|
||||||
}
|
}
|
||||||
|
|
||||||
function isKabelCall(node) {
|
function isAllCall(node) {
|
||||||
if (node.type !== 'CallExpression') return false;
|
return node.type === 'CallExpression' && node.callee.name === 'all';
|
||||||
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 isLabelStatement(node) {
|
function isLabelStatement(node) {
|
||||||
@@ -491,67 +205,126 @@ function labelToP(node) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLanguageLiteral(node) {
|
// List of non-inline widgets that need cleanup
|
||||||
return node.type === 'TaggedTemplateExpression' && languages.has(node.tag.name);
|
// 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
|
function findVisualizerInSubtree(node) {
|
||||||
// this feels kind of stupid, when we also know the location inside the string op (tidal.mjs)
|
if (!node || typeof node !== 'object') return null;
|
||||||
// but maybe it's the only way
|
|
||||||
|
|
||||||
function isTemplateLiteral(node, value) {
|
// Check if this node is a visualizer call
|
||||||
return node.type === 'TaggedTemplateExpression' && node.tag.name === value;
|
const viz = isVisualizerCall(node);
|
||||||
}
|
if (viz) return viz;
|
||||||
|
|
||||||
function collectHaskellMiniLocations(haskellCode, offset) {
|
// Recursively search children
|
||||||
return haskellCode
|
for (const key of Object.keys(node)) {
|
||||||
.split('')
|
if (key === 'parent') continue; // Skip parent references to avoid cycles
|
||||||
.reduce((acc, char, i) => {
|
const child = node[key];
|
||||||
if (char !== '"') {
|
if (Array.isArray(child)) {
|
||||||
return acc;
|
for (const item of child) {
|
||||||
|
const found = findVisualizerInSubtree(item);
|
||||||
|
if (found) return found;
|
||||||
}
|
}
|
||||||
if (!acc.length || acc[acc.length - 1].length > 1) {
|
} else if (child && typeof child === 'object' && child.type) {
|
||||||
acc.push([i + 1]);
|
const found = findVisualizerInSubtree(child);
|
||||||
} else {
|
if (found) return found;
|
||||||
acc[acc.length - 1].push(i);
|
}
|
||||||
}
|
}
|
||||||
return acc;
|
return null;
|
||||||
}, [])
|
|
||||||
.map(([start, end]) => {
|
|
||||||
const miniString = haskellCode.slice(start, end);
|
|
||||||
return getLeafLocations(`"${miniString}"`, offset + start - 1);
|
|
||||||
})
|
|
||||||
.flat();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function tidalWithLocation(value, offset) {
|
// Creates AST nodes for: userDefinedKeys.add('name'); strudelScope.name = name; globalThis.name = name;
|
||||||
return {
|
// Used in block-based evaluation to persist variables/functions across blocks
|
||||||
type: 'CallExpression',
|
// We add to both strudelScope (for internal lookups) and globalThis (for direct access)
|
||||||
callee: {
|
// We also track the key in userDefinedKeys so clearScope() can remove it later
|
||||||
type: 'Identifier',
|
function createScopeAssignment(name) {
|
||||||
name: 'tidal',
|
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: [
|
// strudelScope.name = name;
|
||||||
{ type: 'Literal', value },
|
{
|
||||||
{ type: 'Literal', value: offset },
|
type: 'ExpressionStatement',
|
||||||
],
|
expression: {
|
||||||
optional: false,
|
type: 'AssignmentExpression',
|
||||||
};
|
operator: '=',
|
||||||
}
|
left: {
|
||||||
|
type: 'MemberExpression',
|
||||||
function languageWithLocation(name, value, offset) {
|
object: {
|
||||||
return {
|
type: 'Identifier',
|
||||||
type: 'CallExpression',
|
name: 'strudelScope',
|
||||||
callee: {
|
},
|
||||||
type: 'Identifier',
|
property: {
|
||||||
name: name,
|
type: 'Identifier',
|
||||||
|
name: name,
|
||||||
|
},
|
||||||
|
computed: false,
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name: name,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
arguments: [
|
// globalThis.name = name;
|
||||||
{ type: 'Literal', value },
|
{
|
||||||
{ type: 'Literal', value: offset },
|
type: 'ExpressionStatement',
|
||||||
],
|
expression: {
|
||||||
optional: false,
|
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) {
|
function findMiniDisableRanges(comments, codeEnd) {
|
||||||
@@ -573,12 +346,3 @@ function findMiniDisableRanges(comments, codeEnd) {
|
|||||||
}
|
}
|
||||||
return ranges;
|
return ranges;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMiniDisabled(offset, miniDisableRanges) {
|
|
||||||
for (const [start, end] of miniDisableRanges) {
|
|
||||||
if (offset >= start && offset < end) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/edo": "workspace:*",
|
||||||
"@strudel/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
"@strudel/tonal": "workspace:*",
|
"@strudel/tonal": "workspace:*",
|
||||||
"@strudel/transpiler": "workspace:*",
|
"@strudel/transpiler": "workspace:*",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export * from '@strudel/webaudio';
|
|||||||
//export * from '@strudel/soundfonts';
|
//export * from '@strudel/soundfonts';
|
||||||
export * from '@strudel/transpiler';
|
export * from '@strudel/transpiler';
|
||||||
export * from '@strudel/mini';
|
export * from '@strudel/mini';
|
||||||
|
export * from '@strudel/edo';
|
||||||
export * from '@strudel/tonal';
|
export * from '@strudel/tonal';
|
||||||
export * from '@strudel/webaudio';
|
export * from '@strudel/webaudio';
|
||||||
import { Pattern, evalScope, setTime } from '@strudel/core';
|
import { Pattern, evalScope, setTime } from '@strudel/core';
|
||||||
@@ -17,6 +18,7 @@ export async function defaultPrebake() {
|
|||||||
evalScope,
|
evalScope,
|
||||||
import('@strudel/core'),
|
import('@strudel/core'),
|
||||||
import('@strudel/mini'),
|
import('@strudel/mini'),
|
||||||
|
import('@strudel/edo'),
|
||||||
import('@strudel/tonal'),
|
import('@strudel/tonal'),
|
||||||
import('@strudel/webaudio'),
|
import('@strudel/webaudio'),
|
||||||
{ hush, evaluate },
|
{ hush, evaluate },
|
||||||
|
|||||||
+38
-1
@@ -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 Tune from './tunejs.js';
|
||||||
import { register } from '@strudel/core';
|
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) => {
|
export const tune = register('tune', (scale, pat) => {
|
||||||
const tune = new Tune();
|
const tune = new Tune();
|
||||||
if (!tune.isValidScale(scale)) {
|
if (!tune.isValidScale(scale)) {
|
||||||
throw new Error('not a valid tune.js scale name: "' + scale + '". See http://abbernie.github.io/tune/scales.html');
|
throw new Error('not a valid tune.js scale name: "' + scale + '". See http://abbernie.github.io/tune/scales.html');
|
||||||
}
|
}
|
||||||
tune.loadScale(scale);
|
tune.loadScale(scale);
|
||||||
|
// if the tonic is a frequency, why are we putting in "1"
|
||||||
tune.tonicize(1);
|
tune.tonicize(1);
|
||||||
return pat.withHap((hap) => {
|
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));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+14
-4
@@ -78,14 +78,21 @@ Tune.prototype.frequency = function(stepIn, octaveIn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// which scale degree (0 - scale length) is our input
|
// which scale degree (0 - scale length) is our input
|
||||||
|
// 60 % 12 = 0
|
||||||
var scaleDegree = stepIn % this.scale.length
|
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) {
|
while (scaleDegree < 0) {
|
||||||
scaleDegree += this.scale.length
|
scaleDegree += this.scale.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tonic is currently always 1
|
||||||
|
// so this is 1*scale[scaledegree]
|
||||||
var freq = this.tonic*this.scale[scaleDegree]
|
var freq = this.tonic*this.scale[scaleDegree]
|
||||||
|
|
||||||
|
// map it to octave
|
||||||
freq = freq*(Math.pow(2,octave))
|
freq = freq*(Math.pow(2,octave))
|
||||||
|
|
||||||
// truncate irrational numbers
|
// truncate irrational numbers
|
||||||
@@ -137,10 +144,13 @@ Tune.prototype.MIDI = function(stepIn,octaveIn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Load a new scale */
|
/* Load a new scale */
|
||||||
|
// sets .scale to ratios
|
||||||
|
|
||||||
Tune.prototype.loadScale = function(scale){
|
Tune.prototype.loadScale = function(scale){
|
||||||
|
|
||||||
/* load the scale */
|
/* load the scale */
|
||||||
|
let name
|
||||||
|
if (typeof scale === 'string') name = scale;
|
||||||
var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies
|
var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies
|
||||||
this.scale = []
|
this.scale = []
|
||||||
for (var i=0;i<freqs.length-1;i++) {
|
for (var i=0;i<freqs.length-1;i++) {
|
||||||
@@ -148,10 +158,10 @@ Tune.prototype.loadScale = function(scale){
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* visualize in console */
|
/* visualize in console */
|
||||||
/* console.log(" ");
|
// console.log(" ");
|
||||||
console.log("LOADED "+name);
|
// console.log("LOADED "+name);
|
||||||
console.log(TuningList[name].description);
|
// console.log(TuningList[name].description);
|
||||||
console.log(this.scale); */
|
// console.log(this.scale);
|
||||||
var vis = [];
|
var vis = [];
|
||||||
for (var i=0;i<100;i++) {
|
for (var i=0;i<100;i++) {
|
||||||
vis[i] = " ";
|
vis[i] = " ";
|
||||||
|
|||||||
+176
-9
@@ -4,8 +4,10 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
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 <https://www.gnu.org/licenses/>.
|
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 <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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) {
|
export function edo(name) {
|
||||||
if (!/^[1-9]+[0-9]*edo$/.test(name)) {
|
if (!/^[1-9]+[0-9]*edo$/.test(name)) {
|
||||||
throw new Error('not an edo scale: "' + 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],
|
'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);
|
return scale.map((r) => r * freq);
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultBase = 220;
|
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) {
|
function getXenScale(scale, indices) {
|
||||||
|
let tune = new Tune();
|
||||||
if (typeof scale === 'string') {
|
if (typeof scale === 'string') {
|
||||||
if (/^[1-9]+[0-9]*edo$/.test(scale)) {
|
if (isEdo(scale)) {
|
||||||
scale = edo(scale);
|
scale = edo(scale);
|
||||||
} else if (presets[scale]) {
|
} else if (presets[scale]) {
|
||||||
scale = presets[scale];
|
scale = presets[scale];
|
||||||
|
} else if (tune.isValidScale(scale)) {
|
||||||
|
tune.loadScale(scale);
|
||||||
|
scale = tune.scale;
|
||||||
} else {
|
} else {
|
||||||
throw new Error('unknown scale name: "' + scale + '"');
|
throw new Error('unknown scale name: "' + scale + '"');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scale = withBase(defaultBase, scale);
|
scale = _withBase(defaultBase, scale);
|
||||||
if (!indices) {
|
if (!indices) {
|
||||||
return scale;
|
return scale;
|
||||||
}
|
}
|
||||||
@@ -47,16 +59,171 @@ function xenOffset(xenScale, offset, index = 0) {
|
|||||||
return xenScale[i] * Math.pow(2, oct);
|
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
|
// 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) {
|
export const xen = register('xen', function (scaleNameOrRatios, pat) {
|
||||||
return pat.withHap((hap) => {
|
return pat.withHaps((haps) => {
|
||||||
const scale = getXenScale(scaleNameOrRatios);
|
haps = haps.map((hap) => {
|
||||||
const frequency = xenOffset(scale, parseNumeral(hap.value));
|
let hVal = hap.value;
|
||||||
return hap.withValue(() => frequency);
|
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) => {
|
return pat.withHap((hap) => {
|
||||||
const frequency = xenOffset(ratios, parseNumeral(hap.value));
|
const frequency = xenOffset(ratios, parseNumeral(hap.value));
|
||||||
return hap.withValue(() => frequency);
|
return hap.withValue(() => frequency);
|
||||||
|
|||||||
Generated
+103
-7
@@ -4,6 +4,30 @@ settings:
|
|||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
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:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
@@ -11,6 +35,9 @@ importers:
|
|||||||
'@strudel/core':
|
'@strudel/core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:packages/core
|
version: link:packages/core
|
||||||
|
'@strudel/edo':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:packages/edo
|
||||||
'@strudel/mini':
|
'@strudel/mini':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:packages/mini
|
version: link:packages/mini
|
||||||
@@ -93,6 +120,9 @@ importers:
|
|||||||
'@strudel/draw':
|
'@strudel/draw':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/draw
|
version: link:../../packages/draw
|
||||||
|
'@strudel/edo':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/edo
|
||||||
'@strudel/mini':
|
'@strudel/mini':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/mini
|
version: link:../../packages/mini
|
||||||
@@ -171,25 +201,25 @@ importers:
|
|||||||
packages/codemirror:
|
packages/codemirror:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/autocomplete':
|
'@codemirror/autocomplete':
|
||||||
specifier: ^6.18.4
|
specifier: 'catalog:'
|
||||||
version: 6.18.4
|
version: 6.18.4
|
||||||
'@codemirror/commands':
|
'@codemirror/commands':
|
||||||
specifier: ^6.8.0
|
specifier: 'catalog:'
|
||||||
version: 6.8.0
|
version: 6.8.0
|
||||||
'@codemirror/lang-javascript':
|
'@codemirror/lang-javascript':
|
||||||
specifier: ^6.2.2
|
specifier: 'catalog:'
|
||||||
version: 6.2.2
|
version: 6.2.2
|
||||||
'@codemirror/language':
|
'@codemirror/language':
|
||||||
specifier: ^6.10.8
|
specifier: 'catalog:'
|
||||||
version: 6.10.8
|
version: 6.10.8
|
||||||
'@codemirror/search':
|
'@codemirror/search':
|
||||||
specifier: ^6.5.8
|
specifier: 'catalog:'
|
||||||
version: 6.5.8
|
version: 6.5.8
|
||||||
'@codemirror/state':
|
'@codemirror/state':
|
||||||
specifier: ^6.5.1
|
specifier: 'catalog:'
|
||||||
version: 6.5.1
|
version: 6.5.1
|
||||||
'@codemirror/view':
|
'@codemirror/view':
|
||||||
specifier: ^6.36.2
|
specifier: 'catalog:'
|
||||||
version: 6.36.2
|
version: 6.36.2
|
||||||
'@lezer/highlight':
|
'@lezer/highlight':
|
||||||
specifier: ^1.2.1
|
specifier: ^1.2.1
|
||||||
@@ -221,6 +251,9 @@ importers:
|
|||||||
'@tonaljs/tonal':
|
'@tonaljs/tonal':
|
||||||
specifier: ^4.10.0
|
specifier: ^4.10.0
|
||||||
version: 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:
|
nanostores:
|
||||||
specifier: ^0.11.3
|
specifier: ^0.11.3
|
||||||
version: 0.11.3
|
version: 0.11.3
|
||||||
@@ -280,6 +313,28 @@ importers:
|
|||||||
specifier: ^6.0.11
|
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)
|
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/embed: {}
|
||||||
|
|
||||||
packages/gamepad:
|
packages/gamepad:
|
||||||
@@ -446,6 +501,9 @@ importers:
|
|||||||
'@strudel/draw':
|
'@strudel/draw':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../draw
|
version: link:../draw
|
||||||
|
'@strudel/edo':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../edo
|
||||||
'@strudel/hydra':
|
'@strudel/hydra':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../hydra
|
version: link:../hydra
|
||||||
@@ -621,6 +679,9 @@ importers:
|
|||||||
'@strudel/core':
|
'@strudel/core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core
|
||||||
|
'@strudel/edo':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../edo
|
||||||
'@strudel/mini':
|
'@strudel/mini':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../mini
|
version: link:../mini
|
||||||
@@ -702,6 +763,21 @@ importers:
|
|||||||
'@astrojs/tailwind':
|
'@astrojs/tailwind':
|
||||||
specifier: ^5.1.5
|
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)
|
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':
|
'@docsearch/css':
|
||||||
specifier: ^3.8.3
|
specifier: ^3.8.3
|
||||||
version: 3.8.3
|
version: 3.8.3
|
||||||
@@ -735,6 +811,9 @@ importers:
|
|||||||
'@strudel/draw':
|
'@strudel/draw':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/draw
|
version: link:../packages/draw
|
||||||
|
'@strudel/edo':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../packages/edo
|
||||||
'@strudel/gamepad':
|
'@strudel/gamepad':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/gamepad
|
version: link:../packages/gamepad
|
||||||
@@ -3551,6 +3630,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==}
|
resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
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:
|
collapse-white-space@2.1.0:
|
||||||
resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
|
resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
|
||||||
|
|
||||||
@@ -11280,6 +11368,14 @@ snapshots:
|
|||||||
|
|
||||||
cmd-shim@6.0.3: {}
|
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: {}
|
collapse-white-space@2.1.0: {}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
|
|||||||
@@ -10,3 +10,12 @@ onlyBuiltDependencies:
|
|||||||
- sharp
|
- sharp
|
||||||
- tree-sitter
|
- tree-sitter
|
||||||
- tree-sitter-haskell
|
- 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"
|
||||||
|
|||||||
@@ -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`] = `
|
exports[`runs examples > example "apply" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/1 | note:C3 ]",
|
"[ 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`] = `
|
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 ]",
|
"[ 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`] = `
|
exports[`runs examples > example "density" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | s:crackle density:0.01 ]",
|
"[ 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`] = `
|
exports[`runs examples > example "end" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | s:oh end: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`] = `
|
exports[`runs examples > example "fanchor" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | note:f s:sawtooth cutoff:1000 lpenv:8 fanchor:0 ]",
|
"[ 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`] = `
|
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 ]",
|
"[ 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 "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`] = `
|
exports[`runs examples > example "grow" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/10 | s:tha bank:mridangam ]",
|
"[ 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`] = `
|
exports[`runs examples > example "inhabit" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | s:bd ]",
|
"[ 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 "keyDown" example index 0 1`] = `[]`;
|
||||||
|
|
||||||
exports[`runs examples > example "lastOf" 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`] = `
|
exports[`runs examples > example "partials" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/16 | s:user note:A3 partials:[1 0 1 0 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`] = `
|
exports[`runs examples > example "pickmodRestart" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | note:C3 s:piano ]",
|
"[ 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`] = `
|
exports[`runs examples > example "setGainCurve" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | s:bd gain:0.5 ]",
|
"[ 0/1 → 1/4 | s:bd gain:0.5 ]",
|
||||||
@@ -13059,6 +13490,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`] = `
|
exports[`runs examples > example "undegrade" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | s:hh ]",
|
"[ 0/1 → 1/8 | s:hh ]",
|
||||||
@@ -13832,6 +14354,76 @@ exports[`runs examples > example "when" example index 0 1`] = `
|
|||||||
|
|
||||||
exports[`runs examples > example "whenKey" 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`] = `
|
exports[`runs examples > example "withValue" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/3 | 10 ]",
|
"[ 0/1 → 1/3 | 10 ]",
|
||||||
@@ -13971,6 +14563,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`] = `
|
exports[`runs examples > example "xfade" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | s:hh gain:0 ]",
|
"[ 0/1 → 1/8 | s:hh gain:0 ]",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const skippedExamples = [
|
|||||||
'accelerationX',
|
'accelerationX',
|
||||||
'defaultmidimap',
|
'defaultmidimap',
|
||||||
'midimaps',
|
'midimaps',
|
||||||
|
'clearScope',
|
||||||
'bmod',
|
'bmod',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { mini, m } from '@strudel/mini/mini.mjs';
|
|||||||
// import euclid from '@strudel/core/euclid.mjs';
|
// import euclid from '@strudel/core/euclid.mjs';
|
||||||
//import '@strudel/midi/midi.mjs';
|
//import '@strudel/midi/midi.mjs';
|
||||||
import * as tonalHelpers from '@strudel/tonal';
|
import * as tonalHelpers from '@strudel/tonal';
|
||||||
|
import * as edoHelpers from '@strudel/edo';
|
||||||
import '@strudel/xen/xen.mjs';
|
import '@strudel/xen/xen.mjs';
|
||||||
// import '@strudel/xen/tune.mjs';
|
// import '@strudel/xen/tune.mjs';
|
||||||
// import '@strudel/core/euclid.mjs';
|
// import '@strudel/core/euclid.mjs';
|
||||||
@@ -126,6 +127,7 @@ const loadSoundfont = () => {};
|
|||||||
const loadCsound = () => {};
|
const loadCsound = () => {};
|
||||||
const loadCSound = () => {};
|
const loadCSound = () => {};
|
||||||
const loadcsound = () => {};
|
const loadcsound = () => {};
|
||||||
|
const getDuration = () => {};
|
||||||
|
|
||||||
const midin = () => {
|
const midin = () => {
|
||||||
return (ccNum) => strudel.ref(() => 0); // returns ref with default value 0
|
return (ccNum) => strudel.ref(() => 0); // returns ref with default value 0
|
||||||
@@ -144,6 +146,7 @@ evalScope(
|
|||||||
toneHelpersMocked,
|
toneHelpersMocked,
|
||||||
uiHelpersMocked,
|
uiHelpersMocked,
|
||||||
webaudio,
|
webaudio,
|
||||||
|
edoHelpers,
|
||||||
tonalHelpers,
|
tonalHelpers,
|
||||||
gamepadHelpers,
|
gamepadHelpers,
|
||||||
/*
|
/*
|
||||||
@@ -168,6 +171,7 @@ evalScope(
|
|||||||
loadCSound,
|
loadCSound,
|
||||||
loadCsound,
|
loadCsound,
|
||||||
loadcsound,
|
loadcsound,
|
||||||
|
getDuration,
|
||||||
setcps: id,
|
setcps: id,
|
||||||
setcpm: id,
|
setcpm: id,
|
||||||
Clock: {}, // whatever
|
Clock: {}, // whatever
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { afterEach } from 'vitest';
|
import { afterEach } from 'vitest';
|
||||||
import { useRNG } from './packages/core/signal.mjs';
|
import { useRNG } from './packages/core/signal.mjs';
|
||||||
|
import { setDefaultJoin } from './packages/core/pattern.mjs';
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// Avoid bleed between tests
|
// Avoid bleed between tests
|
||||||
useRNG('legacy');
|
useRNG('legacy');
|
||||||
|
setDefaultJoin('in');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,8 +10,11 @@ import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
|
|||||||
import tailwind from '@astrojs/tailwind';
|
import tailwind from '@astrojs/tailwind';
|
||||||
import AstroPWA from '@vite-pwa/astro';
|
import AstroPWA from '@vite-pwa/astro';
|
||||||
|
|
||||||
const site = process.env.STRUDEL_SITE || `https://strudel.cc/`; // root url without a path
|
import process from 'node:process';
|
||||||
const base = process.env.STRUDEL_BASE || '/'; // base path of the strudel site
|
|
||||||
|
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;
|
const baseNoTrailing = base.endsWith('/') ? base.slice(0, -1) : base;
|
||||||
|
|
||||||
// this rehype plugin fixes relative links
|
// this rehype plugin fixes relative links
|
||||||
|
|||||||
@@ -17,6 +17,11 @@
|
|||||||
"@astrojs/react": "^4.1.6",
|
"@astrojs/react": "^4.1.6",
|
||||||
"@astrojs/rss": "^4.0.11",
|
"@astrojs/rss": "^4.0.11",
|
||||||
"@astrojs/tailwind": "^5.1.5",
|
"@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/css": "^3.8.3",
|
||||||
"@docsearch/react": "^3.8.3",
|
"@docsearch/react": "^3.8.3",
|
||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
@@ -28,6 +33,7 @@
|
|||||||
"@strudel/csound": "workspace:*",
|
"@strudel/csound": "workspace:*",
|
||||||
"@strudel/desktopbridge": "workspace:*",
|
"@strudel/desktopbridge": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
|
"@strudel/edo": "workspace:*",
|
||||||
"@strudel/gamepad": "workspace:*",
|
"@strudel/gamepad": "workspace:*",
|
||||||
"@strudel/hydra": "workspace:*",
|
"@strudel/hydra": "workspace:*",
|
||||||
"@strudel/midi": "workspace:*",
|
"@strudel/midi": "workspace:*",
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export const SIDEBAR: Sidebar = {
|
|||||||
Understand: [
|
Understand: [
|
||||||
{ text: 'Coding syntax', link: 'learn/code' },
|
{ text: 'Coding syntax', link: 'learn/code' },
|
||||||
{ text: 'Pitch', link: 'understand/pitch' },
|
{ text: 'Pitch', link: 'understand/pitch' },
|
||||||
{ text: 'Xen Harmonic Functions', link: 'learn/xen' },
|
{ text: 'Xenharmonic Functions', link: 'learn/xen' },
|
||||||
{ text: 'Cycles', link: 'understand/cycles' },
|
{ text: 'Cycles', link: 'understand/cycles' },
|
||||||
{ text: 'Voicings', link: 'understand/voicings' },
|
{ text: 'Voicings', link: 'understand/voicings' },
|
||||||
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
|
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
---
|
---
|
||||||
title: Xen Harmonic Functions
|
title: Xenharmonic Functions
|
||||||
layout: ../../layouts/MainLayout.astro
|
layout: ../../layouts/MainLayout.astro
|
||||||
---
|
---
|
||||||
|
|
||||||
import { MiniRepl } from '../../docs/MiniRepl';
|
import { MiniRepl } from '../../docs/MiniRepl';
|
||||||
import { JsDoc } from '../../docs/JsDoc';
|
import { JsDoc } from '../../docs/JsDoc';
|
||||||
|
|
||||||
# Xen Harmonic Functions
|
# Xenharmonic Functions (experimental)
|
||||||
|
|
||||||
|
{/* TODO expand explanation of xenharmony */}
|
||||||
|
|
||||||
These functions allow the use of scales other than your typical chromatic 12 based ones.
|
These functions allow the use of scales other than your typical chromatic 12 based ones.
|
||||||
|
|
||||||
### tune(scale)
|
### tune(scale)
|
||||||
|
|
||||||
|
{/* TODO (maybe): combine jsdoc things in tune.mjs with here */}
|
||||||
|
|
||||||
<JsDoc client:idle name="tune" h={0} />
|
<JsDoc client:idle name="tune" h={0} />
|
||||||
|
|
||||||
Here's an example of how to configure a basic hexany scale:
|
Here's an example of how to configure a basic hexany scale:
|
||||||
|
|
||||||
<MiniRepl client:idle tune={`"0 1 2 3 4 5".tune("hexany15").mul("220").freq()`} />
|
<MiniRepl client:idle tune={`i("0 1 2 3 4 5").tune("hexany15").mul("220").freq()`} />
|
||||||
|
|
||||||
Try other scales like `hexany1`, `iraq`, `gumbeng`, `gunkali`, or `tranh3`
|
Try other scales like `hexany1`, `iraq`, `gumbeng`, `gunkali`, or `tranh3`
|
||||||
|
|
||||||
@@ -26,7 +30,7 @@ You can set your root to be a particular note with `getFreq`
|
|||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
tune={`"4 8 9 10 - - 5 7 9 11 - -".tune("tranh3")
|
tune={`i("4 8 9 10 - - 5 7 9 11 - -").tune("tranh3")
|
||||||
.mul(getFreq('c3'))
|
.mul(getFreq('c3'))
|
||||||
.freq().clip(.5).room(1)`}
|
.freq().clip(.5).room(1)`}
|
||||||
/>
|
/>
|
||||||
@@ -35,7 +39,7 @@ Some tunings become more pronounced with a longer reverb decay:
|
|||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
tune={`"<[5 6 8 10] - [5 7 9 12] -> -".tune("gumbeng")
|
tune={`i("<[5 6 8 10] - [5 7 9 12] -> -").tune("gumbeng")
|
||||||
.mul(getFreq('c3'))
|
.mul(getFreq('c3'))
|
||||||
.freq().clip(.8).room("3:10").rdim(10000).rfade(5)`}
|
.freq().clip(.8).room("3:10").rdim(10000).rfade(5)`}
|
||||||
/>
|
/>
|
||||||
@@ -44,7 +48,7 @@ Additionally, you can combo this with `fmap` so that the base note changes:
|
|||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
tune={`"9 11 12 10 - - -".tune("gunkali")
|
tune={`i("9 11 12 10 - - -").tune("gunkali")
|
||||||
.mul("<c3 c3 a3 d#3>".fmap(getFreq))
|
.mul("<c3 c3 a3 d#3>".fmap(getFreq))
|
||||||
.freq().legato("2 .7").room("1:15").rdim(8500).rlp(14000).rfade(8)`}
|
.freq().legato("2 .7").room("1:15").rdim(8500).rlp(14000).rfade(8)`}
|
||||||
/>
|
/>
|
||||||
@@ -53,8 +57,7 @@ Combining this with various polyrhythm tricks can become very evocative:
|
|||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
tune={`"<[0 3 1 -] [-1 4 2 8]> ~ ~,<-4 -5>"
|
tune={`i("<[0 3 1 -] [-1 4 2 8]> ~ ~,<-4 -5>".add(4))
|
||||||
.transpose(4)
|
|
||||||
.tune("iraq")
|
.tune("iraq")
|
||||||
.mul("<c3 d3 c#3>".fmap(getFreq))
|
.mul("<c3 d3 c#3>".fmap(getFreq))
|
||||||
.freq().clip(.5).room(1).rfade(9)`}
|
.freq().clip(.5).room(1).rfade(9)`}
|
||||||
@@ -67,7 +70,7 @@ Take the `sanza` tuning:
|
|||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
tune={`"4 5 6 7 8 9".tune("sanza")
|
tune={`i("4 5 6 7 8 9").tune("sanza")
|
||||||
.mul(getFreq('c3'))
|
.mul(getFreq('c3'))
|
||||||
.freq()`}
|
.freq()`}
|
||||||
/>
|
/>
|
||||||
@@ -75,15 +78,15 @@ Take the `sanza` tuning:
|
|||||||
Notes 7 and 9 will clash quite a bit if you arp them normally. Many tunings will have this sort of sound, and it can feel distracting on its own.
|
Notes 7 and 9 will clash quite a bit if you arp them normally. Many tunings will have this sort of sound, and it can feel distracting on its own.
|
||||||
See how close they are on the pitch wheel?
|
See how close they are on the pitch wheel?
|
||||||
|
|
||||||
<MiniRepl client:idle tune={`"[7 9]!3".tune("sanza").mul(getFreq('c3')).freq()._pitchwheel()`} />
|
<MiniRepl client:idle tune={`i("[7 9]!3").tune("sanza").mul(getFreq('c3')).freq()._pitchwheel()`} />
|
||||||
|
|
||||||
This quality is often due to how the tunings were formed with instruments that were played differently than a piano.
|
This quality is often due to how the tunings were formed with instruments that were played differently than a piano.
|
||||||
As such, some tunings are much better strummed, with the subtle clash of the detuned notes actually making the sound much more magical:
|
As such, some tunings are much better strummed, with the subtle clash of the detuned notes actually making the sound much more magical:
|
||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
tune={`"[0 1 2 3 4 5 6]@0.3 -"
|
tune={`i("[0 1 2 3 4 5 6]@0.3 -"
|
||||||
.transpose("<2 5 8 1>")
|
.add("<2 5 8 1>"))
|
||||||
.tune("sanza")
|
.tune("sanza")
|
||||||
.mul(getFreq('c3')).freq()
|
.mul(getFreq('c3')).freq()
|
||||||
.legato("3").room(1).rfade(5)`}
|
.legato("3").room(1).rfade(5)`}
|
||||||
@@ -93,3 +96,23 @@ Note the legato and reverb effects make sure the sound of the strumming gets to
|
|||||||
tones sound even more alive, too.
|
tones sound even more alive, too.
|
||||||
|
|
||||||
The `tranh3` tuning has a similar set of notes, with two clashing. You might trying plugging that in above and see if you find a favorite strumming pattern.
|
The `tranh3` tuning has a similar set of notes, with two clashing. You might trying plugging that in above and see if you find a favorite strumming pattern.
|
||||||
|
|
||||||
|
You can also give tune a list of frequencies to use as the scale:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`i("0 1 2 3 4").tune([
|
||||||
|
261.6255653006,
|
||||||
|
302.72962012827,
|
||||||
|
350.29154279212,
|
||||||
|
405.32593044476,
|
||||||
|
469.00678383895,
|
||||||
|
523.2511306012
|
||||||
|
]).mul(220).freq();`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### xen(scaleOrRatios)
|
||||||
|
|
||||||
|
{/* TODO add explanation of EDO to documentation */}
|
||||||
|
|
||||||
|
<JsDoc client:idle name="Pattern.xen" h={0} />
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: Pattern Aligment
|
title: Pattern Alignment
|
||||||
layout: ../../layouts/MainLayout.astro
|
layout: ../../layouts/MainLayout.astro
|
||||||
---
|
---
|
||||||
|
|
||||||
import { MiniRepl } from '../../docs/MiniRepl';
|
import { MiniRepl } from '../../docs/MiniRepl';
|
||||||
|
|
||||||
# Pattern Aligment & Combination
|
# Pattern Alignment & Combination
|
||||||
|
|
||||||
One core aspect of Strudel, inherited from Tidal, is the flexible way that patterns can be combined, irrespective of their structure. Its declarative approach means a live coder does not have to think about the details of _how_ this is done, only _what_ is to be done.
|
One core aspect of Strudel, inherited from Tidal, is the flexible way that patterns can be combined, irrespective of their structure. Its declarative approach means a live coder does not have to think about the details of _how_ this is done, only _what_ is to be done.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
title: Helix Keybindings
|
||||||
|
layout: ../../layouts/MainLayout.astro
|
||||||
|
---
|
||||||
|
|
||||||
|
# Helix Keybindings in the REPL
|
||||||
|
|
||||||
|
The Strudel REPL supports [Helix editor](https://helix-editor.com/) keybindings through the `codemirror-helix` extension. Helix is a post-modern modal text editor with a focus on selection-first editing and multiple cursors.
|
||||||
|
|
||||||
|
## Enabling Helix Mode
|
||||||
|
|
||||||
|
1. Open the Settings panel in the REPL (click the settings icon or use the keyboard shortcut)
|
||||||
|
2. Find the "Keybindings" section
|
||||||
|
3. Select "Helix" from the available options
|
||||||
|
|
||||||
|
## Key Differences from Vim
|
||||||
|
|
||||||
|
Helix uses a selection-first approach, which means:
|
||||||
|
|
||||||
|
- **Selection → Action** (Helix) vs **Action → Motion** (Vim)
|
||||||
|
- For example, to delete a word in Helix: `w` (select word) → `d` (delete)
|
||||||
|
- In Vim, it would be: `dw` (delete word)
|
||||||
|
|
||||||
|
## Common Helix Commands
|
||||||
|
|
||||||
|
### Normal Mode
|
||||||
|
|
||||||
|
- `i` — Enter insert mode before selection
|
||||||
|
- `a` — Enter insert mode after selection
|
||||||
|
- `v` — Enter visual/select mode
|
||||||
|
- `w` — Select next word
|
||||||
|
- `e` — Select to end of word
|
||||||
|
- `b` — Select previous word
|
||||||
|
- `x` — Select/extend line
|
||||||
|
- `d` — Delete selection
|
||||||
|
- `c` — Change selection (delete and enter insert mode)
|
||||||
|
- `y` — Yank (copy) selection
|
||||||
|
- `p` — Paste after selection
|
||||||
|
- `u` — Undo
|
||||||
|
- `U` — Redo
|
||||||
|
- `/` — Search
|
||||||
|
- `n` — Select next search match
|
||||||
|
- `N` — Select previous search match
|
||||||
|
|
||||||
|
### Movement
|
||||||
|
|
||||||
|
- `h, j, k, l` — Move left, down, up, right
|
||||||
|
- `gg` — Go to start of document
|
||||||
|
- `ge` — Go to end of document
|
||||||
|
- `{` / `}` — Move to previous/next paragraph
|
||||||
|
- `%` — Match bracket
|
||||||
|
|
||||||
|
### Multi-cursor
|
||||||
|
|
||||||
|
- `C` — Duplicate cursor to line below
|
||||||
|
- `Alt-C` — Duplicate cursor to line above
|
||||||
|
- `,` — Remove primary cursor
|
||||||
|
- `Alt-,` — Remove all secondary cursors
|
||||||
|
|
||||||
|
## Strudel-Specific Features
|
||||||
|
|
||||||
|
Currently, Helix mode in Strudel provides standard Helix keybindings. Unlike Vim mode, there are no custom Strudel-specific commands (like `:w` to evaluate) yet.
|
||||||
|
|
||||||
|
To evaluate code while in Helix mode, use the standard shortcuts:
|
||||||
|
|
||||||
|
- `Ctrl+Enter` or `Alt+Enter` — Evaluate code
|
||||||
|
- `Alt+.` — Stop playback
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Helix Documentation](https://docs.helix-editor.com/)
|
||||||
|
- [Helix Keymap](https://docs.helix-editor.com/keymap.html)
|
||||||
|
- [codemirror-helix on npm](https://www.npmjs.com/package/codemirror-helix)
|
||||||
|
- [codemirror-helix on GitLab](https://gitlab.com/_rvidal/codemirror-helix)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The `codemirror-helix` extension is described as an "initial version" and may not implement all Helix features
|
||||||
|
- Behavior may differ slightly from the native Helix editor
|
||||||
|
- If you encounter issues, you can switch back to other keybinding modes in Settings
|
||||||
|
- The keybinding preference is saved in your browser's local storage
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
If you'd like to add Strudel-specific Helix commands (similar to Vim's `:w` for evaluate), contributions are welcome! See the implementation in `/packages/codemirror/keybindings.mjs` for reference.
|
||||||
+11
-11
@@ -10,50 +10,50 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code .cm-scroller {
|
.code-container .cm-scroller {
|
||||||
padding-top: 10px !important;
|
padding-top: 10px !important;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
#code .cm-content {
|
.code-container .cm-content {
|
||||||
padding-bottom: 50vh;
|
padding-bottom: 50vh;
|
||||||
}
|
}
|
||||||
#code .cm-line > * {
|
.code-container .cm-line > * {
|
||||||
background: var(--lineBackground);
|
background: var(--lineBackground);
|
||||||
}
|
}
|
||||||
|
|
||||||
#code .cm-editor {
|
.code-container .cm-editor {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code .cm-theme {
|
.code-container .cm-theme {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code .cm-theme-light {
|
.code-container .cm-theme-light {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code .cm-cursorLayer {
|
.code-container .cm-cursorLayer {
|
||||||
animation-name: inherit !important;
|
animation-name: inherit !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code .cm-cursor {
|
.code-container .cm-cursor {
|
||||||
border-left: 2px solid currentcolor !important;
|
border-left: 2px solid currentcolor !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code .cm-foldGutter {
|
.code-container .cm-foldGutter {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code .cm-focused {
|
.code-container .cm-focused {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code .cm-matchingBracket {
|
.code-container .cm-matchingBracket {
|
||||||
text-decoration: underline 0.18rem;
|
text-decoration: underline 0.18rem;
|
||||||
text-underline-offset: 0.22rem;
|
text-underline-offset: 0.22rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ export function Code(Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={'text-gray-100 cursor-text pb-0 overflow-auto grow z-10'}
|
className={'code-container text-gray-100 cursor-text pb-0 overflow-auto grow z-10'}
|
||||||
id="code"
|
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
containerRef.current = el;
|
containerRef.current = el;
|
||||||
if (!editorRef.current) {
|
if (!editorRef.current) {
|
||||||
|
|||||||
@@ -8,31 +8,3 @@ export function ActionButton({ children, label, labelIsHidden, className, ...but
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SpecialActionButton(props) {
|
|
||||||
const { className, ...buttonProps } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ActionButton {...buttonProps} className={cx('bg-background p-2 max-w-[300px] hover:opacity-50', className)} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ActionInput({ label, className, ...props }) {
|
|
||||||
return (
|
|
||||||
<label className={cx('inline-flex items-center cursor-pointer ', className)}>
|
|
||||||
<input {...props} className="sr-only peer" />
|
|
||||||
|
|
||||||
<span className="inline-flex items-center peer-hover:opacity-50">{label}</span>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SpecialActionInput({ className, ...props }) {
|
|
||||||
return (
|
|
||||||
<ActionInput
|
|
||||||
{...props}
|
|
||||||
className={className}
|
|
||||||
label={<span className="bg-background p-2 max-w-[300px]">{props.label}</span>}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,42 +1,50 @@
|
|||||||
import { errorLogger } from '@strudel/core';
|
import { errorLogger } from '@strudel/core';
|
||||||
import { useSettings, storePrebakeScript } from '../../../settings.mjs';
|
import { storePrebakeScript } from '../../../settings.mjs';
|
||||||
import { SpecialActionInput } from '../button/action-button';
|
import { confirmDialog } from '@src/repl/util.mjs';
|
||||||
import { confirmDialog, SETTING_CHANGE_RELOAD_MSG } from '@src/repl/util.mjs';
|
|
||||||
|
|
||||||
async function importScript(script) {
|
async function importScript(script, updateEditor) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.readAsText(script);
|
reader.readAsText(script);
|
||||||
|
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
const text = reader.result;
|
const text = reader.result;
|
||||||
storePrebakeScript(text);
|
storePrebakeScript(text);
|
||||||
|
updateEditor && updateEditor(text);
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.onerror = () => {
|
reader.onerror = () => {
|
||||||
errorLogger(new Error('failed to import prebake script'), 'importScript');
|
errorLogger(new Error('failed to import prebake script'), 'importScript');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export function ImportPrebakeScriptButton() {
|
|
||||||
const settings = useSettings();
|
|
||||||
|
|
||||||
|
export async function exportScript(script) {
|
||||||
|
const blob = new Blob([script], { type: 'application/javascript' });
|
||||||
|
const downloadLink = document.createElement('a');
|
||||||
|
downloadLink.href = window.URL.createObjectURL(blob);
|
||||||
|
const date = new Date().toISOString().split('T')[0];
|
||||||
|
downloadLink.download = `prebake_${date}.strudel`;
|
||||||
|
document.body.appendChild(downloadLink);
|
||||||
|
downloadLink.click();
|
||||||
|
document.body.removeChild(downloadLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImportPrebakeScriptButton({ updateEditor }) {
|
||||||
|
const handleChange = async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
const confirmed = await confirmDialog('Warning: This will overwrite the current prebake.\nContinue?');
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await importScript(file, updateEditor);
|
||||||
|
} catch (e) {
|
||||||
|
errorLogger(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<SpecialActionInput
|
<label className="space-x-1 inline-flex items-center cursor-pointer">
|
||||||
type="file"
|
<input type="file" accept=".strudel,.js" className="sr-only peer" onChange={handleChange} />
|
||||||
label="import prebake script"
|
<span className="inline-flex items-center peer-hover:opacity-50 text-xs max-w-[300px]">import</span>
|
||||||
accept=".strudel"
|
</label>
|
||||||
onChange={async (e) => {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
const confirmed = await confirmDialog(SETTING_CHANGE_RELOAD_MSG);
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await importScript(file);
|
|
||||||
window.location.reload();
|
|
||||||
} catch (e) {
|
|
||||||
errorLogger(e);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import jsdocJson from '../../../../../doc.json';
|
|||||||
import { Textbox } from '@src/repl/components/panel/SettingsTab';
|
import { Textbox } from '@src/repl/components/panel/SettingsTab';
|
||||||
import { settingsMap, useSettings } from '@src/settings.mjs';
|
import { settingsMap, useSettings } from '@src/settings.mjs';
|
||||||
|
|
||||||
const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
|
const isValid = ({ name, description, tags = [] }) => {
|
||||||
|
const isSupradoughOnly = tags.includes('supradough') && !tags.includes('superdough');
|
||||||
|
const isSuperdirtOnly = tags.includes('superdirt') && !tags.includes('superdough');
|
||||||
|
return name && !name.startsWith('_') && !!description && !isSupradoughOnly && !isSuperdirtOnly;
|
||||||
|
};
|
||||||
|
|
||||||
const availableFunctions = (() => {
|
const availableFunctions = (() => {
|
||||||
const seen = new Set(); // avoid repetition
|
const seen = new Set(); // avoid repetition
|
||||||
@@ -14,36 +18,32 @@ const availableFunctions = (() => {
|
|||||||
if (seen.has(doc.name)) continue;
|
if (seen.has(doc.name)) continue;
|
||||||
|
|
||||||
// jsdoc also uses "tags" for when you use @something in the comments and it doesn't know what
|
// jsdoc also uses "tags" for when you use @something in the comments and it doesn't know what
|
||||||
// @something is. We only want data from comments like `@tags fx, superdough` here.
|
// @something is. We only want data from comments like `@tags superdough` here.
|
||||||
// If nothing is specified, we default to "untagged" for debugging
|
// If nothing is specified, we default to "untagged" for debugging
|
||||||
doc.tags = doc.tags?.filter((t) => t && typeof t === 'string') || ['untagged'];
|
doc.tags = doc.tags?.filter((t) => t && typeof t === 'string') || ['untagged'];
|
||||||
functions.push(doc);
|
|
||||||
|
|
||||||
const synonyms = doc.synonyms || [];
|
const synonyms = doc.synonyms || [];
|
||||||
|
let names = [doc.name];
|
||||||
seen.add(doc.name);
|
seen.add(doc.name);
|
||||||
for (const s of synonyms) {
|
for (const s of synonyms) {
|
||||||
if (!s || seen.has(s)) continue;
|
if (!s || seen.has(s)) continue;
|
||||||
|
names.push(s);
|
||||||
seen.add(s);
|
seen.add(s);
|
||||||
// Swap `doc.name` in for `s` in the list of synonyms
|
|
||||||
const synonymsWithDoc = [doc.name, ...synonyms].filter((x) => x && x !== s);
|
|
||||||
functions.push({
|
|
||||||
...doc,
|
|
||||||
name: s, // update names for the synonym
|
|
||||||
longname: s,
|
|
||||||
synonyms: synonymsWithDoc,
|
|
||||||
synonyms_text: synonymsWithDoc.join(', '),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
doc.allNames = names.join(' ');
|
||||||
|
doc.synonyms = names.slice(1);
|
||||||
|
functions.push(doc);
|
||||||
}
|
}
|
||||||
return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const tagCounts = {};
|
const tagCounts = {};
|
||||||
|
const ignoredTags = ['supradough', 'superdirt'];
|
||||||
// const tagOptions = { all: `all (${availableFunctions.length})` };
|
// const tagOptions = { all: `all (${availableFunctions.length})` };
|
||||||
const tagOptions = { all: `all` };
|
const tagOptions = { all: `all` };
|
||||||
for (const doc of availableFunctions) {
|
for (const doc of availableFunctions) {
|
||||||
(doc.tags || ['untagged']).forEach((t) => {
|
(doc.tags || ['untagged']).forEach((t) => {
|
||||||
if (typeof t === 'string' && t) {
|
if (typeof t === 'string' && t && !ignoredTags.includes(t)) {
|
||||||
tagCounts[t] = (tagCounts[t] || 0) + 1;
|
tagCounts[t] = (tagCounts[t] || 0) + 1;
|
||||||
//tagOptions[t] = `${t} (${tagCounts[t]})`;
|
//tagOptions[t] = `${t} (${tagCounts[t]})`;
|
||||||
tagOptions[t] = t;
|
tagOptions[t] = t;
|
||||||
@@ -82,7 +82,7 @@ export const Reference = memo(function Reference() {
|
|||||||
}
|
}
|
||||||
const lowerCaseSearch = search.toLowerCase();
|
const lowerCaseSearch = search.toLowerCase();
|
||||||
return (
|
return (
|
||||||
entry.name.toLowerCase().includes(lowerCaseSearch) ||
|
(entry.allNames || entry.name).toLowerCase().includes(lowerCaseSearch) ||
|
||||||
(entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false)
|
(entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -151,7 +151,7 @@ export const Reference = memo(function Reference() {
|
|||||||
<Fragment key={`entry-${entry.name}`}>
|
<Fragment key={`entry-${entry.name}`}>
|
||||||
<a
|
<a
|
||||||
className={
|
className={
|
||||||
'cursor-pointer hover:opacity-50 text-ellipsis block' +
|
'whitespace-nowrap cursor-pointer hover:opacity-50 text-ellipsis block' +
|
||||||
(entry.name === selectedFunction ? 'bg-lineHighlight font-bold' : '')
|
(entry.name === selectedFunction ? 'bg-lineHighlight font-bold' : '')
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -162,7 +162,7 @@ export const Reference = memo(function Reference() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{entry.name}
|
{entry.name} {entry.synonyms && <small className="opacity-50">{entry.synonyms?.join(', ')}</small>}
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
@@ -204,7 +204,7 @@ export const Reference = memo(function Reference() {
|
|||||||
</h3>
|
</h3>
|
||||||
{entry.tags && (
|
{entry.tags && (
|
||||||
<span className="ml-2 text-xs text-foreground border border-muted px-1 py-0.5">
|
<span className="ml-2 text-xs text-foreground border border-muted px-1 py-0.5">
|
||||||
{entry.tags.join(', ')}
|
{entry.tags.filter((t) => !ignoredTags.includes(t)).join(', ')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
|
import { defaultSettings, settingsMap, useSettings, storePrebakeScript, setSettingsTab } from '../../../settings.mjs';
|
||||||
import { themes } from '@strudel/codemirror';
|
import { themes } from '@strudel/codemirror';
|
||||||
|
import { PrebakeCodeMirror } from '../../../repl/prebakeCodeMirror.mjs';
|
||||||
import { confirmAndReloadPage, isUdels } from '../../util.mjs';
|
import { confirmAndReloadPage, isUdels } from '../../util.mjs';
|
||||||
import { ButtonGroup } from './Forms.jsx';
|
import { ButtonGroup } from './Forms.jsx';
|
||||||
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
||||||
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
||||||
import { confirmDialog } from '../../util.mjs';
|
import { confirmDialog } from '../../util.mjs';
|
||||||
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
|
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
|
||||||
import { SpecialActionButton } from '../button/action-button.jsx';
|
import { ActionButton } from '../button/action-button.jsx';
|
||||||
import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
|
import { exportScript, ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
function cx(...classes) {
|
import cx from '@src/cx.mjs';
|
||||||
// : Array<string | undefined>
|
|
||||||
return classes.filter(Boolean).join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputClass =
|
const inputClass =
|
||||||
'bg-background text-xs h-8 max-h-8 border border-box rounded-0 text-foreground border-muted placeholder-muted focus:outline-none focus:ring-0 focus:border-foreground';
|
'bg-background text-xs h-8 max-h-8 border border-box rounded-0 text-foreground border-muted placeholder-muted focus:outline-none focus:ring-0 focus:border-foreground';
|
||||||
@@ -123,7 +121,7 @@ const fontFamilyOptions = {
|
|||||||
galactico: 'galactico',
|
galactico: 'galactico',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SettingsTab({ started }) {
|
function MainSettingsContent({ started }) {
|
||||||
const {
|
const {
|
||||||
theme,
|
theme,
|
||||||
keybindings,
|
keybindings,
|
||||||
@@ -149,12 +147,12 @@ export function SettingsTab({ started }) {
|
|||||||
isTabIndentationEnabled,
|
isTabIndentationEnabled,
|
||||||
isMultiCursorEnabled,
|
isMultiCursorEnabled,
|
||||||
patternAutoStart,
|
patternAutoStart,
|
||||||
includePrebakeScriptInShare,
|
isBlockBasedEvalEnabled,
|
||||||
} = useSettings();
|
} = useSettings();
|
||||||
const shouldAlwaysSync = isUdels();
|
const shouldAlwaysSync = isUdels();
|
||||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||||
return (
|
return (
|
||||||
<div className="p-4 text-foreground space-y-4 w-full" style={{ fontFamily }}>
|
<div className="p-4 text-foreground space-y-4 w-full overflow-auto" style={{ fontFamily }}>
|
||||||
{canChangeAudioDevice && (
|
{canChangeAudioDevice && (
|
||||||
<FormItem label="Audio Output Device">
|
<FormItem label="Audio Output Device">
|
||||||
<AudioDeviceSelector
|
<AudioDeviceSelector
|
||||||
@@ -232,20 +230,12 @@ export function SettingsTab({ started }) {
|
|||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
<FormItem label="Prebake">
|
|
||||||
<ImportPrebakeScriptButton />
|
|
||||||
<Checkbox
|
|
||||||
label="Include prebake script in share"
|
|
||||||
onChange={(cbEvent) => settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)}
|
|
||||||
value={includePrebakeScriptInShare}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem label="Keybindings">
|
<FormItem label="Keybindings">
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
value={keybindings}
|
value={keybindings}
|
||||||
onChange={(keybindings) => settingsMap.setKey('keybindings', keybindings)}
|
onChange={(keybindings) => settingsMap.setKey('keybindings', keybindings)}
|
||||||
items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs', vscode: 'VSCode' }}
|
items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs', helix: 'Helix', vscode: 'VSCode' }}
|
||||||
></ButtonGroup>
|
></ButtonGroup>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label="Panel Position">
|
<FormItem label="Panel Position">
|
||||||
@@ -306,6 +296,11 @@ export function SettingsTab({ started }) {
|
|||||||
onChange={(cbEvent) => settingsMap.setKey('isMultiCursorEnabled', cbEvent.target.checked)}
|
onChange={(cbEvent) => settingsMap.setKey('isMultiCursorEnabled', cbEvent.target.checked)}
|
||||||
value={isMultiCursorEnabled}
|
value={isMultiCursorEnabled}
|
||||||
/>
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Enable Block-based Evaluation (EXPERIMENTAL)"
|
||||||
|
onChange={(cbEvent) => settingsMap.setKey('isBlockBasedEvalEnabled', cbEvent.target.checked)}
|
||||||
|
value={isBlockBasedEvalEnabled}
|
||||||
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Enable flashing on evaluation"
|
label="Enable flashing on evaluation"
|
||||||
onChange={(cbEvent) => settingsMap.setKey('isFlashEnabled', cbEvent.target.checked)}
|
onChange={(cbEvent) => settingsMap.setKey('isFlashEnabled', cbEvent.target.checked)}
|
||||||
@@ -340,7 +335,7 @@ export function SettingsTab({ started }) {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
|
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
|
||||||
<FormItem label="Reset Settings">
|
<FormItem label="Reset Settings">
|
||||||
<SpecialActionButton
|
<ActionButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
confirmDialog('Sure?').then((r) => {
|
confirmDialog('Sure?').then((r) => {
|
||||||
if (r) {
|
if (r) {
|
||||||
@@ -349,10 +344,70 @@ export function SettingsTab({ started }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
className="bg-background p-2 max-w-[300px] hover:opacity-50"
|
||||||
>
|
>
|
||||||
restore default settings
|
restore default settings
|
||||||
</SpecialActionButton>
|
</ActionButton>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PrebakeSettingsContent() {
|
||||||
|
const { fontFamily, includePrebakeScriptInShare, prebakeScript } = useSettings();
|
||||||
|
const editorRef = useRef();
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
editorRef.current?.cleanup();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full text-foreground w-full overflow-auto" style={{ fontFamily }}>
|
||||||
|
<div className="flex flex-col grow overflow-hidden h-full bg-background">
|
||||||
|
<section
|
||||||
|
className="pb-0 overflow-auto grow z-10 code-container"
|
||||||
|
ref={(el) => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editorRef.current = new PrebakeCodeMirror(prebakeScript, (code) => storePrebakeScript(code), el);
|
||||||
|
}}
|
||||||
|
></section>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center border-t border-muted px-4 whitespace-nowrap">
|
||||||
|
<Checkbox
|
||||||
|
label="share with patterns"
|
||||||
|
className="whitespace-nowrap max-w-[200px]"
|
||||||
|
onChange={(cbEvent) => settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)}
|
||||||
|
value={includePrebakeScriptInShare}
|
||||||
|
/>
|
||||||
|
<div className="py-2 flex flex-row items-center space-x-3 ">
|
||||||
|
<ImportPrebakeScriptButton updateEditor={(code) => editorRef?.current.setCode(code)} />
|
||||||
|
<ActionButton onClick={() => exportScript(prebakeScript)}>export</ActionButton>
|
||||||
|
<ActionButton onClick={() => editorRef.current?.savePrebake()}>save</ActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function SettingsTab({ started }) {
|
||||||
|
const { settingsTab } = useSettings();
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full text-foreground flex flex-col overflow-hidden">
|
||||||
|
<div className="px-2 shrink-0 h-8 space-x-4 flex max-w-full overflow-x-auto border-b border-muted">
|
||||||
|
<ButtonGroup
|
||||||
|
wrap
|
||||||
|
value={settingsTab}
|
||||||
|
onChange={(value) => setSettingsTab(value)}
|
||||||
|
items={{
|
||||||
|
settings: 'settings',
|
||||||
|
prebake: 'prebake',
|
||||||
|
}}
|
||||||
|
></ButtonGroup>
|
||||||
|
</div>
|
||||||
|
{settingsTab === 'settings' && <MainSettingsContent started={started} />}
|
||||||
|
{settingsTab === 'prebake' && <PrebakeSettingsContent />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { toggleLineComment } from '@codemirror/commands';
|
||||||
|
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
||||||
|
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||||
|
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||||
|
import { drawSelection, EditorView, keymap } from '@codemirror/view';
|
||||||
|
import { logger } from '@strudel/core';
|
||||||
|
import { basicSetup, flash, initTheme, extensions, parseBooleans, codemirrorSettings } from '@strudel/codemirror';
|
||||||
|
import { evaluate } from '@strudel/transpiler';
|
||||||
|
|
||||||
|
export class PrebakeCodeMirror {
|
||||||
|
constructor(initialCode, storePrebake, container) {
|
||||||
|
const settings = codemirrorSettings.get();
|
||||||
|
this.storePrebake = storePrebake;
|
||||||
|
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
||||||
|
const initialSettings = Object.keys(compartments).map((key) =>
|
||||||
|
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
||||||
|
);
|
||||||
|
initTheme(settings.theme);
|
||||||
|
let state = EditorState.create({
|
||||||
|
doc: initialCode,
|
||||||
|
extensions: [
|
||||||
|
...initialSettings,
|
||||||
|
basicSetup,
|
||||||
|
javascript(),
|
||||||
|
javascriptLanguage.data.of({
|
||||||
|
closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
||||||
|
bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
||||||
|
}),
|
||||||
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
|
EditorView.updateListener.of((v) => {
|
||||||
|
if (v.docChanged) {
|
||||||
|
this.code = v.state.doc.toString();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
drawSelection({ cursorBlinkRate: 0 }),
|
||||||
|
Prec.highest(
|
||||||
|
keymap.of([
|
||||||
|
{
|
||||||
|
mac: 'Meta-Enter',
|
||||||
|
run: () => {
|
||||||
|
this.savePrebake();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Ctrl-Enter',
|
||||||
|
run: () => {
|
||||||
|
this.savePrebake();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Alt-Enter',
|
||||||
|
run: () => {
|
||||||
|
this.savePrebake();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.code = initialCode;
|
||||||
|
this.view = new EditorView({
|
||||||
|
state,
|
||||||
|
parent: container,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSaveEvent = async (e) => {
|
||||||
|
if (e.detail.view !== this.view) {
|
||||||
|
return; // ignore events from other editors
|
||||||
|
}
|
||||||
|
await this.savePrebake();
|
||||||
|
e?.cancelable && e.preventDefault?.();
|
||||||
|
};
|
||||||
|
const handleToggleComment = (e) => {
|
||||||
|
if (e.detail.view !== this.view) {
|
||||||
|
return; // ignore events from other editors
|
||||||
|
}
|
||||||
|
this.toggleComment();
|
||||||
|
e?.cancelable && e.preventDefault?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('repl-evaluate', handleSaveEvent);
|
||||||
|
document.addEventListener('repl-toggle-comment', handleToggleComment);
|
||||||
|
this.cleanup = () => {
|
||||||
|
document.removeEventListener('prebake-evaluate', handleSaveEvent);
|
||||||
|
document.removeEventListener('prebake-toggle-comment', handleToggleComment);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async savePrebake() {
|
||||||
|
flash(this.view);
|
||||||
|
this.storePrebake(this.code);
|
||||||
|
evaluate(this.code, { addReturn: false }); // run prebake
|
||||||
|
logger('[prebake] prebake saved');
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleComment() {
|
||||||
|
try {
|
||||||
|
// Honor selections; toggleLineComment handles both selections and
|
||||||
|
// single line
|
||||||
|
toggleLineComment(this.view);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error handling repl-toggle-comment event', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCode(code) {
|
||||||
|
const changes = {
|
||||||
|
from: 0,
|
||||||
|
to: this.view.state.doc.length,
|
||||||
|
insert: code,
|
||||||
|
};
|
||||||
|
this.view.dispatch({ changes });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core';
|
import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core';
|
||||||
import { getDrawContext } from '@strudel/draw';
|
import { getDrawContext } from '@strudel/draw';
|
||||||
import { evaluate, transpiler } from '@strudel/transpiler';
|
import { transpiler, evaluate } from '@strudel/transpiler';
|
||||||
import {
|
import {
|
||||||
getAudioContextCurrentTime,
|
getAudioContextCurrentTime,
|
||||||
renderPatternAudio,
|
renderPatternAudio,
|
||||||
@@ -71,6 +71,7 @@ export function useReplContext() {
|
|||||||
const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput;
|
const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput;
|
||||||
const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds;
|
const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds;
|
||||||
const init = useCallback(() => {
|
const init = useCallback(() => {
|
||||||
|
setActivePattern(getViewingPatternData().id);
|
||||||
const drawTime = [-2, 2];
|
const drawTime = [-2, 2];
|
||||||
const drawContext = getDrawContext();
|
const drawContext = getDrawContext();
|
||||||
const editor = new StrudelMirror({
|
const editor = new StrudelMirror({
|
||||||
@@ -86,12 +87,12 @@ export function useReplContext() {
|
|||||||
pattern: silence,
|
pattern: silence,
|
||||||
drawTime,
|
drawTime,
|
||||||
drawContext,
|
drawContext,
|
||||||
prebake: async () =>
|
prebake: async () => {
|
||||||
Promise.all([modulesLoading, presets]).then(() => {
|
await Promise.all([modulesLoading, presets]);
|
||||||
if (prebakeScript?.length) {
|
if (prebakeScript) {
|
||||||
return evaluate(prebakeScript ?? '');
|
return evaluate(prebakeScript, { addReturn: false });
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
onUpdateState: (state) => {
|
onUpdateState: (state) => {
|
||||||
setReplState({ ...state });
|
setReplState({ ...state });
|
||||||
},
|
},
|
||||||
@@ -106,17 +107,24 @@ export function useReplContext() {
|
|||||||
//post to iframe parent (like Udels) if it exists...
|
//post to iframe parent (like Udels) if it exists...
|
||||||
window.parent?.postMessage(code);
|
window.parent?.postMessage(code);
|
||||||
|
|
||||||
setLatestCode(code);
|
// Get the full buffer content from the editor instead of just the evaluated block
|
||||||
window.location.hash = '#' + code2hash(code);
|
const fullBufferCode = editorRef.current?.code || code;
|
||||||
setDocumentTitle(code);
|
setLatestCode(fullBufferCode);
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.location.hash = '#' + code2hash(fullBufferCode);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[useReplContext] Failed to update hash:', e.message);
|
||||||
|
}
|
||||||
|
setDocumentTitle(fullBufferCode);
|
||||||
const viewingPatternData = getViewingPatternData();
|
const viewingPatternData = getViewingPatternData();
|
||||||
setVersionDefaultsFrom(code);
|
setVersionDefaultsFrom(fullBufferCode);
|
||||||
const data = { ...viewingPatternData, code };
|
const data = { ...viewingPatternData, code: fullBufferCode };
|
||||||
let id = data.id;
|
let id = data.id;
|
||||||
const isExamplePattern = viewingPatternData.collection !== userPattern.collection;
|
const isExamplePattern = viewingPatternData.collection !== userPattern.collection;
|
||||||
|
|
||||||
if (isExamplePattern) {
|
if (isExamplePattern) {
|
||||||
const codeHasChanged = code !== viewingPatternData.code;
|
const codeHasChanged = fullBufferCode !== viewingPatternData.code;
|
||||||
if (codeHasChanged) {
|
if (codeHasChanged) {
|
||||||
// fork example
|
// fork example
|
||||||
const newPattern = userPattern.duplicate(data);
|
const newPattern = userPattern.duplicate(data);
|
||||||
@@ -168,9 +176,8 @@ export function useReplContext() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let editorSettings = {};
|
let editorSettings = {};
|
||||||
Object.keys(defaultSettings).forEach((key) => {
|
Object.keys(defaultSettings).forEach((key) => {
|
||||||
if (Object.prototype.hasOwnProperty.call(_settings, key)) {
|
// Don't use hasOwnProperty - nanostore uses proxies so values may not be own properties
|
||||||
editorSettings[key] = _settings[key];
|
editorSettings[key] = _settings[key];
|
||||||
}
|
|
||||||
});
|
});
|
||||||
editorRef.current?.updateSettings(editorSettings);
|
editorRef.current?.updateSettings(editorSettings);
|
||||||
}, [_settings]);
|
}, [_settings]);
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export function loadModules() {
|
|||||||
let modules = [
|
let modules = [
|
||||||
import('@strudel/core'),
|
import('@strudel/core'),
|
||||||
import('@strudel/draw'),
|
import('@strudel/draw'),
|
||||||
|
import('@strudel/edo'),
|
||||||
import('@strudel/tonal'),
|
import('@strudel/tonal'),
|
||||||
import('@strudel/mini'),
|
import('@strudel/mini'),
|
||||||
import('@strudel/xen'),
|
import('@strudel/xen'),
|
||||||
@@ -108,6 +109,7 @@ export function confirmDialog(msg) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
export const SETTING_CHANGE_RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?';
|
export const SETTING_CHANGE_RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?';
|
||||||
|
|
||||||
export function confirmAndReloadPage(onSuccess) {
|
export function confirmAndReloadPage(onSuccess) {
|
||||||
confirmDialog(SETTING_CHANGE_RELOAD_MSG).then((r) => {
|
confirmDialog(SETTING_CHANGE_RELOAD_MSG).then((r) => {
|
||||||
if (r == true) {
|
if (r == true) {
|
||||||
|
|||||||
@@ -18,6 +18,20 @@ export const soundFilterType = {
|
|||||||
ALL: 'all',
|
ALL: 'all',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const initialPrebakeScript = `// Prebake script
|
||||||
|
//
|
||||||
|
// This is code that is loaded before your pattern is run.
|
||||||
|
// You can use it to define custom functions to use in any pattern.
|
||||||
|
//
|
||||||
|
// This is an initial example script. You can edit it to add
|
||||||
|
// your own funtions.
|
||||||
|
//
|
||||||
|
// To use a script shared by some other user you can use
|
||||||
|
// the import-button or paste the script in this editor.
|
||||||
|
|
||||||
|
const ratchet = register('ratchet', (pat) => pat.sometimes(ply(2)))
|
||||||
|
`;
|
||||||
|
|
||||||
export const defaultSettings = {
|
export const defaultSettings = {
|
||||||
activeFooter: 'intro',
|
activeFooter: 'intro',
|
||||||
keybindings: 'codemirror',
|
keybindings: 'codemirror',
|
||||||
@@ -33,6 +47,7 @@ export const defaultSettings = {
|
|||||||
isPatternHighlightingEnabled: true,
|
isPatternHighlightingEnabled: true,
|
||||||
isTabIndentationEnabled: false,
|
isTabIndentationEnabled: false,
|
||||||
isMultiCursorEnabled: false,
|
isMultiCursorEnabled: false,
|
||||||
|
isBlockBasedEvalEnabled: false,
|
||||||
theme: 'strudelTheme',
|
theme: 'strudelTheme',
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
@@ -46,13 +61,14 @@ export const defaultSettings = {
|
|||||||
isPanelPinned: false,
|
isPanelPinned: false,
|
||||||
isPanelOpen: true,
|
isPanelOpen: true,
|
||||||
userPatterns: '{}',
|
userPatterns: '{}',
|
||||||
prebakeScript: '',
|
prebakeScript: initialPrebakeScript,
|
||||||
audioEngineTarget: audioEngineTargets.webaudio,
|
audioEngineTarget: audioEngineTargets.webaudio,
|
||||||
isButtonRowHidden: false,
|
isButtonRowHidden: false,
|
||||||
isCSSAnimationDisabled: false,
|
isCSSAnimationDisabled: false,
|
||||||
maxPolyphony: 128,
|
maxPolyphony: 128,
|
||||||
multiChannelOrbits: false,
|
multiChannelOrbits: false,
|
||||||
includePrebakeScriptInShare: true,
|
includePrebakeScriptInShare: true,
|
||||||
|
settingsTab: 'settings',
|
||||||
};
|
};
|
||||||
|
|
||||||
let search = null;
|
let search = null;
|
||||||
@@ -89,6 +105,7 @@ export const $settings = computed(settingsMap, (state) => {
|
|||||||
isSyncEnabled: isUdels() ? true : parseBoolean(state.isSyncEnabled),
|
isSyncEnabled: isUdels() ? true : parseBoolean(state.isSyncEnabled),
|
||||||
isTabIndentationEnabled: parseBoolean(state.isTabIndentationEnabled),
|
isTabIndentationEnabled: parseBoolean(state.isTabIndentationEnabled),
|
||||||
isMultiCursorEnabled: parseBoolean(state.isMultiCursorEnabled),
|
isMultiCursorEnabled: parseBoolean(state.isMultiCursorEnabled),
|
||||||
|
isBlockBasedEvalEnabled: parseBoolean(state.isBlockBasedEvalEnabled),
|
||||||
fontSize: Number(state.fontSize),
|
fontSize: Number(state.fontSize),
|
||||||
panelPosition: state.activeFooter !== '' && !isUdels() ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is!
|
panelPosition: state.activeFooter !== '' && !isUdels() ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is!
|
||||||
isPanelPinned: parseBoolean(state.isPanelPinned),
|
isPanelPinned: parseBoolean(state.isPanelPinned),
|
||||||
@@ -113,6 +130,7 @@ export function useSettings() {
|
|||||||
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
|
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
|
||||||
export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool);
|
export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool);
|
||||||
export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool);
|
export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool);
|
||||||
|
export const setSettingsTab = (tab) => settingsMap.setKey('settingsTab', tab);
|
||||||
|
|
||||||
export const storePrebakeScript = (script) => settingsMap.setKey('prebakeScript', script);
|
export const storePrebakeScript = (script) => settingsMap.setKey('prebakeScript', script);
|
||||||
|
|
||||||
|
|||||||
@@ -207,9 +207,10 @@ export async function exportPatterns() {
|
|||||||
const userPatterns = userPattern.getAll();
|
const userPatterns = userPattern.getAll();
|
||||||
const blob = new Blob([JSON.stringify(userPatterns)], { type: 'application/json' });
|
const blob = new Blob([JSON.stringify(userPatterns)], { type: 'application/json' });
|
||||||
const downloadLink = document.createElement('a');
|
const downloadLink = document.createElement('a');
|
||||||
|
const prefix = window.location.hostname.split('.').join('_');
|
||||||
downloadLink.href = window.URL.createObjectURL(blob);
|
downloadLink.href = window.URL.createObjectURL(blob);
|
||||||
const date = new Date().toISOString().split('T')[0];
|
const date = new Date().toISOString().split('T')[0];
|
||||||
downloadLink.download = `strudel_patterns_${date}.json`;
|
downloadLink.download = `${prefix}_patterns_${date}.json`;
|
||||||
document.body.appendChild(downloadLink);
|
document.body.appendChild(downloadLink);
|
||||||
downloadLink.click();
|
downloadLink.click();
|
||||||
document.body.removeChild(downloadLink);
|
document.body.removeChild(downloadLink);
|
||||||
|
|||||||
Reference in New Issue
Block a user