mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-15 15:23:54 -04:00
Compare commits
233 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c150752372 | |||
| e248bf85f3 | |||
| 6c51c0261a | |||
| deefde7b50 | |||
| d4f63e8de3 | |||
| 19cb3dedc2 | |||
| a7c3407da7 | |||
| 6870b04fb2 | |||
| 2b2646a768 | |||
| 97ef5bc335 | |||
| 85e6d436ef | |||
| d3e2b7c7b4 | |||
| def1738259 | |||
| 0feeb1e701 | |||
| 813cc6c190 | |||
| 62d4f84e69 | |||
| 49a1e11cd8 | |||
| 941c97da0d | |||
| 95a9d301a1 | |||
| 475f17ddfd | |||
| bd68c6a0a7 | |||
| 2635716c87 | |||
| 58f956b57e | |||
| 0ae2c120b3 | |||
| 05a43ef687 | |||
| f21aeb55bd | |||
| d83139980b | |||
| 0c61cd7670 | |||
| 13b11efa15 | |||
| f73b395648 | |||
| 9c47efd3cf | |||
| f4066089f4 | |||
| 0e81987c4e | |||
| 23dbe53013 | |||
| 0e4d0484a3 | |||
| 37bf2e4dc7 | |||
| e69761397e | |||
| 10c33f24f5 | |||
| ea0ec7edb9 | |||
| f900642359 | |||
| 4cc63b8b01 | |||
| 26832c0fe7 | |||
| 01abc8ae79 | |||
| 84df683794 | |||
| ca1aca0c00 | |||
| e8668a1552 | |||
| 86d65c72f0 | |||
| 9f19e4616a | |||
| e15ce7d402 | |||
| ef5ddc90c2 | |||
| ae32743f12 | |||
| 7294cd5443 | |||
| cdae661b24 | |||
| 28af99914f | |||
| 63f42faca8 | |||
| 2624b97500 | |||
| 633b7287c0 | |||
| 3805ba8f76 | |||
| 82e88d4a28 | |||
| 5cd45d5ea1 | |||
| d930057000 | |||
| daf27ed971 | |||
| 8520044f3e | |||
| 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 | |||
| 2b3f719484 | |||
| 93cf9fcb6a | |||
| a5b867f1b1 | |||
| d8645e9a71 | |||
| 03b221a2a2 | |||
| c96b1c2faf | |||
| 87e723649e | |||
| e35a2000de | |||
| 4b8d80016b | |||
| d2f2c6df02 | |||
| 9bcaaa3ac4 | |||
| 2e7ec9ea27 | |||
| bc9e97b9ac | |||
| 00f0ef56bc | |||
| 44ad05fb20 | |||
| 18cc1e4466 | |||
| 055f009357 | |||
| 2fe1712dae | |||
| 7c22b29493 | |||
| 0ef30d5e8d | |||
| 160ef84b47 | |||
| 1a7f464998 | |||
| ee326c709e | |||
| a0291e8a42 | |||
| ecada58edc | |||
| bb7f587025 | |||
| dab1a018e4 | |||
| 0c01a997ac | |||
| b8dc50267e | |||
| 1aaa04ab50 | |||
| 1860d31624 | |||
| 47557cf06f | |||
| 213edfb211 | |||
| 84e59645fd | |||
| 4df19f9ff8 | |||
| ba7721f53c | |||
| fd1e3d248a | |||
| bad81e3850 | |||
| a409149dae | |||
| a6b3b9ef5e | |||
| ec73c48e2e | |||
| c77675c492 | |||
| 5197af1aa4 | |||
| 65dd79e374 | |||
| 85f6472894 | |||
| 54fd877be1 | |||
| 7d948fd1a7 | |||
| 3373fa39a6 | |||
| 1de8123437 | |||
| 781c477078 | |||
| e2cdce5ac9 | |||
| ad331c8c2e | |||
| 43293dc80f | |||
| 73965a3a39 |
@@ -0,0 +1,11 @@
|
||||
**/node_modules
|
||||
**/.angular
|
||||
**/dist
|
||||
.astro/
|
||||
.forgejo/
|
||||
.idea/
|
||||
.vscode/
|
||||
test/
|
||||
.gitignore
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
@@ -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
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Build and Deploy to beta (warm.strudel.cc)
|
||||
name: Build and Deploy to warm (strudel.cc)
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -37,4 +37,4 @@ jobs:
|
||||
apt update && apt install -y rsync
|
||||
mkdir ~/.ssh
|
||||
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
|
||||
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/warm.strudel.cc
|
||||
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Build and Deploy to live (strudel.cc)
|
||||
name: Build and Deploy to cold (cold.strudel.cc)
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
@@ -34,4 +34,4 @@ jobs:
|
||||
apt update && apt install -y rsync
|
||||
mkdir ~/.ssh
|
||||
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
|
||||
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc
|
||||
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/cold.strudel.cc
|
||||
@@ -29,6 +29,7 @@ const editor = new StrudelMirror({
|
||||
import('@strudel/core'),
|
||||
import('@strudel/draw'),
|
||||
import('@strudel/mini'),
|
||||
import('@strudel/edo'),
|
||||
import('@strudel/tonal'),
|
||||
import('@strudel/webaudio'),
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@strudel/codemirror": "workspace:*",
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/draw": "workspace:*",
|
||||
"@strudel/edo": "workspace:*",
|
||||
"@strudel/mini": "workspace:*",
|
||||
"@strudel/soundfonts": "workspace:*",
|
||||
"@strudel/tonal": "workspace:*",
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from './packages/core/index.mjs';
|
||||
export * from './packages/csound/index.mjs';
|
||||
export * from './packages/desktopbridge/index.mjs';
|
||||
export * from './packages/draw/index.mjs';
|
||||
export * from './packages/edo/index.mjs';
|
||||
export * from './packages/embed/index.mjs';
|
||||
export * from './packages/hydra/index.mjs';
|
||||
export * from './packages/midi/index.mjs';
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"homepage": "https://strudel.cc",
|
||||
"dependencies": {
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/edo": "workspace:*",
|
||||
"@strudel/mini": "workspace:*",
|
||||
"@strudel/tonal": "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';
|
||||
import { persistentAtom } from '@nanostores/persistent';
|
||||
import { logger, registerControl, repl } from '@strudel/core';
|
||||
import { cleanupDraw, Drawer } from '@strudel/draw';
|
||||
|
||||
import { cleanupDraw, cleanupDrawContext, Drawer } from '@strudel/draw';
|
||||
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
||||
import { basicSetup } from './basicSetup.mjs';
|
||||
import { evalBlock } from './block_utilities.mjs';
|
||||
import { flash, isFlashEnabled } from './flash.mjs';
|
||||
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
||||
import { keybindings } from './keybindings.mjs';
|
||||
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||
import { jumpToCharacter } from './labelJump.mjs';
|
||||
import { getSliderWidgets, sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||
import { activateTheme, initTheme, theme } from './themes.mjs';
|
||||
import { isTooltipEnabled } from './tooltip.mjs';
|
||||
import { updateWidgets, widgetPlugin } from './widget.mjs';
|
||||
import { jumpToCharacter } from './labelJump.mjs';
|
||||
import { getActiveWidgets, updateWidgets, widgetPlugin } from './widget.mjs';
|
||||
|
||||
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
||||
|
||||
@@ -64,6 +64,7 @@ export const defaultSettings = {
|
||||
isLineWrappingEnabled: false,
|
||||
isTabIndentationEnabled: false,
|
||||
isMultiCursorEnabled: false,
|
||||
isBlockBasedEvalEnabled: false,
|
||||
theme: 'strudelTheme',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 18,
|
||||
@@ -75,7 +76,7 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS
|
||||
});
|
||||
|
||||
// https://codemirror.net/docs/guide/
|
||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) {
|
||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo, strudelMirror }) {
|
||||
const settings = codemirrorSettings.get();
|
||||
const initialSettings = Object.keys(compartments).map((key) =>
|
||||
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
||||
@@ -105,11 +106,26 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
keymap.of([
|
||||
{
|
||||
key: 'Ctrl-Enter',
|
||||
run: () => onEvaluate?.(),
|
||||
run: () => {
|
||||
// issue with referencing settings, this works more reliably
|
||||
if (strudelMirror?.isBlockBasedEvalEnabled) {
|
||||
evalBlock(strudelMirror);
|
||||
return true;
|
||||
} else {
|
||||
return onEvaluate?.();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'Alt-Enter',
|
||||
run: () => onEvaluate?.(),
|
||||
run: () => {
|
||||
if (strudelMirror?.isBlockBasedEvalEnabled) {
|
||||
evalBlock(strudelMirror);
|
||||
return true;
|
||||
} else {
|
||||
return onEvaluate?.();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'Ctrl-.',
|
||||
@@ -171,6 +187,7 @@ export class StrudelMirror {
|
||||
this.onDraw = onDraw || this.draw;
|
||||
this.id = id || s4();
|
||||
this.solo = solo;
|
||||
this.isBlockBasedEvalEnabled = false; // Will be updated via updateSettings()
|
||||
|
||||
this.drawer = new Drawer((haps, time, _, painters) => {
|
||||
const currentFrame = haps.filter((hap) => hap.isActive(time));
|
||||
@@ -201,20 +218,28 @@ export class StrudelMirror {
|
||||
cleanupDraw(true, id);
|
||||
}
|
||||
},
|
||||
beforeEval: async () => {
|
||||
cleanupDraw(true, id);
|
||||
beforeEval: async ({ blockBased } = {}) => {
|
||||
// Only clean up all drawings for full evaluation
|
||||
// Block-based eval should preserve animations (like .scope()) from other blocks
|
||||
if (!blockBased) {
|
||||
cleanupDraw(true, id);
|
||||
}
|
||||
await this.prebaked;
|
||||
await replOptions?.beforeEval?.();
|
||||
},
|
||||
afterEval: (options) => {
|
||||
// remember for when highlighting is toggled on
|
||||
this.miniLocations = options.meta?.miniLocations;
|
||||
this.widgets = options.meta?.widgets;
|
||||
this.miniLocations = options.meta?.miniLocations || [];
|
||||
this.widgets = options.meta?.widgets || [];
|
||||
|
||||
const sliders = this.widgets.filter((w) => w.type === 'slider');
|
||||
updateSliderWidgets(this.editor, sliders);
|
||||
const widgets = this.widgets.filter((w) => w.type !== 'slider');
|
||||
updateWidgets(this.editor, widgets);
|
||||
updateMiniLocations(this.editor, this.miniLocations);
|
||||
// range-aware update for block-based evaluation
|
||||
const range = options.range && options.range.length >= 2 ? options.range : null;
|
||||
|
||||
updateSliderWidgets(this.editor, sliders, range);
|
||||
updateWidgets(this.editor, widgets, range);
|
||||
updateMiniLocations(this.editor, this.miniLocations, range);
|
||||
replOptions?.afterEval?.(options);
|
||||
// if no painters are set (.onPaint was not called), then we only need
|
||||
// the present moment (for highlighting)
|
||||
@@ -222,8 +247,14 @@ export class StrudelMirror {
|
||||
this.drawer.setDrawTime(drawTime);
|
||||
// invalidate drawer after we've set the appropriate drawTime
|
||||
this.drawer.invalidate(this.repl.scheduler);
|
||||
|
||||
// Clean up draw context if a non-inline widget was removed
|
||||
if (options.widgetRemoved) {
|
||||
cleanupDrawContext(id);
|
||||
}
|
||||
},
|
||||
});
|
||||
this.cleanupDrawContext = () => cleanupDrawContext(id);
|
||||
this.editor = initEditor({
|
||||
root,
|
||||
initialCode,
|
||||
@@ -236,7 +267,9 @@ export class StrudelMirror {
|
||||
onEvaluate: () => this.evaluate(),
|
||||
onStop: () => this.stop(),
|
||||
mondo: replOptions.mondo,
|
||||
strudelMirror: this,
|
||||
});
|
||||
|
||||
const cmEditor = this.root.querySelector('.cm-editor');
|
||||
if (cmEditor) {
|
||||
this.root.style.display = 'block';
|
||||
@@ -260,6 +293,9 @@ export class StrudelMirror {
|
||||
// Handle global evaluation requests (e.g., from Vim :w)
|
||||
this.onEvaluateRequest = (e) => {
|
||||
try {
|
||||
if (e.detail.view !== this.editor) {
|
||||
return; // ignore events from other editors
|
||||
}
|
||||
// Evaluate current editor on repl-evaluate
|
||||
logger('[repl] evaluate via event');
|
||||
this.evaluate();
|
||||
@@ -274,6 +310,9 @@ export class StrudelMirror {
|
||||
// Toggle comments requested from Vim (gc)
|
||||
this.onToggleComment = (e) => {
|
||||
try {
|
||||
if (e.detail.view !== this.editor) {
|
||||
return; // ignore events from other editors
|
||||
}
|
||||
// Honor selections; toggleLineComment handles both selections and
|
||||
// single line
|
||||
toggleLineComment(this.editor);
|
||||
@@ -306,13 +345,17 @@ export class StrudelMirror {
|
||||
this.flash();
|
||||
await this.repl.evaluate(this.code, autostart);
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.repl.scheduler.stop();
|
||||
this.repl.stop();
|
||||
}
|
||||
|
||||
// Listen for global stop requests (e.g., from Vim :q)
|
||||
onStopRequest = (e) => {
|
||||
try {
|
||||
if (e.detail.view !== this.editor) {
|
||||
return; // ignore events from other editors
|
||||
}
|
||||
this.stop();
|
||||
e?.cancelable && e.preventDefault?.();
|
||||
} catch (err) {
|
||||
@@ -326,8 +369,8 @@ export class StrudelMirror {
|
||||
this.evaluate();
|
||||
}
|
||||
}
|
||||
flash(ms) {
|
||||
flash(this.editor, ms);
|
||||
flash(ms, range) {
|
||||
flash(this.editor, ms, range);
|
||||
}
|
||||
highlight(haps, time) {
|
||||
highlightMiniLocations(this.editor, time, haps);
|
||||
@@ -359,6 +402,10 @@ export class StrudelMirror {
|
||||
setLineWrappingEnabled(enabled) {
|
||||
this.reconfigureExtension('isLineWrappingEnabled', enabled);
|
||||
}
|
||||
|
||||
setBlockBasedEvalEnabled(enabled) {
|
||||
this.reconfigureExtension('isBlockBasedEvalEnabled', enabled);
|
||||
}
|
||||
setBracketMatchingEnabled(enabled) {
|
||||
this.reconfigureExtension('isBracketMatchingEnabled', enabled);
|
||||
}
|
||||
@@ -380,6 +427,10 @@ export class StrudelMirror {
|
||||
for (let key in extensions) {
|
||||
this.reconfigureExtension(key, settings[key]);
|
||||
}
|
||||
// Update block-based eval setting on the instance
|
||||
if (settings.isBlockBasedEvalEnabled !== undefined) {
|
||||
this.isBlockBasedEvalEnabled = parseBooleans(settings.isBlockBasedEvalEnabled);
|
||||
}
|
||||
const updated = { ...codemirrorSettings.get(), ...settings };
|
||||
codemirrorSettings.set(updated);
|
||||
}
|
||||
@@ -393,14 +444,26 @@ export class StrudelMirror {
|
||||
this.setFontSize(value);
|
||||
}
|
||||
}
|
||||
setCode(code) {
|
||||
const changes = {
|
||||
from: 0,
|
||||
to: this.editor.state.doc.length,
|
||||
insert: code,
|
||||
};
|
||||
replaceCode(code, from, to) {
|
||||
const changes = { from, to, insert: code };
|
||||
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() {
|
||||
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
|
||||
this.onEvaluateRequest && document.removeEventListener('repl-evaluate', this.onEvaluateRequest);
|
||||
|
||||
@@ -14,7 +14,8 @@ export const flashField = StateField.define({
|
||||
const mark = Decoration.mark({
|
||||
attributes: { style: `background-color: rgba(255,255,255, .4); filter: invert(10%)` },
|
||||
});
|
||||
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
||||
const range = e.value.range || { from: 0, to: tr.newDoc.length };
|
||||
flash = Decoration.set([mark.range(range.from, range.to)]);
|
||||
} else {
|
||||
flash = Decoration.set([]);
|
||||
}
|
||||
@@ -29,8 +30,9 @@ export const flashField = StateField.define({
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
export const flash = (view, ms = 200) => {
|
||||
view.dispatch({ effects: setFlash.of(true) });
|
||||
export const flash = (view, ms = 200, range) => {
|
||||
const flashData = range ? { range } : true;
|
||||
view.dispatch({ effects: setFlash.of(flashData) });
|
||||
setTimeout(() => {
|
||||
view.dispatch({ effects: setFlash.of(false) });
|
||||
}, ms);
|
||||
|
||||
@@ -3,8 +3,9 @@ import { Decoration, EditorView } from '@codemirror/view';
|
||||
|
||||
export const setMiniLocations = StateEffect.define();
|
||||
export const showMiniLocations = StateEffect.define();
|
||||
export const updateMiniLocations = (view, locations) => {
|
||||
view.dispatch({ effects: setMiniLocations.of(locations) });
|
||||
export const displayMiniLocations = StateEffect.define();
|
||||
export const updateMiniLocations = (view, locations, range = null) => {
|
||||
view.dispatch({ effects: setMiniLocations.of({ locations, range }) });
|
||||
};
|
||||
export const highlightMiniLocations = (view, atTime, haps) => {
|
||||
view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) });
|
||||
@@ -21,23 +22,54 @@ const miniLocations = StateField.define({
|
||||
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setMiniLocations)) {
|
||||
// this is called on eval, with the mini locations obtained from the transpiler
|
||||
// codemirror will automatically remap the marks when the document is edited
|
||||
// create a mark for each mini location, adding the range to the spec to find it later
|
||||
const marks = e.value
|
||||
.filter(([from]) => from < tr.newDoc.length)
|
||||
.map(([from, to]) => [from, Math.min(to, tr.newDoc.length)])
|
||||
.map(
|
||||
(range) =>
|
||||
Decoration.mark({
|
||||
id: range.join(':'),
|
||||
// this green is only to verify that the decoration moves when the document is edited
|
||||
// it will be removed later, so the mark is not visible by default
|
||||
attributes: { style: `background-color: #00CA2880` },
|
||||
}).range(...range), // -> Decoration
|
||||
);
|
||||
//block-based eval case
|
||||
if (e.value.range) {
|
||||
const stateMiniLocations = getMiniLocationsFromDecorations(locations);
|
||||
|
||||
locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet<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 miniLocationHighlights = EditorView.decorations.compute([miniLocations, visibleMiniLocations], (state) => {
|
||||
const iterator = state.field(miniLocations).iter();
|
||||
const { haps } = state.field(visibleMiniLocations);
|
||||
const builder = new RangeSetBuilder();
|
||||
const displayMiniLocationsState = StateField.define({
|
||||
create() {
|
||||
return true; // default to showing miniLocations
|
||||
},
|
||||
update(display, tr) {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(displayMiniLocations)) {
|
||||
display = e.value;
|
||||
}
|
||||
}
|
||||
return display;
|
||||
},
|
||||
});
|
||||
|
||||
// // Derive the set of decorations from the miniLocations and visibleLocations
|
||||
const miniLocationHighlights = EditorView.decorations.compute(
|
||||
[miniLocations, visibleMiniLocations, displayMiniLocationsState],
|
||||
(state) => {
|
||||
// Check if miniLocations display is disabled
|
||||
const shouldDisplay = state.field(displayMiniLocationsState);
|
||||
if (!shouldDisplay) {
|
||||
return Decoration.none; // Return empty decorations if display is disabled
|
||||
}
|
||||
|
||||
const iterator = state.field(miniLocations).iter();
|
||||
const { haps } = state.field(visibleMiniLocations);
|
||||
const builder = new RangeSetBuilder();
|
||||
|
||||
while (iterator.value) {
|
||||
const {
|
||||
from,
|
||||
to,
|
||||
value: {
|
||||
spec: { id },
|
||||
},
|
||||
} = iterator;
|
||||
|
||||
if (haps.has(id)) {
|
||||
const hap = haps.get(id);
|
||||
const color = hap.value?.color ?? 'var(--foreground)';
|
||||
const style = hap.value?.markcss || `outline: solid 2px ${color}`;
|
||||
// Get explicit channels for color values
|
||||
/*
|
||||
const swatch = document.createElement('div');
|
||||
swatch.style.color = color;
|
||||
document.body.appendChild(swatch);
|
||||
let channels = getComputedStyle(swatch)
|
||||
.color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/)
|
||||
.slice(1)
|
||||
.map((c) => parseFloat(c || 1));
|
||||
document.body.removeChild(swatch);
|
||||
|
||||
// Get percentage of event
|
||||
const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration;
|
||||
channels[3] *= percent;
|
||||
*/
|
||||
|
||||
builder.add(
|
||||
from,
|
||||
to,
|
||||
Decoration.mark({
|
||||
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
|
||||
attributes: { style },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
iterator.next();
|
||||
}
|
||||
|
||||
return builder.finish();
|
||||
},
|
||||
);
|
||||
|
||||
const getMiniLocationsFromDecorations = (decorations) => {
|
||||
const iterator = decorations.iter();
|
||||
const miniLocationsArray = [];
|
||||
while (iterator.value) {
|
||||
const {
|
||||
from,
|
||||
@@ -89,50 +192,48 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
|
||||
spec: { id },
|
||||
},
|
||||
} = iterator;
|
||||
|
||||
if (haps.has(id)) {
|
||||
const hap = haps.get(id);
|
||||
const color = hap.value?.color ?? 'var(--foreground)';
|
||||
const style = hap.value?.markcss || `outline: solid 2px ${color}`;
|
||||
// Get explicit channels for color values
|
||||
/*
|
||||
const swatch = document.createElement('div');
|
||||
swatch.style.color = color;
|
||||
document.body.appendChild(swatch);
|
||||
let channels = getComputedStyle(swatch)
|
||||
.color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/)
|
||||
.slice(1)
|
||||
.map((c) => parseFloat(c || 1));
|
||||
document.body.removeChild(swatch);
|
||||
|
||||
// Get percentage of event
|
||||
const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration;
|
||||
channels[3] *= percent;
|
||||
*/
|
||||
|
||||
builder.add(
|
||||
from,
|
||||
to,
|
||||
Decoration.mark({
|
||||
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
|
||||
attributes: { style },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
miniLocationsArray.push({
|
||||
from,
|
||||
to,
|
||||
id,
|
||||
});
|
||||
iterator.next();
|
||||
}
|
||||
return miniLocationsArray;
|
||||
};
|
||||
|
||||
return builder.finish();
|
||||
});
|
||||
export const getMiniLocations = (state) => {
|
||||
const decorations = state.field(miniLocations);
|
||||
return getMiniLocationsFromDecorations(decorations);
|
||||
};
|
||||
|
||||
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
|
||||
export const getActiveMiniLocations = (state) => {
|
||||
const miniLocations = getMiniLocations(state);
|
||||
const { haps } = state.field(visibleMiniLocations);
|
||||
|
||||
const activeMiniLocations = miniLocations.filter((location) => haps.has(location.id));
|
||||
return activeMiniLocations;
|
||||
};
|
||||
|
||||
export const highlightExtension = [
|
||||
miniLocations,
|
||||
visibleMiniLocations,
|
||||
displayMiniLocationsState,
|
||||
miniLocationHighlights,
|
||||
];
|
||||
|
||||
export const isPatternHighlightingEnabled = (on, config) => {
|
||||
on &&
|
||||
config &&
|
||||
setTimeout(() => {
|
||||
updateMiniLocations(config.editor, config.miniLocations);
|
||||
}, 100);
|
||||
return on ? Prec.highest(highlightExtension) : [];
|
||||
// NOTE:
|
||||
// Modified this function to always return the highlightExtension, and instead just toggle whether or not the miniLocations are displayed.
|
||||
// This is because block based evaluation only updates regions of miniLocations, and those updates need to be kept track of constantly.
|
||||
// The setTimeout was also removed because it conflicted with the range-specific updates required by
|
||||
// block based evaluation.
|
||||
// Not sure if this is the best approach, but for block based eval I can't think of a better way to do it.
|
||||
|
||||
if (config) {
|
||||
// Toggle the display state for miniLocations
|
||||
config.editor.dispatch({ effects: displayMiniLocations.of(on) });
|
||||
}
|
||||
|
||||
return Prec.highest(highlightExtension);
|
||||
};
|
||||
|
||||
@@ -5,3 +5,4 @@ export * from './slider.mjs';
|
||||
export * from './themes.mjs';
|
||||
export * from './widget.mjs';
|
||||
export { Vim } from './keybindings.mjs';
|
||||
export * from './basicSetup.mjs';
|
||||
|
||||
@@ -6,6 +6,7 @@ import { emacs } from '@replit/codemirror-emacs';
|
||||
import { vim, Vim } from '@replit/codemirror-vim';
|
||||
// import { vim } from './vim_test.mjs';
|
||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||
import { helix, commands } from 'codemirror-helix';
|
||||
import { logger } from '@strudel/core';
|
||||
|
||||
const vscodePlugin = ViewPlugin.fromClass(
|
||||
@@ -20,6 +21,70 @@ const vscodePlugin = ViewPlugin.fromClass(
|
||||
);
|
||||
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
||||
|
||||
function replEval(view) {
|
||||
try {
|
||||
// Dispatch a dedicated evaluate event first
|
||||
let handled = false;
|
||||
try {
|
||||
const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true });
|
||||
handled = document.dispatchEvent(ev) === false; // false means preventDefault was called
|
||||
} catch (e) {
|
||||
console.error('Error dispatching repl-evaluate event', e);
|
||||
}
|
||||
if (handled) {
|
||||
return;
|
||||
}
|
||||
// Try Ctrl+Enter first if not handled by custom event
|
||||
const ctrlEnter = new KeyboardEvent('keydown', {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
ctrlKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
view?.dom?.dispatchEvent?.(ctrlEnter);
|
||||
// If not handled (no handler called preventDefault), try Alt+Enter as
|
||||
// fallback
|
||||
if (!ctrlEnter.defaultPrevented) {
|
||||
const altEnter = new KeyboardEvent('keydown', {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
altKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
view?.dom?.dispatchEvent?.(altEnter);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error dispatching repl evaluation event', e);
|
||||
}
|
||||
}
|
||||
|
||||
function replStop(view) {
|
||||
try {
|
||||
// First try dispatching our custom stop event, then fallback to Alt+.
|
||||
let handled = false;
|
||||
try {
|
||||
const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true });
|
||||
handled = document.dispatchEvent(ev) === false;
|
||||
} catch (e) {
|
||||
console.error('Error dispatching repl-stop event', e);
|
||||
}
|
||||
if (!handled) {
|
||||
const altDot = new KeyboardEvent('keydown', {
|
||||
key: '.',
|
||||
code: 'Period',
|
||||
altKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
view?.dom?.dispatchEvent?.(altDot);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error dispatching repl stop event', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Map Vim :w to trigger the same action as evaluation. We dispatch a custom
|
||||
// event 'repl-evaluate' that the editor listens for, and also simulate
|
||||
// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it
|
||||
@@ -31,7 +96,7 @@ try {
|
||||
// internal actions and works with current selections/visual mode.
|
||||
try {
|
||||
Vim.defineAction('strudelToggleComment', (cm) => {
|
||||
const view = cm?.view || cm;
|
||||
const view = cm.cm6;
|
||||
try {
|
||||
const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
|
||||
document.dispatchEvent(ev);
|
||||
@@ -47,74 +112,23 @@ try {
|
||||
|
||||
// :q to pause/stop
|
||||
Vim.defineEx('quit', 'q', (cm) => {
|
||||
try {
|
||||
const view = cm?.view || cm;
|
||||
// First try dispatching our custom stop event, then fallback to Alt+.
|
||||
let handled = false;
|
||||
try {
|
||||
const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true });
|
||||
handled = document.dispatchEvent(ev) === false;
|
||||
} catch (e) {
|
||||
console.error('Error dispatching repl-stop event', e);
|
||||
}
|
||||
if (!handled) {
|
||||
const altDot = new KeyboardEvent('keydown', {
|
||||
key: '.',
|
||||
code: 'Period',
|
||||
altKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
view?.dom?.dispatchEvent?.(altDot);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error dispatching :q stop event', e);
|
||||
}
|
||||
const view = cm.cm6;
|
||||
logger('[vim] :q — stopping repl');
|
||||
replStop(view);
|
||||
});
|
||||
|
||||
// :w to evaluate
|
||||
Vim.defineEx('write', 'w', (cm) => {
|
||||
const view = cm?.view || cm; // CM6 Vim passes either an object with view or the view itself
|
||||
const view = cm.cm6;
|
||||
try {
|
||||
view?.focus?.();
|
||||
view.focus?.();
|
||||
// Let the app know this came from Vim :w
|
||||
try {
|
||||
logger('[vim] :w — evaluating code');
|
||||
} catch (e) {
|
||||
console.error('Error logging Vim :w evaluation', e);
|
||||
}
|
||||
// Dispatch a dedicated evaluate event first
|
||||
let handled = false;
|
||||
try {
|
||||
const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true });
|
||||
handled = document.dispatchEvent(ev) === false; // false means preventDefault was called
|
||||
} catch (e) {
|
||||
console.error('Error dispatching repl-evaluate event', e);
|
||||
}
|
||||
if (handled) {
|
||||
return;
|
||||
}
|
||||
// Try Ctrl+Enter first if not handled by custom event
|
||||
const ctrlEnter = new KeyboardEvent('keydown', {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
ctrlKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
view?.dom?.dispatchEvent?.(ctrlEnter);
|
||||
// If not handled (no handler called preventDefault), try Alt+Enter as
|
||||
// fallback
|
||||
if (!ctrlEnter.defaultPrevented) {
|
||||
const altEnter = new KeyboardEvent('keydown', {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
altKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
view?.dom?.dispatchEvent?.(altEnter);
|
||||
}
|
||||
replEval(view);
|
||||
} catch (e) {
|
||||
console.error('Error dispatching :w evaluation event', e);
|
||||
}
|
||||
@@ -124,11 +138,49 @@ try {
|
||||
console.error('Vim ex command setup failed (defineEx missing or Vim unavailable)', e);
|
||||
}
|
||||
|
||||
// Map Helix :w to trigger the same action as evaluation. We dispatch a custom
|
||||
// event 'repl-evaluate' that the editor listens for, and also simulate
|
||||
// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it
|
||||
// appears in the Console panel.
|
||||
const helixCommands = commands.of([
|
||||
{
|
||||
// :w to evaluate
|
||||
name: 'write',
|
||||
aliases: ['w'],
|
||||
help: 'Repl-eval',
|
||||
handler(view, args) {
|
||||
try {
|
||||
view?.focus?.(); // Let the app know this came from Helix :w
|
||||
logger('[helix] :w — evaluating code');
|
||||
replEval(view);
|
||||
} catch (e) {
|
||||
console.error('Error dispatching helix :w evaluation event', e);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
// :q to pause/stop
|
||||
name: 'quit',
|
||||
aliases: ['q'],
|
||||
help: 'Repl-stop',
|
||||
handler(view, args) {
|
||||
try {
|
||||
view?.focus?.(); // Let the app know this came from Helix :q
|
||||
logger('[helix] :q — stopping repl');
|
||||
replStop(view);
|
||||
} catch (e) {
|
||||
console.error('Error dispatching helix :q stop event', e);
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const keymaps = {
|
||||
vim,
|
||||
emacs,
|
||||
codemirror: () => keymap.of(defaultKeymap),
|
||||
vscode: vscodeExtension,
|
||||
helix: () => [helix(), helixCommands],
|
||||
};
|
||||
|
||||
export { Vim } from '@replit/codemirror-vim';
|
||||
|
||||
@@ -32,18 +32,19 @@
|
||||
},
|
||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.18.4",
|
||||
"@codemirror/commands": "^6.8.0",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/language": "^6.10.8",
|
||||
"@codemirror/search": "^6.5.8",
|
||||
"@codemirror/state": "^6.5.1",
|
||||
"@codemirror/view": "^6.36.2",
|
||||
"@codemirror/autocomplete": "catalog:",
|
||||
"@codemirror/commands": "catalog:",
|
||||
"@codemirror/lang-javascript": "catalog:",
|
||||
"@codemirror/language": "catalog:",
|
||||
"@codemirror/search": "catalog:",
|
||||
"@codemirror/state": "catalog:",
|
||||
"@codemirror/view": "catalog:",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@nanostores/persistent": "^0.10.2",
|
||||
"@replit/codemirror-emacs": "^6.1.0",
|
||||
"@replit/codemirror-vim": "^6.3.0",
|
||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||
"codemirror-helix": "^0.5.0",
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/draw": "workspace:*",
|
||||
"@strudel/tonal": "workspace:*",
|
||||
|
||||
+106
-17
@@ -2,11 +2,11 @@ import { ref, pure } from '@strudel/core';
|
||||
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
|
||||
import { StateEffect } from '@codemirror/state';
|
||||
|
||||
// Global state storage for all widget types
|
||||
export let sliderValues = {};
|
||||
const getSliderID = (from) => `slider_${from}`;
|
||||
|
||||
export class SliderWidget extends WidgetType {
|
||||
constructor(value, min, max, from, to, step, view) {
|
||||
constructor(value, min, max, from, to, step, view, id) {
|
||||
super();
|
||||
this.value = value;
|
||||
this.min = min;
|
||||
@@ -16,10 +16,21 @@ export class SliderWidget extends WidgetType {
|
||||
this.to = to;
|
||||
this.step = step;
|
||||
this.view = view;
|
||||
this.id = id || `${from}:${to}`; // Range-based ID for stability
|
||||
}
|
||||
|
||||
eq() {
|
||||
return false;
|
||||
eq(other) {
|
||||
if (!(other instanceof SliderWidget)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
this.id === other.id &&
|
||||
this.from === other.from &&
|
||||
this.to === other.to &&
|
||||
this.value === other.value &&
|
||||
this.min === other.min &&
|
||||
this.max === other.max
|
||||
);
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
@@ -38,6 +49,7 @@ export class SliderWidget extends WidgetType {
|
||||
slider.from = this.from;
|
||||
slider.originalFrom = this.originalFrom;
|
||||
slider.to = this.to;
|
||||
slider.id = this.id; // Store range-based ID in DOM element
|
||||
slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)';
|
||||
this.slider = slider;
|
||||
slider.addEventListener('input', (e) => {
|
||||
@@ -49,7 +61,7 @@ export class SliderWidget extends WidgetType {
|
||||
slider.originalValue = insert;
|
||||
slider.value = insert;
|
||||
this.view.dispatch({ changes: change });
|
||||
const id = getSliderID(slider.originalFrom); // matches id generated in transpiler
|
||||
const id = slider.id; // Use range-based ID
|
||||
window.postMessage({ type: 'cm-slider', value: Number(next), id });
|
||||
});
|
||||
return wrap;
|
||||
@@ -62,19 +74,60 @@ export class SliderWidget extends WidgetType {
|
||||
|
||||
export const setSliderWidgets = StateEffect.define();
|
||||
|
||||
export const updateSliderWidgets = (view, widgets) => {
|
||||
view.dispatch({ effects: setSliderWidgets.of(widgets) });
|
||||
export const setSliderWidgetsInRange = StateEffect.define();
|
||||
|
||||
export const updateSliderWidgets = (view, widgets, range = null) => {
|
||||
if (range) {
|
||||
// range argument passed for block-based evaluation
|
||||
view.dispatch({ effects: setSliderWidgetsInRange.of({ widgets, range }) });
|
||||
} else {
|
||||
view.dispatch({ effects: setSliderWidgets.of(widgets) });
|
||||
}
|
||||
};
|
||||
|
||||
function getSliders(widgetConfigs, view) {
|
||||
return widgetConfigs
|
||||
.filter((w) => w.type === 'slider')
|
||||
.map(({ from, to, value, min, max, step }) => {
|
||||
return Decoration.widget({
|
||||
widget: new SliderWidget(value, min, max, from, to, step, view),
|
||||
side: 0,
|
||||
}).range(from /* , to */);
|
||||
});
|
||||
return (
|
||||
widgetConfigs
|
||||
.filter((w) => w.type === 'slider')
|
||||
// Deduplicate sliders that might appear multiple times (e.g., during paste operations)
|
||||
.filter((slider, index, self) => index === self.findIndex((s) => s.from === slider.from && s.to === slider.to))
|
||||
.sort((a, b) => a.from - b.from)
|
||||
.map(({ from, to, value, min, max, step, id }) => {
|
||||
return Decoration.widget({
|
||||
widget: new SliderWidget(value, min, max, from, to, step, view, id),
|
||||
side: 0,
|
||||
}).range(from /* , to */);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function getSliderWidgets(view) {
|
||||
if (!view || !view.state) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sliderPluginInstance = view.plugin(sliderPlugin);
|
||||
if (!sliderPluginInstance || !sliderPluginInstance.decorations) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sliderWidgets = [];
|
||||
|
||||
sliderPluginInstance.decorations.between(0, view.state.doc.length, (from, to, decoration) => {
|
||||
if (decoration.widget instanceof SliderWidget) {
|
||||
sliderWidgets.push({
|
||||
type: 'slider',
|
||||
from: decoration.widget.from,
|
||||
to: decoration.widget.to,
|
||||
value: decoration.widget.value,
|
||||
min: decoration.widget.min,
|
||||
max: decoration.widget.max,
|
||||
step: decoration.widget.step,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return sliderWidgets;
|
||||
}
|
||||
|
||||
export const sliderPlugin = ViewPlugin.fromClass(
|
||||
@@ -101,7 +154,42 @@ export const sliderPlugin = ViewPlugin.fromClass(
|
||||
}
|
||||
}
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setSliderWidgets)) {
|
||||
if (e.is(setSliderWidgetsInRange)) {
|
||||
// Block-aware slider update logic
|
||||
const { widgets, range } = e.value;
|
||||
const [rangeStart, rangeEnd] = range;
|
||||
|
||||
// Get existing slider widgets that should be preserved
|
||||
const existingSliders = [];
|
||||
this.decorations.between(0, update.view.state.doc.length, (from, to, decoration) => {
|
||||
if (decoration.widget instanceof SliderWidget) {
|
||||
// Preserve sliders outside the evaluation range
|
||||
// Use strict > for rangeEnd because when code is deleted, slider positions
|
||||
// map to the deletion boundary (rangeEnd), and those should be removed, not preserved
|
||||
if (from < rangeStart || from > rangeEnd) {
|
||||
existingSliders.push({
|
||||
from,
|
||||
to,
|
||||
value: decoration.widget.value,
|
||||
min: decoration.widget.min,
|
||||
max: decoration.widget.max,
|
||||
step: decoration.widget.step,
|
||||
id: decoration.widget.id || `${from}:${to}`,
|
||||
type: 'slider',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Merge preserved sliders with new widgets
|
||||
const mergedWidgets = [...existingSliders, ...widgets]
|
||||
.filter(
|
||||
(slider, index, self) => index === self.findIndex((s) => s.type === 'slider' && s.id === slider.id),
|
||||
)
|
||||
.sort((a, b) => a.from - b.from);
|
||||
|
||||
this.decorations = Decoration.set(getSliders(mergedWidgets, update.view));
|
||||
} else if (e.is(setSliderWidgets)) {
|
||||
this.decorations = Decoration.set(getSliders(e.value, update.view));
|
||||
}
|
||||
}
|
||||
@@ -132,6 +220,7 @@ export let sliderWithID = (id, value, min, max) => {
|
||||
sliderValues[id] = value; // sync state at eval time (code -> state)
|
||||
return ref(() => sliderValues[id]); // use state at query time
|
||||
};
|
||||
|
||||
// update state when sliders are moved
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('message', (e) => {
|
||||
@@ -140,7 +229,7 @@ if (typeof window !== 'undefined') {
|
||||
// update state when slider is moved
|
||||
sliderValues[e.data.id] = e.data.value;
|
||||
} else {
|
||||
console.warn(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`);
|
||||
console.error(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
+194
-46
@@ -1,55 +1,111 @@
|
||||
import { StateEffect, StateField } from '@codemirror/state';
|
||||
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
|
||||
import { Decoration, EditorView, WidgetType, ViewPlugin } from '@codemirror/view';
|
||||
import { getWidgetID, registerWidgetType } from '@strudel/transpiler';
|
||||
import { Pattern } from '@strudel/core';
|
||||
|
||||
export const addWidget = StateEffect.define({
|
||||
map: ({ from, to }, change) => {
|
||||
return { from: change.mapPos(from), to: change.mapPos(to) };
|
||||
},
|
||||
});
|
||||
export const setWidgets = StateEffect.define();
|
||||
|
||||
export const updateWidgets = (view, widgets) => {
|
||||
view.dispatch({ effects: addWidget.of(widgets) });
|
||||
export const setWidgetsInRange = StateEffect.define();
|
||||
|
||||
export const updateWidgets = (view, widgets, range = null) => {
|
||||
if (range) {
|
||||
// range argument passed for block-based evaluation
|
||||
view.dispatch({ effects: setWidgetsInRange.of({ widgets, range }) });
|
||||
} else {
|
||||
view.dispatch({ effects: setWidgets.of(widgets) });
|
||||
}
|
||||
};
|
||||
|
||||
function getWidgets(widgetConfigs) {
|
||||
return (
|
||||
widgetConfigs
|
||||
// codemirror throws an error if we don't sort
|
||||
.sort((a, b) => a.to - b.to)
|
||||
.map((widgetConfig) => {
|
||||
function getWidgets(widgetConfigs, view) {
|
||||
const filtered = widgetConfigs
|
||||
// Filter to widget configs only (exclude sliders)
|
||||
.filter((w) => w && w.type && w.type !== 'slider')
|
||||
// Deduplicate widgets by ID, matching slider behavior for stable widget identity
|
||||
.filter((widget, index, self) => index === self.findIndex((w) => w.type === widget.type && w.id === widget.id));
|
||||
|
||||
// Filter out widgets whose range is encompassed by another widget
|
||||
// const nonEncompassed = filterEncompassedWidgets(filtered);
|
||||
|
||||
return filtered
|
||||
.sort((a, b) => (a.to || 0) - (b.to || 0))
|
||||
.map((widgetConfig) => {
|
||||
try {
|
||||
return Decoration.widget({
|
||||
widget: new BlockWidget(widgetConfig),
|
||||
widget: new BlockWidget(widgetConfig, view),
|
||||
side: 0,
|
||||
block: true,
|
||||
}).range(widgetConfig.to);
|
||||
})
|
||||
);
|
||||
}).range(widgetConfig.to || widgetConfig.from || 0);
|
||||
} catch (error) {
|
||||
console.error('error creating widget', error);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean); // Remove any null results from failed creations
|
||||
}
|
||||
|
||||
const widgetField = StateField.define(
|
||||
/* <DecorationSet> */ {
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(widgets, tr) {
|
||||
widgets = widgets.map(tr.changes);
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(addWidget)) {
|
||||
try {
|
||||
widgets = widgets.update({
|
||||
filter: () => false,
|
||||
add: getWidgets(e.value),
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('err', error);
|
||||
export const widgetPlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations; //: DecorationSet
|
||||
|
||||
constructor(view /* : EditorView */) {
|
||||
this.decorations = Decoration.set([]);
|
||||
}
|
||||
|
||||
update(update /* : ViewUpdate */) {
|
||||
update.transactions.forEach((tr) => {
|
||||
if (tr.docChanged) {
|
||||
this.decorations = this.decorations.map(tr.changes);
|
||||
const iterator = this.decorations.iter();
|
||||
// Apply changes to iterator.from and iterator.to if docChanged
|
||||
while (iterator.value) {
|
||||
// when the widgets are moved, we need to tell the dom node the current position
|
||||
// this is important because the widget functions have to work with the dom node
|
||||
if (iterator.value?.widget instanceof BlockWidget) {
|
||||
iterator.value.widget.from = iterator.from;
|
||||
iterator.value.widget.to = iterator.to;
|
||||
}
|
||||
iterator.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
return widgets;
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setWidgetsInRange)) {
|
||||
// Block-aware widget update logic
|
||||
const { widgets, range } = e.value;
|
||||
const [rangeStart, rangeEnd] = range;
|
||||
|
||||
// Get existing widget widgets that should be preserved
|
||||
const existingWidgets = [];
|
||||
this.decorations.between(0, update.view.state.doc.length, (from, to, decoration) => {
|
||||
if (decoration.widget instanceof BlockWidget) {
|
||||
// Preserve widgets outside the evaluation range
|
||||
// Use strict > for rangeEnd because when code is deleted, widget positions
|
||||
// map to the deletion boundary (rangeEnd), and those should be removed, not preserved
|
||||
if (from < rangeStart || from > rangeEnd) {
|
||||
existingWidgets.push({
|
||||
from: decoration.widget.from,
|
||||
to: decoration.widget.to,
|
||||
type: decoration.widget.type,
|
||||
index: decoration.widget.index,
|
||||
id: decoration.widget.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Merge preserved widgets with new widgets, deduplicating by ID
|
||||
const mergedWidgets = [...existingWidgets, ...widgets].filter(
|
||||
(widget, index, self) => index === self.findIndex((w) => w.type === widget.type && w.id === widget.id),
|
||||
);
|
||||
|
||||
this.decorations = Decoration.set(getWidgets(mergedWidgets, update.view));
|
||||
} else if (e.is(setWidgets)) {
|
||||
this.decorations = Decoration.set(getWidgets(e.value, update.view));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -60,24 +116,116 @@ export function setWidget(id, el) {
|
||||
}
|
||||
|
||||
export class BlockWidget extends WidgetType {
|
||||
constructor(widgetConfig) {
|
||||
constructor(widgetConfig, view) {
|
||||
super();
|
||||
|
||||
// Graceful handling of invalid configs like sliders
|
||||
if (!widgetConfig || typeof widgetConfig !== 'object') {
|
||||
widgetConfig = { type: 'unknown', from: 0, to: 0 };
|
||||
}
|
||||
|
||||
this.from = widgetConfig.from || 0;
|
||||
this.originalFrom = widgetConfig.from || 0;
|
||||
this.to = widgetConfig.to || this.from;
|
||||
this.originalTo = widgetConfig.to || this.from;
|
||||
this.type = widgetConfig.type || 'unknown';
|
||||
this.index = widgetConfig.index || 0;
|
||||
this.view = view;
|
||||
|
||||
// Use range-based ID for stability, similar to sliders
|
||||
this.id = widgetConfig.id || getWidgetID?.(widgetConfig);
|
||||
this.widgetConfig = widgetConfig;
|
||||
}
|
||||
eq() {
|
||||
return true;
|
||||
|
||||
eq(other) {
|
||||
if (!(other instanceof BlockWidget)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
this.id === other.id &&
|
||||
this.from === other.from &&
|
||||
this.to === other.to &&
|
||||
this.type === other.type &&
|
||||
this.index === other.index
|
||||
);
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
const id = getWidgetID(this.widgetConfig);
|
||||
const el = widgetElements[id];
|
||||
return el;
|
||||
let wrap = document.createElement('span');
|
||||
wrap.setAttribute('aria-hidden', 'true');
|
||||
wrap.className = 'cm-widget-container';
|
||||
|
||||
let el = widgetElements[this.id];
|
||||
if (el) {
|
||||
// Ensure the element has the correct ID
|
||||
el.id = this.id;
|
||||
wrap.appendChild(el);
|
||||
} else {
|
||||
// Create a placeholder element if the widget element doesn't exist
|
||||
// This prevents CodeMirror errors when widget is missing
|
||||
const placeholder = document.createElement('span');
|
||||
placeholder.setAttribute('aria-hidden', 'true');
|
||||
placeholder.className = 'cm-widget-placeholder';
|
||||
placeholder.style.cssText = 'display: none;'; // Hide placeholder
|
||||
placeholder.id = this.id;
|
||||
wrap.appendChild(placeholder);
|
||||
}
|
||||
|
||||
return wrap;
|
||||
}
|
||||
|
||||
ignoreEvent(e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const widgetPlugin = [widgetField];
|
||||
export function getActiveWidgets(view) {
|
||||
if (!view || !view.state) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const widgetPluginInstance = view.plugin(widgetPlugin);
|
||||
if (!widgetPluginInstance || !widgetPluginInstance.decorations) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const widgets = [];
|
||||
|
||||
widgetPluginInstance.decorations.between(0, view.state.doc.length, (from, to, decoration) => {
|
||||
if (decoration.widget instanceof BlockWidget) {
|
||||
widgets.push({
|
||||
type: decoration.widget.type,
|
||||
from: decoration.widget.from,
|
||||
to: decoration.widget.to,
|
||||
index: decoration.widget.index,
|
||||
id: decoration.widget.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
export function getAllWidgetIds(view) {
|
||||
if (!view || !view.state) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const widgetPluginInstance = view.plugin(widgetPlugin);
|
||||
if (!widgetPluginInstance || !widgetPluginInstance.decorations) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const widgetIds = [];
|
||||
|
||||
widgetPluginInstance.decorations.between(0, view.state.doc.length, (from, to, decoration) => {
|
||||
if (decoration.widget instanceof BlockWidget) {
|
||||
widgetIds.push(decoration.widget.id);
|
||||
}
|
||||
});
|
||||
|
||||
return widgetIds;
|
||||
}
|
||||
|
||||
// widget implementer API to create a new widget type
|
||||
export function registerWidget(type, fn) {
|
||||
|
||||
+51
-25
@@ -394,6 +394,18 @@ export const { source, src } = registerControl('source', 'src');
|
||||
*/
|
||||
// also see https://codeberg.org/uzu/strudel/pulls/63
|
||||
export const { n } = registerControl('n');
|
||||
|
||||
/**
|
||||
* Selects the given degree. Currently used in `xen` and `tune`:
|
||||
*
|
||||
* @name i
|
||||
* @tags tonal
|
||||
* @param {number | Pattern} value
|
||||
* @example
|
||||
* i("0 1 2 3 4 5 6 7").xen("<5edo 10edo 15edo hexany15>")
|
||||
*/
|
||||
export const { i } = registerControl('i');
|
||||
|
||||
/**
|
||||
* Plays the given note name or midi number. A note name consists of
|
||||
*
|
||||
@@ -534,6 +546,7 @@ export const { fmi, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2
|
||||
* @name fmenv
|
||||
* @tags fm, envelope, superdough, supradough
|
||||
* @param {number | Pattern} type lin | exp
|
||||
* @synonyms fme
|
||||
* @example
|
||||
* note("c e g b g e")
|
||||
* .fm(4)
|
||||
@@ -543,9 +556,10 @@ export const { fmi, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2
|
||||
* ._scope()
|
||||
*
|
||||
*/
|
||||
export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fmenv8 } = registerMultiControl(
|
||||
export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fmenv8, fme } = registerMultiControl(
|
||||
'fmenv',
|
||||
8,
|
||||
'fme',
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -558,6 +572,7 @@ export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fm
|
||||
* @tags fm, envelope, superdough, supradough
|
||||
* @synonyms fmatt
|
||||
* @param {number | Pattern} time attack time
|
||||
* @synonyms fmatt
|
||||
* @example
|
||||
* note("c e g b g e")
|
||||
* .fm(4)
|
||||
@@ -616,6 +631,7 @@ export const { fmwave, fmwave1, fmwave2, fmwave3, fmwave4, fmwave5, fmwave6, fmw
|
||||
* @tags fm, envelope, superdough, supradough
|
||||
* @synonyms fmdec
|
||||
* @param {number | Pattern} time decay time
|
||||
* @synonyms fmdec
|
||||
* @example
|
||||
* note("c e g b g e")
|
||||
* .fm(4)
|
||||
@@ -655,6 +671,7 @@ export const {
|
||||
* @tags fm, envelope, superdough, supradough
|
||||
* @synonyms fmsus
|
||||
* @param {number | Pattern} level sustain level
|
||||
* @synonyms fmsus
|
||||
* @example
|
||||
* note("c e g b g e")
|
||||
* .fm(4)
|
||||
@@ -1093,7 +1110,7 @@ export const { duckonset } = registerControl('duckonset', 'duckons');
|
||||
*
|
||||
* @name duckattack
|
||||
* @tags amplitude, envelope, orbit, superdough
|
||||
* @synonyms duckatt
|
||||
* @synonyms duckatt, datt
|
||||
*
|
||||
* @param {number | Pattern} time The attack time in seconds
|
||||
* @example
|
||||
@@ -1105,13 +1122,13 @@ export const { duckonset } = registerControl('duckonset', 'duckons');
|
||||
* ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:5").duckattack("0.4:0.1")
|
||||
*
|
||||
*/
|
||||
export const { duckattack } = registerControl('duckattack', 'duckatt');
|
||||
export const { duckattack } = registerControl('duckattack', 'duckatt', 'datt');
|
||||
|
||||
/**
|
||||
* Create byte beats with custom expressions
|
||||
*
|
||||
* @name byteBeatExpression
|
||||
* @synonyms bbexpr
|
||||
* @synonyms bbexpr, bb
|
||||
* @tags superdough
|
||||
*
|
||||
* @param {number | Pattern} byteBeatExpression bitwise expression for creating bytebeat
|
||||
@@ -1119,7 +1136,7 @@ export const { duckattack } = registerControl('duckattack', 'duckatt');
|
||||
* s("bytebeat").bbexpr('t*(t>>15^t>>66)')
|
||||
*
|
||||
*/
|
||||
export const { byteBeatExpression, bbexpr } = registerControl('byteBeatExpression', 'bbexpr');
|
||||
export const { byteBeatExpression, bbexpr } = registerControl('byteBeatExpression', 'bbexpr', 'bb');
|
||||
|
||||
/**
|
||||
* Create byte beats with custom expressions
|
||||
@@ -1166,6 +1183,7 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
|
||||
* Controls the lfo rate for the pulsewidth of the pulse oscillator
|
||||
*
|
||||
* @name pwrate
|
||||
* @synonyms pwr
|
||||
* @tags superdough, lfo
|
||||
* @param {number | Pattern} rate
|
||||
* @example
|
||||
@@ -1173,19 +1191,20 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
|
||||
|
||||
*
|
||||
*/
|
||||
export const { pwrate } = registerControl('pwrate');
|
||||
export const { pwrate } = registerControl('pwrate', 'pwr');
|
||||
|
||||
/**
|
||||
* Controls the lfo sweep for the pulsewidth of the pulse oscillator
|
||||
*
|
||||
* @name pwsweep
|
||||
* @synonyms pws
|
||||
* @tags superdough, lfo
|
||||
* @param {number | Pattern} sweep
|
||||
* @example
|
||||
* n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>")
|
||||
*
|
||||
*/
|
||||
export const { pwsweep } = registerControl('pwsweep');
|
||||
export const { pwsweep } = registerControl('pwsweep', 'pws');
|
||||
|
||||
/**
|
||||
* Phaser audio effect that approximates popular guitar pedals.
|
||||
@@ -1874,19 +1893,6 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback']
|
||||
*/
|
||||
export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb');
|
||||
|
||||
/**
|
||||
* Sets the level of the signal that is fed back into the delay.
|
||||
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
|
||||
*
|
||||
* @name delayfeedback
|
||||
* @tags orbit, superdough, supradough
|
||||
* @param {number | Pattern} feedback between 0 and 1
|
||||
* @synonyms delayfb, dfb
|
||||
* @example
|
||||
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>")
|
||||
*
|
||||
*/
|
||||
export const { delayspeed } = registerControl('delayspeed');
|
||||
/**
|
||||
* Sets the time of the delay effect.
|
||||
*
|
||||
@@ -1898,6 +1904,22 @@ export const { delayspeed } = registerControl('delayspeed');
|
||||
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
|
||||
*
|
||||
*/
|
||||
export const { delayspeed } = registerControl('delayspeed');
|
||||
|
||||
/**
|
||||
* Sets the time of the delay effect in seconds.
|
||||
*
|
||||
* @name delaytime
|
||||
* @tags orbit, superdough, supradough
|
||||
* @param {number | Pattern} delay in seconds
|
||||
* @synonyms delayt, dt
|
||||
* @example
|
||||
* note("d d a# a".fast(2))
|
||||
* .s("sawtooth")
|
||||
* .delay(.8)
|
||||
* .delaytime(1/2)
|
||||
* .delayspeed("<2 .5 -1 -2>")
|
||||
*/
|
||||
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
|
||||
|
||||
/**
|
||||
@@ -1906,12 +1928,12 @@ export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt',
|
||||
* @name delaysync
|
||||
* @tags orbit, superdough
|
||||
* @param {number | Pattern} cycles delay length in cycles
|
||||
* @synonyms delayt, dt
|
||||
* @synonyms delays, ds
|
||||
* @example
|
||||
* s("bd bd").delay(.25).delaysync("<1 2 3 5>".div(8))
|
||||
*
|
||||
*/
|
||||
export const { delaysync } = registerControl('delaysync');
|
||||
export const { delaysync } = registerControl('delaysync', 'delays', 'ds');
|
||||
|
||||
/**
|
||||
* Specifies whether delaytime is calculated relative to cps.
|
||||
@@ -2560,13 +2582,17 @@ export const { compressorRelease } = registerControl('compressorRelease');
|
||||
export const { speed } = registerControl('speed');
|
||||
|
||||
/**
|
||||
* Changes the speed of sample playback, i.e. a cheap way of changing pitch.
|
||||
* Changes the pitch of the sample without changing its speed.
|
||||
* The frequencies are multiplied by (factor + 1) for positive numbers
|
||||
* and by max(factor / 4 + 1, 0) for negative numbers.
|
||||
* So tuning up by octaves can be done with 1, 3, 7, ...
|
||||
* and tuning down by octaves with -2, -3, -3.5...
|
||||
*
|
||||
* @name stretch
|
||||
* @tags pitch, samples
|
||||
* @param {number | Pattern} factor -inf to inf, negative numbers play the sample backwards.
|
||||
* @param {number | Pattern} factor between `-4` and `inf`. Positive increases pitch, 0 does nothing, negative decreases the pitch.
|
||||
* @example
|
||||
* s("gm_flute").stretch("1 2 .5")
|
||||
* s("gm_flute").stretch("<2 1 0 -2>")
|
||||
*
|
||||
*/
|
||||
export const { stretch } = registerControl('stretch');
|
||||
|
||||
@@ -5,6 +5,34 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
export const strudelScope = {};
|
||||
// Make strudelScope available globally so transpiled code can access it
|
||||
globalThis.strudelScope = strudelScope;
|
||||
|
||||
// Track user-defined keys (from block-based eval) so we can clear them without removing strudel functions
|
||||
export const userDefinedKeys = new Set();
|
||||
globalThis.userDefinedKeys = userDefinedKeys;
|
||||
|
||||
/**
|
||||
* Clears all user-defined variables and functions from the scope.
|
||||
* This removes variables created during block-based evaluation.
|
||||
* @name clearScope
|
||||
* @example
|
||||
* // After defining variables in blocks:
|
||||
* // let myVar = 5
|
||||
* // function myFunc() { return 10; }
|
||||
* clearScope() // removes myVar and myFunc from scope
|
||||
*/
|
||||
export const clearScope = () => {
|
||||
for (const key of userDefinedKeys) {
|
||||
delete strudelScope[key];
|
||||
delete globalThis[key];
|
||||
}
|
||||
userDefinedKeys.clear();
|
||||
// Return silence if available (for use in pattern expressions), otherwise undefined
|
||||
return globalThis.silence;
|
||||
};
|
||||
// Make clearScope available globally
|
||||
globalThis.clearScope = clearScope;
|
||||
|
||||
export const evalScope = async (...args) => {
|
||||
const results = await Promise.allSettled(args);
|
||||
|
||||
+146
-1
@@ -1056,7 +1056,55 @@ function _composeOp(a, b, func) {
|
||||
|
||||
// pattern composers
|
||||
const COMPOSERS = {
|
||||
/**
|
||||
* When called on a pattern `a`, with a input pattern `b` (`a.set(b)`),
|
||||
* combines `a` and `b` such that anything defined in `b`
|
||||
* and anything defined in `a` that is *not* defined in `b`
|
||||
* will be in the resulting pattern.
|
||||
*
|
||||
* The structure is maintained from `a`,
|
||||
* because the default pattern alignment is `in`,
|
||||
* see the section on `Pattern Alignment`
|
||||
* in the technical manual in the docs
|
||||
*
|
||||
* This is the inverse of `keep`
|
||||
*
|
||||
* See examples below
|
||||
* @name set
|
||||
* @param {Pattern} pat
|
||||
* @returns {Pattern}
|
||||
* @memberof Pattern
|
||||
* @tags internal, combiners
|
||||
* @example
|
||||
* // because input pattern has `s` set,
|
||||
* // it overrides the "sine" declared earlier
|
||||
* note("c a f e").s("sine").set(s("triangle"))
|
||||
*/
|
||||
set: [(a, b) => b],
|
||||
/**
|
||||
* When called on a pattern `a`, with a input pattern `b` (`a.keep(b)`),
|
||||
* combines `a` and `b` such that anything defined in `a`,
|
||||
* and anything defined in `b` that is *not* defined in `a`
|
||||
* will be in the resulting pattern
|
||||
*
|
||||
* The structure is maintained from `a`,
|
||||
* because the default pattern alignment is `in`,
|
||||
* see the section on `Pattern Alignment`
|
||||
* in the technical manual in the docs
|
||||
*
|
||||
* This is the inverse of `set`
|
||||
*
|
||||
* See examples below
|
||||
* @name keep
|
||||
* @param {Pattern} pat
|
||||
* @memberof Pattern
|
||||
* @returns {Pattern}
|
||||
* @tags internal, combiners
|
||||
* @example
|
||||
* // notes, already defined, will stay "c a f e",
|
||||
* // while "s", not defined, will be set to "piano"
|
||||
* note("c a f e").keep(note("e f a c").s("piano"))
|
||||
*/
|
||||
keep: [(a) => a],
|
||||
keepif: [(a, b) => (b ? a : undefined)],
|
||||
|
||||
@@ -1110,7 +1158,6 @@ const COMPOSERS = {
|
||||
div: [numeralArgs((a, b) => a / b)],
|
||||
mod: [numeralArgs(_mod)],
|
||||
pow: [numeralArgs(Math.pow)],
|
||||
log2: [numeralArgs(Math.log2)],
|
||||
band: [numeralArgs((a, b) => a & b)],
|
||||
bor: [numeralArgs((a, b) => a | b)],
|
||||
bxor: [numeralArgs((a, b) => a ^ b)],
|
||||
@@ -1694,6 +1741,12 @@ export const func = curry((a, b) => reify(b).func(a));
|
||||
*
|
||||
*/
|
||||
export function register(name, func, patternify = true, preserveSteps = false, join = (x) => x.innerJoin()) {
|
||||
if (isPattern(name)) {
|
||||
throw new Error(
|
||||
'Name argument for register is a pattern, try using single quotes (\'name\') instead of double quotes ("name")',
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(name)) {
|
||||
const result = {};
|
||||
for (const name_item of name) {
|
||||
@@ -1816,6 +1869,8 @@ export const floor = register('floor', function (pat) {
|
||||
return pat.asNumber().fmap((v) => Math.floor(v));
|
||||
});
|
||||
|
||||
export const log2 = register('log2', (pat) => pat.asNumber().fmap((v) => Math.log2(v)));
|
||||
|
||||
/**
|
||||
* Assumes a numerical pattern. Returns a new pattern with all values set to
|
||||
* their mathematical ceiling. E.g. `3.2` replaced with `4`, and `-4.2`
|
||||
@@ -2497,6 +2552,20 @@ export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func,
|
||||
return stack(left, right).setSteps(__steps ? lcm(left._steps, right._steps) : undefined);
|
||||
});
|
||||
|
||||
/**
|
||||
* Like juxBy, except it flips the ears each cycle.
|
||||
* @name juxFlipBy
|
||||
* @synonyms juxflipby, fluxBy, fluxby
|
||||
* @example
|
||||
* s("bd lt [~ ht] mt cp ~ bd hh").juxFlipBy(".8", rev)
|
||||
*/
|
||||
export const { juxFlipBy, juxflipby, fluxBy, fluxby } = register(
|
||||
['juxFlipBy', 'juxflipby', 'fluxBy', 'fluxby'],
|
||||
function (by, func, pat) {
|
||||
return pat.juxBy(slowcat(by, -by), func);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* The jux function creates strange stereo effects, by applying a function to a pattern, but only in the right-hand channel.
|
||||
* @tags temporal, superdough
|
||||
@@ -2511,6 +2580,21 @@ export const jux = register('jux', function (func, pat) {
|
||||
return pat._juxBy(1, func, pat);
|
||||
});
|
||||
|
||||
/**
|
||||
* Like jux, but flips the ears each cycle.
|
||||
* @name juxFlip
|
||||
* @synonyms juxflip, flux
|
||||
* @example
|
||||
* s("bd lt [~ ht] mt cp ~ bd hh").juxFlip(rev)
|
||||
* @example
|
||||
* s("bd lt [~ ht] mt cp ~ bd hh").juxFlip(press)
|
||||
* @example
|
||||
* s("bd lt [~ ht] mt cp ~ bd hh").juxFlip(iter(4))
|
||||
*/
|
||||
export const { juxFlip, flux } = register(['juxFlip', 'juxflip', 'flux'], function (func, pat) {
|
||||
return pat._juxFlipBy(1, func, pat);
|
||||
});
|
||||
|
||||
/**
|
||||
* Superimpose and offset multiple times, applying the given function each time.
|
||||
* @tags temporal, functional
|
||||
@@ -3275,6 +3359,11 @@ Pattern.prototype.shrinklist = function (amount) {
|
||||
|
||||
export const shrinklist = (amount, pat) => pat.shrinklist(amount);
|
||||
|
||||
Pattern.prototype.growlist = function (amount) {
|
||||
return this.shrinklist(amount).reverse();
|
||||
};
|
||||
export const growlist = (amount, pat) => pat.growlist(amount);
|
||||
|
||||
/**
|
||||
* *Experimental*
|
||||
*
|
||||
@@ -4043,3 +4132,59 @@ Pattern.prototype.worklet = function (src, ...inputs) {
|
||||
};
|
||||
|
||||
export const worklet = (...args) => pure({}).worklet(...args);
|
||||
|
||||
/**
|
||||
* Creates a pattern of numbers in base b from a number or pattern of numbers
|
||||
* limited to d digits long from the right
|
||||
*
|
||||
* @name base
|
||||
* @tags generators
|
||||
* @param {number} n - number to convert (can be a pattern or array)
|
||||
* @param {number} b - base to convert to (defaults to 10) (can be a pattern)
|
||||
* @param {number} d - max number of digits to produce for each n (defaults to 0 for all) (can be a pattern)
|
||||
* @example
|
||||
* $: note(base("7175 543", 10, 3)).scale("c:major").s("saw")
|
||||
* // $: note("1 7 5 5 4 3").scale("c:major").s("saw")
|
||||
*/
|
||||
export const base = (n, b = 10, d = 0) => {
|
||||
if (Array.isArray(n)) {
|
||||
n = sequence(n);
|
||||
}
|
||||
n = reify(n);
|
||||
b = reify(b);
|
||||
d = reify(d);
|
||||
|
||||
return d
|
||||
.withValue((e) => {
|
||||
return b
|
||||
.withValue((c) => {
|
||||
return n
|
||||
.withValue((v) => {
|
||||
let digits = [];
|
||||
let value = v;
|
||||
while (value > 0) {
|
||||
digits.unshift(value % c);
|
||||
value = Math.floor(value / c);
|
||||
}
|
||||
if (e) {
|
||||
const l = digits.length;
|
||||
if (l > e) {
|
||||
digits = digits.slice(-1 * e);
|
||||
}
|
||||
/*
|
||||
if (l < e){
|
||||
for (let i = l; i < e; i++) {
|
||||
digits.unshift("~");//0); //Would like to be padding this but ~- doesn't work
|
||||
}
|
||||
console.log("digits", digits);
|
||||
}
|
||||
*/
|
||||
}
|
||||
return sequence(digits);
|
||||
})
|
||||
.squeezeJoin();
|
||||
})
|
||||
.squeezeJoin();
|
||||
})
|
||||
.squeezeJoin();
|
||||
};
|
||||
|
||||
+11
-8
@@ -71,29 +71,32 @@ export const pickmod = register('pickmod', function (lookup, pat) {
|
||||
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
|
||||
* @tags combiners, functional
|
||||
* @param {Pattern} pat
|
||||
* @param {Pattern} lookup a pattern of indices
|
||||
* @param {function[]} funcs the array of functions from which to pull
|
||||
* @param {Pattern} lookup a pattern of indices or names
|
||||
* @param {function[] | object} lookup the array or lookup object of functions from which to pull
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
|
||||
* @example
|
||||
* note("<c2 d2>(3,8)").s("square")
|
||||
* .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) {
|
||||
return pat.apply(pick(lookup, funcs));
|
||||
export const pickF = register('pickF', function (pickPattern, lookup, pat) {
|
||||
return pat.apply(pick(lookup, pickPattern));
|
||||
});
|
||||
|
||||
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @tags combiners
|
||||
* @param {Pattern} pat
|
||||
* @param {Pattern} lookup a pattern of indices
|
||||
* @param {function[]} funcs the array of functions from which to pull
|
||||
* @param {Pattern} lookup a pattern of indices or names
|
||||
* @param {function[] | object} lookup the array or lookup object of functions from which to pull
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
|
||||
return pat.apply(pickmod(lookup, funcs));
|
||||
export const pickmodF = register('pickmodF', function (pickPattern, lookup, pat) {
|
||||
return pat.apply(pickmod(lookup, pickPattern));
|
||||
});
|
||||
|
||||
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
|
||||
|
||||
+310
-40
@@ -38,6 +38,7 @@ export function repl({
|
||||
pattern: undefined,
|
||||
miniLocations: [],
|
||||
widgets: [],
|
||||
sliders: [],
|
||||
pending: false,
|
||||
started: false,
|
||||
};
|
||||
@@ -79,11 +80,132 @@ export function repl({
|
||||
let allTransform;
|
||||
let eachTransform;
|
||||
|
||||
// Block-based evaluation state
|
||||
let codeBlocks = {};
|
||||
let lastActiveVisualizerLabel = null;
|
||||
// Track which patterns belong to which blocks: { blockRange: [patternKeys] }
|
||||
let blockPatterns = new Map();
|
||||
|
||||
// Helper function to collect properties from all code blocks (handles both labeled and anonymous blocks)
|
||||
function collectFromBlocks(property) {
|
||||
return Object.entries(codeBlocks).flatMap(([key, block]) => {
|
||||
if (key === '$') {
|
||||
// Anonymous blocks are stored as an array of block objects
|
||||
return Array.isArray(block) ? block.flatMap((b) => b[property] || []) : [];
|
||||
}
|
||||
// Labeled blocks are stored as single block objects
|
||||
return block[property] || [];
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to process a single labeled block
|
||||
function processLabeledBlock(labels, i, code, options, meta) {
|
||||
const label = labels[i];
|
||||
const nextLabel = labels[i + 1] || { index: code.length, end: code.length };
|
||||
|
||||
const labelCode = code.slice(label.index, nextLabel.index);
|
||||
const labelRange = [label.index + options.range[0], label.end + options.range[0]];
|
||||
|
||||
// Calculate the full block range (from label start to next label start)
|
||||
const blockStart = label.index + options.range[0];
|
||||
const blockEnd = nextLabel.index + options.range[0];
|
||||
|
||||
const blockWidgets = (meta?.widgets || []).filter((widget) => {
|
||||
const widgetPos = widget.from ?? widget.index ?? 0;
|
||||
return widgetPos >= blockStart && widgetPos < blockEnd;
|
||||
});
|
||||
|
||||
const blockSliders = (meta?.sliders || []).filter((slider) => {
|
||||
const sliderPos = slider.from ?? slider.index ?? 0;
|
||||
return sliderPos >= blockStart && sliderPos < blockEnd;
|
||||
});
|
||||
|
||||
const blockMiniLocations = (meta?.miniLocations || []).filter((loc) => {
|
||||
// const locStart = loc.start ?? loc.from ?? 0;
|
||||
// mini locations can be either [start, end] arrays or objects with start/from
|
||||
const locStart = Array.isArray(loc) ? loc[0] : (loc.start ?? loc.from ?? 0);
|
||||
return locStart >= blockStart && locStart < blockEnd;
|
||||
});
|
||||
|
||||
handleSingleLabelBlock(
|
||||
label,
|
||||
labelCode,
|
||||
{ ...options, range: labelRange },
|
||||
{ widgets: blockWidgets, sliders: blockSliders, miniLocations: blockMiniLocations },
|
||||
);
|
||||
}
|
||||
|
||||
// helper
|
||||
function cleanupConflictingRanges(codeBlocks, currentKey, newRange) {
|
||||
for (const [existingKey, existingBlock] of Object.entries(codeBlocks)) {
|
||||
if (existingKey === currentKey) continue;
|
||||
if (!existingBlock.range) continue;
|
||||
|
||||
const [existingStart, existingEnd] = existingBlock.range;
|
||||
const [newStart, newEnd] = newRange;
|
||||
|
||||
// If ranges overlap (not just touch), remove the stale block
|
||||
if (!(newEnd <= existingStart || newStart >= existingEnd)) {
|
||||
delete codeBlocks[existingKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// helper
|
||||
function handleSingleLabelBlock(label, code, options, meta) {
|
||||
// Detect if this block contains a non-inline widget
|
||||
// The activeVisualizer is now provided by the transpiler for all labels
|
||||
const activeVisualizer = label.activeVisualizer || null;
|
||||
|
||||
if (activeVisualizer !== null) {
|
||||
lastActiveVisualizerLabel = label.name;
|
||||
}
|
||||
|
||||
// Store the entire code block under the label name
|
||||
codeBlocks[label.name] = {
|
||||
code: code,
|
||||
range: options.range,
|
||||
labels: [label.name],
|
||||
miniLocations: meta?.miniLocations || [],
|
||||
widgets: meta?.widgets || [],
|
||||
sliders: meta?.sliders || [],
|
||||
activeVisualizer: activeVisualizer, // Store the widget type if present, null otherwise
|
||||
};
|
||||
|
||||
// Clean up any blocks with conflicting ranges (including declaration blocks)
|
||||
cleanupConflictingRanges(codeBlocks, label.name, options.range);
|
||||
}
|
||||
|
||||
// helper
|
||||
// These blocks return silence but may contain mini notation strings that need highlighting
|
||||
function handleDeclarationBlock(code, options, meta) {
|
||||
const range = options.range || [];
|
||||
if (range.length < 2) return;
|
||||
|
||||
const blockKey = `_decl:${range[0]}:${range[1]}`;
|
||||
|
||||
codeBlocks[blockKey] = {
|
||||
code: code,
|
||||
range: range,
|
||||
labels: [],
|
||||
miniLocations: meta?.miniLocations || [],
|
||||
widgets: meta?.widgets || [],
|
||||
sliders: meta?.sliders || [],
|
||||
activeVisualizer: null,
|
||||
};
|
||||
|
||||
// Clean up any overlapping declaration blocks
|
||||
cleanupConflictingRanges(codeBlocks, blockKey, range);
|
||||
}
|
||||
|
||||
const hush = function () {
|
||||
pPatterns = {};
|
||||
anonymousIndex = 0;
|
||||
allTransform = undefined;
|
||||
eachTransform = undefined;
|
||||
codeBlocks = {};
|
||||
blockPatterns.clear();
|
||||
lastActiveVisualizerLabel = null; // Reset 'all' visualizer tracking
|
||||
return silence;
|
||||
};
|
||||
|
||||
@@ -103,7 +225,60 @@ export function repl({
|
||||
};
|
||||
setTime(() => scheduler.now()); // TODO: refactor?
|
||||
|
||||
const stop = () => scheduler.stop();
|
||||
// Helper function to apply pattern transformations (solo, each, all)
|
||||
// this should be abstracted more
|
||||
function applyPatternTransforms(pattern) {
|
||||
const allPatterns = Object.values(pPatterns);
|
||||
|
||||
if (allPatterns.length) {
|
||||
let patterns = [];
|
||||
let soloActive = false;
|
||||
for (const [key, value] of Object.entries(pPatterns)) {
|
||||
// handle soloed patterns ex: S$: s("bd!4")
|
||||
const isSolod = key.length > 1 && key.startsWith('S');
|
||||
if (isSolod && soloActive === false) {
|
||||
// first time we see a soloed pattern, clear existing patterns
|
||||
patterns = [];
|
||||
soloActive = true;
|
||||
}
|
||||
if (!soloActive || (soloActive && isSolod)) {
|
||||
const valWithState = value.withState((state) => state.setControls({ id: key }));
|
||||
patterns.push(valWithState);
|
||||
}
|
||||
}
|
||||
if (eachTransform) {
|
||||
// Explicit lambda so only element (not index and array) are passed
|
||||
patterns = patterns.map((x) => eachTransform(x));
|
||||
}
|
||||
pattern = stack(...patterns);
|
||||
} else if (eachTransform) {
|
||||
pattern = eachTransform(pattern);
|
||||
}
|
||||
if (allTransforms.length) {
|
||||
for (const transform of allTransforms) {
|
||||
pattern = transform(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPattern(pattern)) {
|
||||
pattern = silence;
|
||||
}
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
codeBlocks = {};
|
||||
blockPatterns.clear();
|
||||
pPatterns = {};
|
||||
lastActiveVisualizerLabel = null; // Reset 'all' visualizer tracking
|
||||
updateState({
|
||||
miniLocations: [],
|
||||
widgets: [],
|
||||
sliders: [],
|
||||
});
|
||||
scheduler.stop();
|
||||
};
|
||||
const start = () => scheduler.start();
|
||||
const pause = () => scheduler.pause();
|
||||
const toggle = () => scheduler.toggle();
|
||||
@@ -132,8 +307,12 @@ export function repl({
|
||||
|
||||
let allTransforms = [];
|
||||
/**
|
||||
* Applies a function to all the running patterns. Note that the patterns are groups together into a single `stack` before the function is applied. This is probably what you want, but see `each` for
|
||||
* Applies a function to all the running patterns. Note that the patterns are grouped together into a single `stack` before the function is applied. This is probably what you want, but see `each` for
|
||||
* a version that applies the function to each pattern separately.
|
||||
*
|
||||
* **Note:** Patterns must be labeled (e.g. with `$:`) to be picked up by `all`. An unlabeled
|
||||
* pattern such as `note("c4")` is not registered and will produce no audio when `all` is present.
|
||||
* Use `$: note("c4")` instead.
|
||||
* ```
|
||||
* $: sound("bd - cp sd")
|
||||
* $: sound("hh*8")
|
||||
@@ -153,6 +332,9 @@ export function repl({
|
||||
};
|
||||
/** Applies a function to each of the running patterns separately. This is intended for future use with upcoming 'stepwise' features. See `all` for a version that applies the function to all the patterns stacked together into a single pattern.
|
||||
*
|
||||
* **Note:** Patterns must be labeled (e.g. with `$:`) to be picked up by `each`. An unlabeled
|
||||
* pattern such as `note("c4")` is not registered and will produce no audio when `each` is present.
|
||||
* Use `$: note("c4")` instead.
|
||||
* ```
|
||||
* $: sound("bd - cp sd")
|
||||
* $: sound("hh*8")
|
||||
@@ -217,7 +399,7 @@ export function repl({
|
||||
});
|
||||
};
|
||||
|
||||
const evaluate = async (code, autostart = true, shouldHush = true) => {
|
||||
const evaluate = async (code, autostart = true) => {
|
||||
if (!code) {
|
||||
throw new Error('no code to evaluate');
|
||||
}
|
||||
@@ -225,59 +407,34 @@ export function repl({
|
||||
updateState({ code, pending: true });
|
||||
await injectPatternMethods();
|
||||
setTime(() => scheduler.now()); // TODO: refactor?
|
||||
await beforeEval?.({ code });
|
||||
await beforeEval?.({ code, blockBased: false });
|
||||
allTransforms = []; // reset all transforms
|
||||
shouldHush && hush();
|
||||
|
||||
codeBlocks = {};
|
||||
hush();
|
||||
|
||||
if (mondo) {
|
||||
code = `mondolang\`${code}\``;
|
||||
}
|
||||
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
||||
if (Object.keys(pPatterns).length) {
|
||||
let patterns = [];
|
||||
let soloActive = false;
|
||||
for (const [key, value] of Object.entries(pPatterns)) {
|
||||
// handle soloed patterns ex: S$: s("bd!4")
|
||||
const isSolod = key.length > 1 && key.startsWith('S');
|
||||
if (isSolod && soloActive === false) {
|
||||
// first time we see a soloed pattern, clear existing patterns
|
||||
patterns = [];
|
||||
soloActive = true;
|
||||
}
|
||||
if (!soloActive || (soloActive && isSolod)) {
|
||||
const valWithState = value.withState((state) => state.setControls({ id: key }));
|
||||
patterns.push(valWithState);
|
||||
}
|
||||
}
|
||||
if (eachTransform) {
|
||||
// Explicit lambda so only element (not index and array) are passed
|
||||
patterns = patterns.map((x) => eachTransform(x));
|
||||
}
|
||||
pattern = stack(...patterns);
|
||||
} else if (eachTransform) {
|
||||
pattern = eachTransform(pattern);
|
||||
}
|
||||
if (allTransforms.length) {
|
||||
for (const transform of allTransforms) {
|
||||
pattern = transform(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPattern(pattern)) {
|
||||
pattern = silence;
|
||||
}
|
||||
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
||||
|
||||
pattern = applyPatternTransforms(pattern);
|
||||
|
||||
logger(`[eval] code updated`);
|
||||
pattern = await setPattern(pattern, autostart);
|
||||
updateState({
|
||||
miniLocations: meta?.miniLocations || [],
|
||||
widgets: meta?.widgets || [],
|
||||
sliders: meta?.sliders || [],
|
||||
activeCode: code,
|
||||
pattern,
|
||||
evalError: undefined,
|
||||
schedulerError: undefined,
|
||||
pending: false,
|
||||
});
|
||||
afterEval?.({ code, pattern, meta });
|
||||
|
||||
afterEval?.({ code, pattern, meta, range: undefined, widgetRemoved: false });
|
||||
return pattern;
|
||||
} catch (err) {
|
||||
logger(`[eval] error: ${err.message}`, 'error');
|
||||
@@ -286,8 +443,121 @@ export function repl({
|
||||
onEvalError?.(err);
|
||||
}
|
||||
};
|
||||
|
||||
const evaluateBlock = async (code, autostart = true, options = {}) => {
|
||||
if (!code) {
|
||||
throw new Error('no code to evaluate');
|
||||
}
|
||||
try {
|
||||
updateState({ code, pending: true });
|
||||
await injectPatternMethods();
|
||||
setTime(() => scheduler.now()); // TODO: refactor?
|
||||
await beforeEval?.({ code, blockBased: true });
|
||||
allTransforms = []; // reset all transforms
|
||||
|
||||
const transpilerOptionsWithBlock = {
|
||||
...transpilerOptions,
|
||||
blockBased: true,
|
||||
range: options.range || [],
|
||||
};
|
||||
|
||||
if (mondo) {
|
||||
code = `mondolang\`${code}\``;
|
||||
}
|
||||
|
||||
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptionsWithBlock);
|
||||
|
||||
// Track activeVisualizer cleanup: check if any block's visualizer was removed
|
||||
let widgetRemoved = false;
|
||||
|
||||
const labels = meta.labels || [];
|
||||
|
||||
// Check for anonymous labels (labels starting with '$')
|
||||
const hasAnonymousLabel = labels.some((label) => label.name.startsWith('$'));
|
||||
|
||||
// Store code blocks in dictionary using labels as keys
|
||||
if (hasAnonymousLabel) {
|
||||
// variable/function declarations that don't return patterns are allowed,
|
||||
// but anonymous pattern blocks pose an issue for block-based evaluation
|
||||
// if an anonymous pattern is evaluated multiple times it will just stack and get louder and louder
|
||||
|
||||
// it's very common for users to write code prefixed with '$'
|
||||
// but to modify and override existing patterns, the patterns must be labeled,
|
||||
// otherwise we'll have no idea of which pattern is being overridden
|
||||
|
||||
// (we probably need to update the docs on this)
|
||||
// we could easily enable it, but it would confuse a lot of people
|
||||
|
||||
throw new Error(
|
||||
'anonymous labels disabled for block based evaluation (see https://strudel.cc/blog/#label-notation)',
|
||||
);
|
||||
} else if (labels.length > 0) {
|
||||
for (let i = 0; i < labels.length; i++) {
|
||||
// processing transpiler output instead of code is simply to avoid
|
||||
// extra regex in detecting whether or not an inline widget has been commented out
|
||||
processLabeledBlock(labels, i, meta.output, options, meta);
|
||||
}
|
||||
} else {
|
||||
// Declaration block (variable/function that returns silence)
|
||||
// Store it so its miniLocations are preserved for highlighting patterns stored in variables
|
||||
handleDeclarationBlock(code, options, meta);
|
||||
}
|
||||
|
||||
meta.miniLocations = collectFromBlocks('miniLocations');
|
||||
meta.widgets = collectFromBlocks('widgets');
|
||||
meta.sliders = collectFromBlocks('sliders');
|
||||
|
||||
// Track activeVisualizer cleanup: check if any block's visualizer was removed
|
||||
const blocksToUpdate = labels.map((label) => label.name);
|
||||
|
||||
// this is the hackiest bit
|
||||
for (const [key, block] of Object.entries(codeBlocks)) {
|
||||
if (blocksToUpdate.includes(key)) {
|
||||
// This block was just updated
|
||||
if (block.activeVisualizer !== null) {
|
||||
// Block now has a visualizer, update tracking
|
||||
lastActiveVisualizerLabel = key;
|
||||
} else if (lastActiveVisualizerLabel === key) {
|
||||
// This block lost its visualizer, trigger cleanup
|
||||
widgetRemoved = true;
|
||||
lastActiveVisualizerLabel = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pPatterns = Object.fromEntries(
|
||||
Object.entries(pPatterns).filter(([key]) => {
|
||||
return Object.keys(codeBlocks).includes(key);
|
||||
}),
|
||||
);
|
||||
|
||||
pattern = applyPatternTransforms(pattern);
|
||||
|
||||
logger(`[eval] code updated`);
|
||||
pattern = await setPattern(pattern, autostart);
|
||||
updateState({
|
||||
miniLocations: meta?.miniLocations || [],
|
||||
widgets: meta?.widgets || [],
|
||||
sliders: meta?.sliders || [],
|
||||
activeCode: code,
|
||||
pattern,
|
||||
evalError: undefined,
|
||||
schedulerError: undefined,
|
||||
pending: false,
|
||||
});
|
||||
|
||||
afterEval?.({ code, pattern, meta, range: options.range, widgetRemoved });
|
||||
return pattern;
|
||||
} catch (err) {
|
||||
logger(`[eval] error: ${err.message}`, 'error');
|
||||
console.error(err);
|
||||
updateState({ evalError: err, pending: false });
|
||||
onEvalError?.(err);
|
||||
}
|
||||
};
|
||||
|
||||
const setCode = (code) => updateState({ code });
|
||||
return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state };
|
||||
return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state };
|
||||
}
|
||||
|
||||
export const getTrigger =
|
||||
|
||||
@@ -316,7 +316,7 @@ export const run = (n) => saw.range(0, n).round().segment(n);
|
||||
* // "hh".s().struct("1 0 1")
|
||||
*/
|
||||
export const binary = (n) => {
|
||||
const nBits = reify(n).log2(0).floor().add(1);
|
||||
const nBits = reify(n).log2().floor().add(1);
|
||||
return binaryN(n, nBits);
|
||||
};
|
||||
|
||||
@@ -348,7 +348,7 @@ export const binaryN = (n, nBits = 16) => {
|
||||
* .partials(binaryL(irand(4096).add(1)))
|
||||
*/
|
||||
export const binaryL = (n) => {
|
||||
const nBits = reify(n).log2(0).floor().add(1);
|
||||
const nBits = reify(n).log2().floor().add(1);
|
||||
return binaryNL(n, nBits);
|
||||
};
|
||||
|
||||
@@ -389,7 +389,9 @@ export const randL = (n) => {
|
||||
export const randrun = (n) => {
|
||||
return signal((t, controls) => {
|
||||
// Without adding 0.5, the first cycle is always 0,1,2,3,...
|
||||
const rands = getRandsAtTime(t.floor().add(0.5), n, controls.randSeed);
|
||||
let rands = getRandsAtTime(t.floor().add(0.5), n, controls.randSeed);
|
||||
// Support n = 1
|
||||
if (!Array.isArray(rands)) rands = [rands];
|
||||
const nums = rands
|
||||
.map((n, i) => [n, i])
|
||||
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
|
||||
|
||||
@@ -9,7 +9,7 @@ import Fraction from 'fraction.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs';
|
||||
import { fastcat, sequence, State, TimeSpan, Hap } from '../index.mjs';
|
||||
import { fastcat, sequence, State, TimeSpan, Hap, note } from '../index.mjs';
|
||||
|
||||
const st = (begin, end) => new State(ts(begin, end));
|
||||
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
|
||||
@@ -59,3 +59,9 @@ describe('perx', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shuffle', () => {
|
||||
it('returns original pattern if input is 1', () => {
|
||||
expect(note('c d e f').sound('piano').shuffle(1).firstCycle()).toEqual(note('c d e f').sound('piano').firstCycle());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -339,8 +339,13 @@ export function uniqsortr(a) {
|
||||
|
||||
export function unicodeToBase64(text) {
|
||||
const utf8Bytes = new TextEncoder().encode(text);
|
||||
const base64String = btoa(String.fromCharCode(...utf8Bytes));
|
||||
return base64String;
|
||||
let binaryString = '';
|
||||
const chunkSize = 0x8000;
|
||||
for (let i = 0; i < utf8Bytes.length; i += chunkSize) {
|
||||
const chunk = utf8Bytes.subarray(i, i + chunkSize);
|
||||
binaryString += String.fromCharCode.apply(null, chunk);
|
||||
}
|
||||
return btoa(binaryString);
|
||||
}
|
||||
|
||||
export function base64ToUnicode(base64String) {
|
||||
|
||||
@@ -78,6 +78,16 @@ export const cleanupDraw = (clearScreen = true, id) => {
|
||||
stopAllAnimations(id);
|
||||
};
|
||||
|
||||
export const cleanupDrawContext = (replID) => {
|
||||
const ctx = getDrawContext();
|
||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
|
||||
// clear the big canvas context, ignore inline widgets
|
||||
Object.keys(animationFrames).forEach(
|
||||
(id) => (!replID || id.startsWith(replID)) && !id.startsWith('_') && stopAnimationFrame(id),
|
||||
);
|
||||
};
|
||||
|
||||
Pattern.prototype.onPaint = function (painter) {
|
||||
return this.withState((state) => {
|
||||
if (!state.controls.painters) {
|
||||
|
||||
@@ -54,11 +54,42 @@ export function pitchwheel({
|
||||
}
|
||||
|
||||
if (edo) {
|
||||
edo = haps.length >= 1 && haps[0].value && haps[0].value.edo ? haps[0].value.edo : edo;
|
||||
root = haps.length >= 1 && haps[0].value && haps[0].value.root ? haps[0].value.root : root;
|
||||
const degreeIndexes =
|
||||
haps.length >= 1 && haps[0].value && haps[0].value.degreeIndexes ? haps[0].value.degreeIndexes : null;
|
||||
const intLabels = haps.length >= 1 && haps[0].value && haps[0].value.intLabels ? haps[0].value.intLabels : null;
|
||||
ctx.font = '20px sans-serif';
|
||||
const label = `${edo} EDO`;
|
||||
// Draw EDO label:
|
||||
ctx.fillText(label, centerX + radius - ctx.measureText(label).width + 15, centerY + radius);
|
||||
Array.from({ length: edo }, (_, i) => {
|
||||
const angle = freq2angle(root * Math.pow(2, i / edo), root);
|
||||
|
||||
const [x, y] = circlePos(centerX, centerY, radius, angle);
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
|
||||
// Draw interval label for degree i when it exists:
|
||||
if (degreeIndexes === null || degreeIndexes.includes(i)) {
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
|
||||
if (intLabels !== null) {
|
||||
const degree = degreeIndexes.indexOf(i);
|
||||
if (intLabels[degree]) {
|
||||
if (angle < 0.32 && angle > 0.125) {
|
||||
ctx.fillText(intLabels[degree], x - 34, y);
|
||||
} else {
|
||||
if (angle < 0.1 && angle > -1.125) {
|
||||
ctx.fillText(intLabels[degree], x - 7, y - 12);
|
||||
} else {
|
||||
ctx.fillText(intLabels[degree], x + 9, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.globalAlpha = 0.15;
|
||||
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
|
||||
}
|
||||
ctx.fill();
|
||||
});
|
||||
ctx.stroke();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
-64
@@ -23,6 +23,8 @@ import { noteToMidi, getControlName } from '@strudel/core';
|
||||
import { Note } from 'webmidi';
|
||||
import { getAudioContext } from '@strudel/webaudio';
|
||||
import { scheduleAtTime } from '../superdough/helpers.mjs';
|
||||
import { getMidiDeviceNamesString, getDevice } from './util.mjs';
|
||||
import { MidiInput } from './input.mjs';
|
||||
|
||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||
export const { WebMidi } = _WebMidi;
|
||||
@@ -31,10 +33,6 @@ function supportsMidi() {
|
||||
return typeof navigator.requestMIDIAccess === 'function';
|
||||
}
|
||||
|
||||
function getMidiDeviceNamesString(devices) {
|
||||
return devices.map((o) => `'${o.name}'`).join(' | ');
|
||||
}
|
||||
|
||||
export function enableWebMidi(options = {}) {
|
||||
const { onReady, onConnected, onDisconnected, onEnabled } = options;
|
||||
if (WebMidi.enabled) {
|
||||
@@ -72,29 +70,6 @@ export function enableWebMidi(options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function getDevice(indexOrName, devices) {
|
||||
if (!devices.length) {
|
||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||
}
|
||||
if (typeof indexOrName === 'number') {
|
||||
return devices[indexOrName];
|
||||
}
|
||||
const byName = (name) => devices.find((output) => output.name.includes(name));
|
||||
if (typeof indexOrName === 'string') {
|
||||
return byName(indexOrName);
|
||||
}
|
||||
// attempt to default to first IAC device if none is specified
|
||||
const IACOutput = byName('IAC');
|
||||
const device = IACOutput ?? devices[0];
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`🔌 MIDI device '${device ? device : ''}' not found. Use one of ${getMidiDeviceNamesString(devices)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return IACOutput ?? devices[0];
|
||||
}
|
||||
|
||||
// send start/stop messages to outputs when repl starts/stops
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('message', (e) => {
|
||||
@@ -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)) {
|
||||
throw new Error(
|
||||
`[midi] Midi input cannot be a pattern. Make sure to pass device name with single quotes. Example: midin('${
|
||||
@@ -505,24 +480,31 @@ async function _initialize(input) {
|
||||
}')`,
|
||||
);
|
||||
}
|
||||
|
||||
const initial = await enableWebMidi(); // only returns on first init
|
||||
const device = getDevice(input, WebMidi.inputs);
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`[midi] Midi device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const instance = midiInputs[input] || new MidiInput(input);
|
||||
midiInputs[input] = instance;
|
||||
|
||||
if (initial) {
|
||||
const device = instance.initialDevice;
|
||||
|
||||
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
|
||||
logger(
|
||||
`[midi] Midi enabled! Using "${device.name}". ${
|
||||
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
|
||||
}`,
|
||||
device
|
||||
? `[midi] Midi enabled! Using "${device.name}". ${
|
||||
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
|
||||
}`
|
||||
: `[midi] Midi enabled! Waiting for device "${input}"... Currently connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
|
||||
);
|
||||
}
|
||||
return device;
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
// MIDI input wrappers, by specified input string/index
|
||||
const midiInputs = {};
|
||||
|
||||
/**
|
||||
* MIDI input: Opens a MIDI input port to receive MIDI control change messages.
|
||||
*
|
||||
@@ -543,31 +525,10 @@ async function _initialize(input) {
|
||||
* note("c a f e").s("saw")
|
||||
* .when(cc(0).gt(0), x => x.postgain(0))
|
||||
*/
|
||||
let listeners = {};
|
||||
const refs = {};
|
||||
const refsByChan = {};
|
||||
export async function midin(input) {
|
||||
const device = await _initialize(input);
|
||||
refs[input] ??= {};
|
||||
refsByChan[input] ??= {};
|
||||
const cc = (cc, chan) => {
|
||||
if (chan !== undefined) {
|
||||
return ref(() => refsByChan[input][cc]?.[chan] || 0);
|
||||
}
|
||||
return ref(() => refs[input][cc] || 0);
|
||||
};
|
||||
const instance = await _initializeInput(input);
|
||||
|
||||
listeners[input] && device.removeListener('midimessage', listeners[input]);
|
||||
listeners[input] = (e) => {
|
||||
const [ccNum, v] = e.dataBytes;
|
||||
const chan = e.message.channel;
|
||||
const scaled = v / 127;
|
||||
refsByChan[input][ccNum] ??= {};
|
||||
refsByChan[input][ccNum][chan] = scaled;
|
||||
refs[input][ccNum] = scaled;
|
||||
};
|
||||
device.addListener('midimessage', listeners[input]);
|
||||
return cc;
|
||||
return instance.createCC.bind(instance);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -576,6 +537,9 @@ export async function midin(input) {
|
||||
* The note length is fixed as Superdough is not currently set up for undetermined
|
||||
* note durations
|
||||
*
|
||||
* The 'midichan' control value contains the number of the channel the note is coming from
|
||||
* so it could be filtered or manipulated further in the chain.
|
||||
*
|
||||
* @name midikeys
|
||||
* @tags external_io, midi
|
||||
* @param {string | number} input MIDI device name or index defaulting to 0
|
||||
@@ -591,6 +555,10 @@ export async function midin(input) {
|
||||
* .s("saw")
|
||||
* .add(note(rand.mul(0.3)))
|
||||
* .lpf(1000).lpe(2).room(0.5)
|
||||
* @example
|
||||
* // discard all notes not coming out from midi channel 2
|
||||
* const kb = await midikeys('Arturia KeyStep 32')
|
||||
* kb().filterValues(v=>v.midichan==2).s("tri")
|
||||
*/
|
||||
const kHaps = {};
|
||||
const kListeners = {};
|
||||
@@ -622,7 +590,16 @@ function _triggerKeyboard(input, cps, now, latencyCycles) {
|
||||
return true;
|
||||
}
|
||||
export async function midikeys(input) {
|
||||
const device = await _initialize(input);
|
||||
const instance = await _initializeInput(input);
|
||||
|
||||
// TODO: support unpluggable device usage
|
||||
const device = instance.initialDevice;
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`[midi] Midi device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!kHaps[input]) {
|
||||
kHaps[input] = [];
|
||||
}
|
||||
@@ -663,7 +640,7 @@ export async function midikeys(input) {
|
||||
*/
|
||||
return;
|
||||
} else {
|
||||
value = { ...value, note: Math.round(note), velocity: velocity / 127 };
|
||||
value = { ...value, note: Math.round(note), velocity: velocity / 127, midichan: message.channel };
|
||||
}
|
||||
kHaps[input].push(new Hap(span, span, value, {}));
|
||||
if (!noteoff && triggerAvailable) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+10
-2
@@ -4,7 +4,7 @@ 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/>.
|
||||
*/
|
||||
|
||||
import { Pattern, isPattern } from '@strudel/core';
|
||||
import { Pattern, isPattern, createParams } from '@strudel/core';
|
||||
import Paho from 'paho-mqtt';
|
||||
|
||||
const connections = {};
|
||||
@@ -61,7 +61,6 @@ Pattern.prototype.mqtt = function (
|
||||
const props = {
|
||||
onSuccess: onConnect,
|
||||
onFailure: onFailure,
|
||||
useSSL: true,
|
||||
};
|
||||
|
||||
if (username) {
|
||||
@@ -119,3 +118,12 @@ Pattern.prototype.mqtt = function (
|
||||
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
|
||||
});
|
||||
};
|
||||
|
||||
// This adds the 'move' and 'motor' commands to strudel
|
||||
export const { move, motor } = createParams('move', 'motor');
|
||||
window.move = move;
|
||||
window.motor = motor;
|
||||
// This adds the 'robot' command
|
||||
Pattern.prototype.robot = function (robot_id, address = 'ws://192.168.8.248:9001/mqtt') {
|
||||
return this.mqtt(undefined, undefined, '/move/' + robot_id, address);
|
||||
};
|
||||
|
||||
@@ -42,7 +42,7 @@ export function parseControlsFromHap(hap, cps) {
|
||||
if (isNote(controls.note)) {
|
||||
controls.midinote = noteToMidi(controls.note, controls.octave || 3);
|
||||
} else {
|
||||
controls.note = parseNumeral(controls.note);
|
||||
controls.midinote = parseNumeral(controls.note);
|
||||
}
|
||||
}
|
||||
controls.bank && (controls.s = controls.bank + controls.s);
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"@strudel/codemirror": "workspace:*",
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/draw": "workspace:*",
|
||||
"@strudel/edo": "workspace:*",
|
||||
"@strudel/hydra": "workspace:*",
|
||||
"@strudel/midi": "workspace:*",
|
||||
"@strudel/mini": "workspace:*",
|
||||
|
||||
@@ -8,6 +8,7 @@ export async function prebake() {
|
||||
core,
|
||||
import('@strudel/draw'),
|
||||
import('@strudel/mini'),
|
||||
import('@strudel/edo'),
|
||||
import('@strudel/tonal'),
|
||||
import('@strudel/webaudio'),
|
||||
import('@strudel/codemirror'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
||||
import { registerSound, registerWaveTable } from './index.mjs';
|
||||
import { registerSound, registerWaveTable, soundMap } from './index.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import {
|
||||
getADSRValues,
|
||||
@@ -30,6 +30,29 @@ function humanFileSize(bytes, si) {
|
||||
return bytes.toFixed(1) + ' ' + units[u];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the duration, in seconds, of the given sample.
|
||||
* Has optional param `n` (for instance, the `2` in `s("casio:2")`)
|
||||
*
|
||||
* Note: `must` be called with await, otherwise you'll get a pending Promise object.
|
||||
*
|
||||
* @name getDuration,getDur
|
||||
* @tag samples
|
||||
* @param {string} sampleName
|
||||
* @param {number} (optional) n
|
||||
*
|
||||
* @example
|
||||
* // Set a patterns cycle length to exactly the length of the sample
|
||||
* samples('github:tidalcycles/dirt-samples')
|
||||
* let k = await getDuration('sax')
|
||||
* s("sax").cps(1/k)
|
||||
*/
|
||||
export const getDuration = (s, n = 0) => {
|
||||
return getSampleBufferSource({ s, n }, soundMap.get(s)[s].data.samples).then((x) => x.bufferDuration);
|
||||
};
|
||||
|
||||
export const getDur = getDuration;
|
||||
|
||||
export function getSampleInfo(hapValue, bank) {
|
||||
const { speed = 1.0 } = hapValue;
|
||||
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import { map } from 'nanostores';
|
||||
import { logger } from './logger.mjs';
|
||||
import { connectLFO, connectEnvelope, connectBusModulator } from './modulators.mjs';
|
||||
import { loadBuffer } from './sampler.mjs';
|
||||
import { getSampleBufferSource, loadBuffer } from './sampler.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
||||
import { resetSeenKeys } from './wavetable.mjs';
|
||||
|
||||
@@ -485,8 +485,10 @@ export function waveformN(partials, phases, type) {
|
||||
if (phase !== 0) {
|
||||
const c = Math.cos(PI2 * phase);
|
||||
const s = Math.sin(PI2 * phase);
|
||||
R = c * R - s * I;
|
||||
I = s * R + c * I;
|
||||
const R0 = R;
|
||||
const I0 = I;
|
||||
R = c * R0 - s * I0;
|
||||
I = s * R0 + c * I0;
|
||||
}
|
||||
real[n + 1] = R;
|
||||
imag[n + 1] = I;
|
||||
|
||||
@@ -214,7 +214,7 @@ class CoarseProcessor extends AudioWorkletProcessor {
|
||||
coarse = Math.max(1, coarse);
|
||||
for (let n = 0; n < blockSize; n++) {
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
output[i][n] = n % coarse === 0 ? input[i][n] : output[i][n - 1];
|
||||
output[i][n] = n % coarse < 1 ? input[i][n] : output[i][n - 1];
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
# @strudel/transpiler
|
||||
|
||||
This package contains a JS code transpiler with the following features:
|
||||
This package contains a JS code transpiler with a plugin system that can be used to transform the code.
|
||||
|
||||
A plugin can be registered via a `registerTranspilerPlugin` call :
|
||||
|
||||
```js
|
||||
registerTranspilerPlugin({
|
||||
walk: (context) => ({
|
||||
enter: function(node, parent, prop, index) {},
|
||||
leave: function(node, parent, prop, index) {}
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
where the enter/leave functions (both optional) follow the `estree-walker` walk API.
|
||||
|
||||
4 plugins are currently hosted inside `@strudel/transpiler`
|
||||
|
||||
- mini: add locations of mini notation strings (double quoted or backticked) for highlighting
|
||||
- widgets: add handling of sliders & draw widgets
|
||||
- sample: make it possible to call a sample without await
|
||||
- kabelsalat: transform the code to handle the `K(..)` kabelsalat notation
|
||||
|
||||
it also
|
||||
|
||||
- add locations of mini notation strings (double quoted or backticked) for highlighting
|
||||
- converts pseudo note variables to note strings
|
||||
- adds return statement to the last expression
|
||||
- handles label capturing for block-based eval
|
||||
|
||||
## Install
|
||||
|
||||
|
||||
@@ -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 { transpiler } from './transpiler.mjs';
|
||||
|
||||
export * from './transpiler.mjs';
|
||||
|
||||
export const evaluate = (code) => _evaluate(code, transpiler);
|
||||
import './plugin-kabelsalat.mjs';
|
||||
import './plugin-mini.mjs';
|
||||
import './plugin-sample.mjs';
|
||||
import './plugin-widgets.mjs';
|
||||
|
||||
export { registerWidgetType, getWidgetID } from './plugin-widgets.mjs';
|
||||
export const evaluate = (code, transpilerOptions) => _evaluate(code, transpiler, transpilerOptions);
|
||||
|
||||
@@ -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 { transpiler } from '../transpiler.mjs';
|
||||
import { transpiler } from '../index.mjs';
|
||||
|
||||
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 escodegen from 'escodegen';
|
||||
import { walk } from 'estree-walker';
|
||||
|
||||
let widgetMethods = [];
|
||||
export function registerWidgetType(type) {
|
||||
widgetMethods.push(type);
|
||||
}
|
||||
|
||||
let languages = new Map();
|
||||
// config = { getLocations: (code: string, offset?: number) => number[][] }
|
||||
// see mondough.mjs for example use
|
||||
@@ -17,9 +16,31 @@ let languages = new Map();
|
||||
export function registerLanguage(type, config) {
|
||||
languages.set(type, config);
|
||||
}
|
||||
export function getLanguages() {
|
||||
return languages;
|
||||
}
|
||||
|
||||
const plugins = [];
|
||||
|
||||
export function registerTranspilerPlugin(plugin) {
|
||||
plugins.push(plugin);
|
||||
}
|
||||
export function getPlugins() {
|
||||
return plugins.flat(Infinity);
|
||||
}
|
||||
|
||||
export function transpiler(input, options = {}) {
|
||||
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
|
||||
options = {
|
||||
wrapAsync: false,
|
||||
addReturn: true,
|
||||
emitMiniLocations: true,
|
||||
emitWidgets: true,
|
||||
blockBased: false,
|
||||
range: [],
|
||||
...options,
|
||||
};
|
||||
|
||||
const { wrapAsync, addReturn, emitMiniLocations, emitWidgets, blockBased, range } = options;
|
||||
|
||||
const comments = [];
|
||||
let ast = parse(input, {
|
||||
@@ -29,169 +50,103 @@ export function transpiler(input, options = {}) {
|
||||
onComment: comments,
|
||||
});
|
||||
|
||||
const miniDisableRanges = findMiniDisableRanges(comments, input.length);
|
||||
let miniLocations = [];
|
||||
const collectMiniLocations = (value, node) => {
|
||||
const minilang = languages.get('minilang');
|
||||
if (minilang) {
|
||||
const code = `[${value}]`;
|
||||
const locs = minilang.getLocations(code, node.start);
|
||||
miniLocations = miniLocations.concat(locs);
|
||||
} else {
|
||||
const leafLocs = getLeafLocations(`"${value}"`, node.start, input);
|
||||
miniLocations = miniLocations.concat(leafLocs);
|
||||
}
|
||||
};
|
||||
let widgets = [];
|
||||
let miniDisableRanges = findMiniDisableRanges(comments, input.length);
|
||||
|
||||
// Position offset for block-based evaluation
|
||||
let nodeOffset = range && range.length > 0 ? range[0] : 0;
|
||||
|
||||
// Track declarations to add to strudelScope for block-based eval
|
||||
let scopeDeclarations = [];
|
||||
|
||||
let labels = [];
|
||||
|
||||
const context = { options, input, nodeOffset, miniDisableRanges, labels };
|
||||
const plugins = getPlugins().map((plugin) => plugin.walk?.(context));
|
||||
|
||||
walk(ast, {
|
||||
enter(node, parent /* , prop, index */) {
|
||||
if (isLanguageLiteral(node)) {
|
||||
const { name } = node.tag;
|
||||
const language = languages.get(name);
|
||||
const code = node.quasi.quasis[0].value.raw;
|
||||
const offset = node.quasi.start + 1;
|
||||
if (emitMiniLocations) {
|
||||
const locs = language.getLocations(code, offset);
|
||||
miniLocations = miniLocations.concat(locs);
|
||||
enter(node, parent, prop, index) {
|
||||
// Apply position offset for block-based evaluation
|
||||
if (blockBased && node.start !== undefined) {
|
||||
node.start = node.start + nodeOffset;
|
||||
node.end = node.end + nodeOffset;
|
||||
}
|
||||
// Collect variable and function declarations for strudelScope (block-based eval)
|
||||
if (blockBased && parent?.type === 'Program') {
|
||||
if (node.type === 'VariableDeclaration') {
|
||||
for (const declarator of node.declarations) {
|
||||
if (declarator.id?.name) {
|
||||
scopeDeclarations.push(declarator.id.name);
|
||||
}
|
||||
}
|
||||
} else if (node.type === 'FunctionDeclaration' && node.id?.name) {
|
||||
scopeDeclarations.push(node.id.name);
|
||||
}
|
||||
this.skip();
|
||||
return this.replace(languageWithLocation(name, code, offset));
|
||||
}
|
||||
if (isTemplateLiteral(node, 'tidal')) {
|
||||
const raw = node.quasi.quasis[0].value.raw;
|
||||
const offset = node.quasi.start + 1;
|
||||
if (emitMiniLocations) {
|
||||
const stringLocs = collectHaskellMiniLocations(raw, offset);
|
||||
miniLocations = miniLocations.concat(stringLocs);
|
||||
}
|
||||
this.skip();
|
||||
return this.replace(tidalWithLocation(raw, offset));
|
||||
}
|
||||
if (isBackTickString(node, parent)) {
|
||||
if (isMiniDisabled(node.start, miniDisableRanges)) {
|
||||
return;
|
||||
}
|
||||
const { quasis } = node;
|
||||
const { raw } = quasis[0].value;
|
||||
this.skip();
|
||||
emitMiniLocations && collectMiniLocations(raw, node);
|
||||
return this.replace(miniWithLocation(raw, node));
|
||||
}
|
||||
if (isStringWithDoubleQuotes(node)) {
|
||||
if (isMiniDisabled(node.start, miniDisableRanges)) {
|
||||
return;
|
||||
}
|
||||
const { value } = node;
|
||||
this.skip();
|
||||
emitMiniLocations && collectMiniLocations(value, node);
|
||||
return this.replace(miniWithLocation(value, node));
|
||||
}
|
||||
if (isSliderFunction(node)) {
|
||||
emitWidgets &&
|
||||
widgets.push({
|
||||
from: node.arguments[0].start,
|
||||
to: node.arguments[0].end,
|
||||
value: node.arguments[0].raw, // don't use value!
|
||||
min: node.arguments[1]?.value ?? 0,
|
||||
max: node.arguments[2]?.value ?? 1,
|
||||
step: node.arguments[3]?.value,
|
||||
type: 'slider',
|
||||
});
|
||||
return this.replace(sliderWithLocation(node));
|
||||
}
|
||||
if (isWidgetMethod(node)) {
|
||||
const type = node.callee.property.name;
|
||||
const index = widgets.filter((w) => w.type === type).length;
|
||||
const widgetConfig = {
|
||||
to: node.end,
|
||||
index,
|
||||
type,
|
||||
id: options.id,
|
||||
};
|
||||
emitWidgets && widgets.push(widgetConfig);
|
||||
return this.replace(widgetWithLocation(node, widgetConfig));
|
||||
}
|
||||
if (isBareSamplesCall(node, parent)) {
|
||||
return this.replace(withAwait(node));
|
||||
|
||||
for (const plugin of plugins) {
|
||||
if (!plugin?.enter?.call(this, node, parent, prop, index)) continue;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLabelStatement(node)) {
|
||||
// Collect label info for block-based evaluation
|
||||
// Store positions WITHOUT offset so repl can slice the transpiler output correctly
|
||||
if (blockBased) {
|
||||
labels.push({
|
||||
name: node.label.name,
|
||||
index: node.start - nodeOffset,
|
||||
end: node.label.end - nodeOffset,
|
||||
fullMatch: input.slice(node.start - nodeOffset, node.label.end - nodeOffset),
|
||||
activeVisualizer: findVisualizerInSubtree(node.body),
|
||||
});
|
||||
}
|
||||
return this.replace(labelToP(node));
|
||||
}
|
||||
// Detect all() calls as special labels for block management
|
||||
// Store positions WITHOUT offset so repl can slice the transpiler output correctly
|
||||
if (blockBased && isAllCall(node)) {
|
||||
labels.push({
|
||||
name: 'all',
|
||||
index: node.start - nodeOffset,
|
||||
end: node.end - nodeOffset,
|
||||
fullMatch: input.slice(node.start - nodeOffset, node.end - nodeOffset),
|
||||
activeVisualizer: node.arguments[0] ? findVisualizerInSubtree(node.arguments[0]) : null,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
leave(node, parent, prop, index) {
|
||||
if (!isKabelCall(node)) return;
|
||||
|
||||
let [expr, ...rest] = node.arguments;
|
||||
if (!expr) throw new Error('K(...) requires an expression');
|
||||
|
||||
if (shouldCallKabelExpression(expr)) {
|
||||
expr = {
|
||||
type: 'CallExpression',
|
||||
callee: expr,
|
||||
arguments: [],
|
||||
optional: false,
|
||||
};
|
||||
for (const plugin of plugins) {
|
||||
if (!plugin?.leave?.call(this, node, parent, prop, index)) continue;
|
||||
return;
|
||||
}
|
||||
|
||||
const { template, patternExprs } = extractPatternPlaceholders(expr);
|
||||
if (patternExprs.length) {
|
||||
const workletArgs = [{ type: 'Literal', value: template }, ...patternExprs, ...rest];
|
||||
|
||||
let callee = node.callee;
|
||||
if (callee.type === 'ChainExpression') callee = callee.expression;
|
||||
if (callee.type === 'MemberExpression') {
|
||||
return this.replace({
|
||||
type: 'CallExpression',
|
||||
callee: workletMemberAst(callee.object),
|
||||
arguments: workletArgs,
|
||||
optional: false,
|
||||
});
|
||||
}
|
||||
return this.replace({
|
||||
type: 'CallExpression',
|
||||
callee: { type: 'Identifier', name: 'worklet' },
|
||||
arguments: workletArgs,
|
||||
optional: false,
|
||||
});
|
||||
}
|
||||
|
||||
const kabelSrc = genExprSource(expr);
|
||||
const workletArgs = [{ type: 'Literal', value: kabelSrc }, ...rest];
|
||||
|
||||
let callee = node.callee;
|
||||
if (callee.type === 'ChainExpression') callee = callee.expression;
|
||||
if (callee.type === 'MemberExpression') {
|
||||
return this.replace({
|
||||
type: 'CallExpression',
|
||||
callee: workletMemberAst(callee.object),
|
||||
arguments: workletArgs,
|
||||
optional: false,
|
||||
});
|
||||
}
|
||||
return this.replace({
|
||||
type: 'CallExpression',
|
||||
callee: { type: 'Identifier', name: 'worklet' },
|
||||
arguments: workletArgs,
|
||||
optional: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
let { body } = ast;
|
||||
|
||||
const silenceExpression = {
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'Identifier',
|
||||
name: 'silence',
|
||||
},
|
||||
};
|
||||
|
||||
if (!body.length) {
|
||||
console.warn('empty body -> fallback to silence');
|
||||
body.push({
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'Identifier',
|
||||
name: 'silence',
|
||||
},
|
||||
});
|
||||
body.push(silenceExpression);
|
||||
} else if (!body?.[body.length - 1]?.expression) {
|
||||
throw new Error('unexpected ast format without body expression');
|
||||
// Last statement is not an expression (e.g., VariableDeclaration, FunctionDeclaration)
|
||||
body.push(silenceExpression);
|
||||
}
|
||||
|
||||
// For block-based eval, add scope assignments before the return statement
|
||||
// This allows variables/functions defined in one block to be used in other blocks
|
||||
if (blockBased && scopeDeclarations.length > 0) {
|
||||
const scopeAssignments = scopeDeclarations.flatMap((name) => createScopeAssignment(name));
|
||||
// Insert scope assignments before the last statement (which will become the return)
|
||||
body.splice(body.length - 1, 0, ...scopeAssignments);
|
||||
}
|
||||
|
||||
// add return to last statement
|
||||
@@ -209,256 +164,15 @@ export function transpiler(input, options = {}) {
|
||||
if (!emitMiniLocations) {
|
||||
return { output };
|
||||
}
|
||||
return { output, miniLocations, widgets };
|
||||
|
||||
let pluginContext;
|
||||
({ options, input, miniDisableRanges, nodeOffset, ...pluginContext } = context);
|
||||
|
||||
return { output, ...pluginContext };
|
||||
}
|
||||
|
||||
function isKabelCall(node) {
|
||||
if (node.type !== 'CallExpression') return false;
|
||||
let callee = node.callee;
|
||||
if (callee.type === 'ChainExpression') callee = callee.expression;
|
||||
if (callee.type === 'MemberExpression') return !callee.computed && callee.property?.name === 'K';
|
||||
return callee.type === 'Identifier' && callee.name === 'K';
|
||||
}
|
||||
|
||||
function shouldCallKabelExpression(expr) {
|
||||
if (expr.type !== 'ArrowFunctionExpression' && expr.type !== 'FunctionExpression') {
|
||||
return false;
|
||||
}
|
||||
if (expr.params.length) {
|
||||
return false;
|
||||
}
|
||||
return expr.body?.type === 'BlockStatement';
|
||||
}
|
||||
|
||||
function genExprSource(expr) {
|
||||
return escodegen.generate(expr, { format: { semicolons: false } });
|
||||
}
|
||||
|
||||
function extractPatternPlaceholders(expr) {
|
||||
const templateExpr = cloneNode(expr);
|
||||
const parents = new Map();
|
||||
const targets = [];
|
||||
|
||||
walk(templateExpr, {
|
||||
enter(node, parent, prop, index) {
|
||||
parents.set(node, { parent, prop, index });
|
||||
const patternExpr = getStrudelPatternExpr(node);
|
||||
if (patternExpr) {
|
||||
targets.push({ node, patternExpr });
|
||||
this.skip();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!targets.length) {
|
||||
return { template: genExprSource(templateExpr), patternExprs: [] };
|
||||
}
|
||||
|
||||
targets.sort((a, b) => getPatternNodeOrder(a.node) - getPatternNodeOrder(b.node));
|
||||
|
||||
const patternExprs = targets.map(({ patternExpr }) => cloneNode(patternExpr));
|
||||
|
||||
let currentExpr = templateExpr;
|
||||
targets.forEach(({ node }, index) => {
|
||||
currentExpr = replaceNode(node, placeholderAst(index), parents, currentExpr);
|
||||
});
|
||||
|
||||
const template = genExprSource(currentExpr);
|
||||
return { template, patternExprs };
|
||||
}
|
||||
|
||||
function getStrudelPatternExpr(node) {
|
||||
if (isStrudelPatternWrap(node)) {
|
||||
const arg = node.arguments?.[0];
|
||||
if (!arg) {
|
||||
throw new Error('S(...) requires an argument');
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
if (isMiniCall(node)) {
|
||||
return node;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isStrudelPatternWrap(node) {
|
||||
if (node.type !== 'CallExpression') {
|
||||
return false;
|
||||
}
|
||||
const callee = node.callee;
|
||||
if (callee.type === 'Identifier') {
|
||||
return callee.name === 'S';
|
||||
}
|
||||
if (callee.type === 'MemberExpression' && !callee.computed) {
|
||||
return callee.property?.name === 'S';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getMinilangName() {
|
||||
const minilang = languages.get('minilang');
|
||||
return minilang?.name || 'm';
|
||||
}
|
||||
|
||||
// Used to identify transpiled `m(...)` calls for proper conversion
|
||||
// to, say, kabelsalat placeholders
|
||||
function isMiniCall(node) {
|
||||
if (node.type !== 'CallExpression') {
|
||||
return false;
|
||||
}
|
||||
const callee = node.callee;
|
||||
if (callee.type !== 'Identifier') {
|
||||
return false;
|
||||
}
|
||||
if (callee.name !== getMinilangName()) {
|
||||
return false;
|
||||
}
|
||||
const firstArg = node.arguments?.[0];
|
||||
return firstArg?.type === 'Literal' && typeof firstArg.value === 'string';
|
||||
}
|
||||
|
||||
// If `start` is available, we use it. If it's already been transpiled
|
||||
// to `m(...)`, use the provided offset
|
||||
function getPatternNodeOrder(node) {
|
||||
if (typeof node.start === 'number') {
|
||||
return node.start;
|
||||
}
|
||||
if (isMiniCall(node)) {
|
||||
const offsetArg = node.arguments?.[1];
|
||||
if (offsetArg?.type === 'Literal' && typeof offsetArg.value === 'number') {
|
||||
return offsetArg.value;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function placeholderAst(index) {
|
||||
return {
|
||||
type: 'MemberExpression',
|
||||
object: { type: 'Identifier', name: 'pat' },
|
||||
property: { type: 'Literal', value: index },
|
||||
computed: true,
|
||||
optional: false,
|
||||
};
|
||||
}
|
||||
|
||||
function replaceNode(node, replacement, parents, currentRoot) {
|
||||
const info = parents.get(node);
|
||||
if (!info || !info.parent) {
|
||||
return replacement;
|
||||
}
|
||||
|
||||
const { parent, prop, index } = info;
|
||||
if (Array.isArray(parent[prop])) {
|
||||
parent[prop][index] = replacement;
|
||||
} else {
|
||||
parent[prop] = replacement;
|
||||
}
|
||||
parents.set(replacement, { parent, prop, index });
|
||||
return currentRoot;
|
||||
}
|
||||
|
||||
function cloneNode(node) {
|
||||
return JSON.parse(JSON.stringify(node));
|
||||
}
|
||||
|
||||
function workletMemberAst(objectExpr) {
|
||||
return {
|
||||
type: 'MemberExpression',
|
||||
object: objectExpr,
|
||||
property: { type: 'Identifier', name: 'worklet' },
|
||||
computed: false,
|
||||
optional: false,
|
||||
};
|
||||
}
|
||||
|
||||
function isStringWithDoubleQuotes(node, locations, code) {
|
||||
if (node.type !== 'Literal') {
|
||||
return false;
|
||||
}
|
||||
return node.raw[0] === '"';
|
||||
}
|
||||
|
||||
function isBackTickString(node, parent) {
|
||||
return node.type === 'TemplateLiteral' && parent.type !== 'TaggedTemplateExpression';
|
||||
}
|
||||
|
||||
function miniWithLocation(value, node) {
|
||||
const { start: fromOffset } = node;
|
||||
|
||||
const minilang = languages.get('minilang');
|
||||
let name = 'm';
|
||||
if (minilang && minilang.name) {
|
||||
name = minilang.name; // name is expected to be exported from the package of the minilang
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
name,
|
||||
},
|
||||
arguments: [
|
||||
{ type: 'Literal', value },
|
||||
{ type: 'Literal', value: fromOffset },
|
||||
],
|
||||
optional: false,
|
||||
};
|
||||
}
|
||||
|
||||
// these functions are connected to @strudel/codemirror -> slider.mjs
|
||||
// maybe someday there will be pluggable transpiler functions, then move this there
|
||||
function isSliderFunction(node) {
|
||||
return node.type === 'CallExpression' && node.callee.name === 'slider';
|
||||
}
|
||||
|
||||
function isWidgetMethod(node) {
|
||||
return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name);
|
||||
}
|
||||
|
||||
function sliderWithLocation(node) {
|
||||
const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id
|
||||
// add loc as identifier to first argument
|
||||
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
||||
node.arguments.unshift({
|
||||
type: 'Literal',
|
||||
value: id,
|
||||
raw: id,
|
||||
});
|
||||
node.callee.name = 'sliderWithID';
|
||||
return node;
|
||||
}
|
||||
|
||||
export function getWidgetID(widgetConfig) {
|
||||
// the widget id is used as id for the dom element + as key for eventual resources
|
||||
// for example, for each scope widget, a new analyser + buffer (large) is created
|
||||
// that means, if we use the index index of line position as id, less garbage is generated
|
||||
// return `widget_${widgetConfig.to}`; // more gargabe
|
||||
//return `widget_${widgetConfig.index}_${widgetConfig.to}`; // also more garbage
|
||||
return `${widgetConfig.id || ''}_widget_${widgetConfig.type}_${widgetConfig.index}`; // less garbage
|
||||
}
|
||||
|
||||
function widgetWithLocation(node, widgetConfig) {
|
||||
const id = getWidgetID(widgetConfig);
|
||||
// add loc as identifier to first argument
|
||||
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
||||
node.arguments.unshift({
|
||||
type: 'Literal',
|
||||
value: id,
|
||||
raw: id,
|
||||
});
|
||||
return node;
|
||||
}
|
||||
|
||||
function isBareSamplesCall(node, parent) {
|
||||
return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression';
|
||||
}
|
||||
|
||||
function withAwait(node) {
|
||||
return {
|
||||
type: 'AwaitExpression',
|
||||
argument: node,
|
||||
};
|
||||
function isAllCall(node) {
|
||||
return node.type === 'CallExpression' && node.callee.name === 'all';
|
||||
}
|
||||
|
||||
function isLabelStatement(node) {
|
||||
@@ -491,67 +205,126 @@ function labelToP(node) {
|
||||
};
|
||||
}
|
||||
|
||||
function isLanguageLiteral(node) {
|
||||
return node.type === 'TaggedTemplateExpression' && languages.has(node.tag.name);
|
||||
// List of non-inline widgets that need cleanup
|
||||
// These are Pattern.prototype methods that create persistent visualizations
|
||||
// (should be repalced by a function call producing an actual list of registered widgets)
|
||||
const nonInlineWidgets = ['punchcard', 'spiral', 'scope', 'pitchwheel', 'spectrum', 'pianoroll', 'wordfall'];
|
||||
|
||||
function isVisualizerCall(node) {
|
||||
if (
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
nonInlineWidgets.includes(node.callee.property?.name)
|
||||
) {
|
||||
return node.callee.property.name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// tidal highlighting
|
||||
// this feels kind of stupid, when we also know the location inside the string op (tidal.mjs)
|
||||
// but maybe it's the only way
|
||||
function findVisualizerInSubtree(node) {
|
||||
if (!node || typeof node !== 'object') return null;
|
||||
|
||||
function isTemplateLiteral(node, value) {
|
||||
return node.type === 'TaggedTemplateExpression' && node.tag.name === value;
|
||||
}
|
||||
// Check if this node is a visualizer call
|
||||
const viz = isVisualizerCall(node);
|
||||
if (viz) return viz;
|
||||
|
||||
function collectHaskellMiniLocations(haskellCode, offset) {
|
||||
return haskellCode
|
||||
.split('')
|
||||
.reduce((acc, char, i) => {
|
||||
if (char !== '"') {
|
||||
return acc;
|
||||
// Recursively search children
|
||||
for (const key of Object.keys(node)) {
|
||||
if (key === 'parent') continue; // Skip parent references to avoid cycles
|
||||
const child = node[key];
|
||||
if (Array.isArray(child)) {
|
||||
for (const item of child) {
|
||||
const found = findVisualizerInSubtree(item);
|
||||
if (found) return found;
|
||||
}
|
||||
if (!acc.length || acc[acc.length - 1].length > 1) {
|
||||
acc.push([i + 1]);
|
||||
} else {
|
||||
acc[acc.length - 1].push(i);
|
||||
}
|
||||
return acc;
|
||||
}, [])
|
||||
.map(([start, end]) => {
|
||||
const miniString = haskellCode.slice(start, end);
|
||||
return getLeafLocations(`"${miniString}"`, offset + start - 1);
|
||||
})
|
||||
.flat();
|
||||
} else if (child && typeof child === 'object' && child.type) {
|
||||
const found = findVisualizerInSubtree(child);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function tidalWithLocation(value, offset) {
|
||||
return {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
name: 'tidal',
|
||||
// Creates AST nodes for: userDefinedKeys.add('name'); strudelScope.name = name; globalThis.name = name;
|
||||
// Used in block-based evaluation to persist variables/functions across blocks
|
||||
// We add to both strudelScope (for internal lookups) and globalThis (for direct access)
|
||||
// We also track the key in userDefinedKeys so clearScope() can remove it later
|
||||
function createScopeAssignment(name) {
|
||||
return [
|
||||
// userDefinedKeys.add('name');
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'MemberExpression',
|
||||
object: {
|
||||
type: 'Identifier',
|
||||
name: 'userDefinedKeys',
|
||||
},
|
||||
property: {
|
||||
type: 'Identifier',
|
||||
name: 'add',
|
||||
},
|
||||
computed: false,
|
||||
},
|
||||
arguments: [
|
||||
{
|
||||
type: 'Literal',
|
||||
value: name,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
arguments: [
|
||||
{ type: 'Literal', value },
|
||||
{ type: 'Literal', value: offset },
|
||||
],
|
||||
optional: false,
|
||||
};
|
||||
}
|
||||
|
||||
function languageWithLocation(name, value, offset) {
|
||||
return {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
name: name,
|
||||
// strudelScope.name = name;
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'AssignmentExpression',
|
||||
operator: '=',
|
||||
left: {
|
||||
type: 'MemberExpression',
|
||||
object: {
|
||||
type: 'Identifier',
|
||||
name: 'strudelScope',
|
||||
},
|
||||
property: {
|
||||
type: 'Identifier',
|
||||
name: name,
|
||||
},
|
||||
computed: false,
|
||||
},
|
||||
right: {
|
||||
type: 'Identifier',
|
||||
name: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
arguments: [
|
||||
{ type: 'Literal', value },
|
||||
{ type: 'Literal', value: offset },
|
||||
],
|
||||
optional: false,
|
||||
};
|
||||
// globalThis.name = name;
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'AssignmentExpression',
|
||||
operator: '=',
|
||||
left: {
|
||||
type: 'MemberExpression',
|
||||
object: {
|
||||
type: 'Identifier',
|
||||
name: 'globalThis',
|
||||
},
|
||||
property: {
|
||||
type: 'Identifier',
|
||||
name: name,
|
||||
},
|
||||
computed: false,
|
||||
},
|
||||
right: {
|
||||
type: 'Identifier',
|
||||
name: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function findMiniDisableRanges(comments, codeEnd) {
|
||||
@@ -573,12 +346,3 @@ function findMiniDisableRanges(comments, codeEnd) {
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
function isMiniDisabled(offset, miniDisableRanges) {
|
||||
for (const [start, end] of miniDisableRanges) {
|
||||
if (offset >= start && offset < end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/edo": "workspace:*",
|
||||
"@strudel/mini": "workspace:*",
|
||||
"@strudel/tonal": "workspace:*",
|
||||
"@strudel/transpiler": "workspace:*",
|
||||
|
||||
@@ -3,6 +3,7 @@ export * from '@strudel/webaudio';
|
||||
//export * from '@strudel/soundfonts';
|
||||
export * from '@strudel/transpiler';
|
||||
export * from '@strudel/mini';
|
||||
export * from '@strudel/edo';
|
||||
export * from '@strudel/tonal';
|
||||
export * from '@strudel/webaudio';
|
||||
import { Pattern, evalScope, setTime } from '@strudel/core';
|
||||
@@ -17,6 +18,7 @@ export async function defaultPrebake() {
|
||||
evalScope,
|
||||
import('@strudel/core'),
|
||||
import('@strudel/mini'),
|
||||
import('@strudel/edo'),
|
||||
import('@strudel/tonal'),
|
||||
import('@strudel/webaudio'),
|
||||
{ hush, evaluate },
|
||||
|
||||
+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 { register } from '@strudel/core';
|
||||
|
||||
/**
|
||||
* Assumes pattern contains numerical scale degrees on the `i` control (see examples below). Accepts a scale name or list of frequencies (see all available names at the link on the reference). Returns a new pattern with all values mapped to a frequency ratio. Similar to `xen`.
|
||||
* @name tune
|
||||
* @returns Pattern
|
||||
* @memberof Pattern
|
||||
* @param {(string | number[] )} scale
|
||||
* @example
|
||||
* i("0 1 2 3 4 5").tune("hexany15").mul("220").freq()
|
||||
* @example
|
||||
* // You can set your root to be a
|
||||
* // particular note with getFreq:
|
||||
* i("4 8 9 10 - - 5 7 9 11 - -").tune("tranh3")
|
||||
* .mul(getFreq('c3'))
|
||||
* .freq().clip(.5).room(1)
|
||||
* @example
|
||||
* // You can also give tune a list of
|
||||
* // frequencies to use as the scale:
|
||||
* i("0 1 2 3 4").tune([
|
||||
* 261.6255653006,
|
||||
* 302.72962012827,
|
||||
* 350.29154279212,
|
||||
* 405.32593044476,
|
||||
* 469.00678383895,
|
||||
* 523.2511306012
|
||||
* ]).mul(220).freq();
|
||||
*
|
||||
* @tags tonal
|
||||
*/
|
||||
|
||||
// Tune.scale seems to be in ratio format
|
||||
export const tune = register('tune', (scale, pat) => {
|
||||
const tune = new Tune();
|
||||
if (!tune.isValidScale(scale)) {
|
||||
throw new Error('not a valid tune.js scale name: "' + scale + '". See http://abbernie.github.io/tune/scales.html');
|
||||
}
|
||||
tune.loadScale(scale);
|
||||
// if the tonic is a frequency, why are we putting in "1"
|
||||
tune.tonicize(1);
|
||||
return pat.withHap((hap) => {
|
||||
return hap.withValue(() => tune.note(hap.value));
|
||||
if (typeof hap.value !== 'object') {
|
||||
throw new Error(`Expected hap to have control 'i' set, but received ${hap.value.i}, try wrapping input in i()`);
|
||||
}
|
||||
// const { i, ...otherValues } = hap.value;
|
||||
// hap.value = { ...otherValues, freq: tune.note(i)}
|
||||
// return hap
|
||||
return hap.withValue(() => tune.note(hap.value.i));
|
||||
});
|
||||
});
|
||||
|
||||
+14
-4
@@ -78,14 +78,21 @@ Tune.prototype.frequency = function(stepIn, octaveIn) {
|
||||
}
|
||||
|
||||
// which scale degree (0 - scale length) is our input
|
||||
// 60 % 12 = 0
|
||||
var scaleDegree = stepIn % this.scale.length
|
||||
|
||||
// what's this doing
|
||||
// 0 scaleDegree = 12
|
||||
// seems to simply be for a negative result from above, maybe another way to do it, but ok for now
|
||||
while (scaleDegree < 0) {
|
||||
scaleDegree += this.scale.length
|
||||
}
|
||||
|
||||
// tonic is currently always 1
|
||||
// so this is 1*scale[scaledegree]
|
||||
var freq = this.tonic*this.scale[scaleDegree]
|
||||
|
||||
// map it to octave
|
||||
freq = freq*(Math.pow(2,octave))
|
||||
|
||||
// truncate irrational numbers
|
||||
@@ -137,10 +144,13 @@ Tune.prototype.MIDI = function(stepIn,octaveIn) {
|
||||
}
|
||||
|
||||
/* Load a new scale */
|
||||
// sets .scale to ratios
|
||||
|
||||
Tune.prototype.loadScale = function(scale){
|
||||
|
||||
/* load the scale */
|
||||
let name
|
||||
if (typeof scale === 'string') name = scale;
|
||||
var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies
|
||||
this.scale = []
|
||||
for (var i=0;i<freqs.length-1;i++) {
|
||||
@@ -148,10 +158,10 @@ Tune.prototype.loadScale = function(scale){
|
||||
}
|
||||
|
||||
/* visualize in console */
|
||||
/* console.log(" ");
|
||||
console.log("LOADED "+name);
|
||||
console.log(TuningList[name].description);
|
||||
console.log(this.scale); */
|
||||
// console.log(" ");
|
||||
// console.log("LOADED "+name);
|
||||
// console.log(TuningList[name].description);
|
||||
// console.log(this.scale);
|
||||
var vis = [];
|
||||
for (var i=0;i<100;i++) {
|
||||
vis[i] = " ";
|
||||
|
||||
+177
-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/>.
|
||||
*/
|
||||
|
||||
import { register, _mod, parseNumeral } from '@strudel/core';
|
||||
import { register, _mod, parseNumeral, removeUndefineds } from '@strudel/core';
|
||||
import Tune from './tunejs.js';
|
||||
|
||||
// returns a list of frequency ratios for given edo scale
|
||||
export function edo(name) {
|
||||
if (!/^[1-9]+[0-9]*edo$/.test(name)) {
|
||||
throw new Error('not an edo scale: "' + name + '"');
|
||||
@@ -18,23 +20,33 @@ const presets = {
|
||||
'12ji': [1 / 1, 16 / 15, 9 / 8, 6 / 5, 5 / 4, 4 / 3, 45 / 32, 3 / 2, 8 / 5, 5 / 3, 16 / 9, 15 / 8],
|
||||
};
|
||||
|
||||
function withBase(freq, scale) {
|
||||
// Given a base frequency such as 220 and an edo scale, returns
|
||||
// an array of frequencies representing the given edo scale in that base
|
||||
function _withBase(freq, scale) {
|
||||
return scale.map((r) => r * freq);
|
||||
}
|
||||
|
||||
const defaultBase = 220;
|
||||
|
||||
const isEdo = (scale) => /^[1-9]+[0-9]*edo$/.test(scale);
|
||||
|
||||
// Assumes a base of 220. Returns a filtered scale based on 'indices'
|
||||
// NOTE: indices functionality is unused
|
||||
function getXenScale(scale, indices) {
|
||||
let tune = new Tune();
|
||||
if (typeof scale === 'string') {
|
||||
if (/^[1-9]+[0-9]*edo$/.test(scale)) {
|
||||
if (isEdo(scale)) {
|
||||
scale = edo(scale);
|
||||
} else if (presets[scale]) {
|
||||
scale = presets[scale];
|
||||
} else if (tune.isValidScale(scale)) {
|
||||
tune.loadScale(scale);
|
||||
scale = tune.scale;
|
||||
} else {
|
||||
throw new Error('unknown scale name: "' + scale + '"');
|
||||
}
|
||||
}
|
||||
scale = withBase(defaultBase, scale);
|
||||
scale = _withBase(defaultBase, scale);
|
||||
if (!indices) {
|
||||
return scale;
|
||||
}
|
||||
@@ -47,16 +59,172 @@ function xenOffset(xenScale, offset, index = 0) {
|
||||
return xenScale[i] * Math.pow(2, oct);
|
||||
}
|
||||
|
||||
const trimFreq = (freq) => parseFloat(freq.toPrecision(10));
|
||||
|
||||
// accepts a scale name such as 31edo, and a pattern
|
||||
// pattern expected to follow format such that a value can be mapped
|
||||
// to an edostep within the scale. Returns the pattern with
|
||||
// values mapped to the frequencies associated with the given edosteps
|
||||
// scaleNameOrRatios: string || number[], steps?: number
|
||||
|
||||
/**
|
||||
* Assumes a numerical pattern of scale steps, and a scale. Scales accepted are all preset scale names of `tune`, arbitrary edos such as 31edo, or an array of frequency ratios. Assumes scales repeat at octave (2/1). Returns a new pattern with all values mapped to their associated frequency, assuming a base frequency of 220hz.
|
||||
*
|
||||
* @name xen
|
||||
* @returns Pattern
|
||||
* @memberof Pattern
|
||||
* @param {(string | number[] )} scaleNameOrRatios
|
||||
* @tags tonal
|
||||
* @example
|
||||
* // A minor triad in 31edo:
|
||||
* i("0 8 18").xen("31edo").piano()
|
||||
* @example
|
||||
* // You can also use xen with frequency ratios.
|
||||
* // This is equivalent to the above:
|
||||
* i("0 1 2").xen([
|
||||
* Math.pow(2, 0/31),
|
||||
* Math.pow(2, 8/31),
|
||||
* Math.pow(2, 18/31),
|
||||
* ]).piano()
|
||||
* @example
|
||||
* // xen also supports all scale names that
|
||||
* // tune does:
|
||||
* i("0 1 2 3 4 5").xen("hexany15")
|
||||
* // equiv to:
|
||||
* // "0 1 2 3 4 5".tune("hexany15").mul("220").freq()
|
||||
* @example
|
||||
* i("0 1 2 3 4 5 6 7").xen("<5edo 10edo 15edo hexany15>")
|
||||
*/
|
||||
|
||||
export const xen = register('xen', function (scaleNameOrRatios, pat) {
|
||||
return pat.withHap((hap) => {
|
||||
const scale = getXenScale(scaleNameOrRatios);
|
||||
const frequency = xenOffset(scale, parseNumeral(hap.value));
|
||||
return hap.withValue(() => frequency);
|
||||
return pat.withHaps((haps) => {
|
||||
haps = haps.map((hap) => {
|
||||
let hVal = hap.value;
|
||||
const isObject = typeof hVal === 'object';
|
||||
if (!isObject) {
|
||||
throw new Error(`Expected hap to have control 'i' set, but received ${hap.value.i}, try wrapping input in i()`);
|
||||
}
|
||||
const { i, ...otherValues } = hVal;
|
||||
const scale = getXenScale(scaleNameOrRatios);
|
||||
let freq = xenOffset(scale, parseNumeral(hVal.i));
|
||||
// 10 is somewhat arbitrary
|
||||
freq = trimFreq(freq);
|
||||
hap.value = { ...otherValues, freq };
|
||||
return isEdo(scaleNameOrRatios)
|
||||
? hap.setContext({ ...hap.context, edoSize: scaleNameOrRatios.match(/^([1-9]+[0-9]*)edo$/)[1] })
|
||||
: hap;
|
||||
});
|
||||
return removeUndefineds(haps);
|
||||
});
|
||||
});
|
||||
|
||||
export const tuning = register('tuning', function (ratios, pat) {
|
||||
/**
|
||||
* Assumes pattern of frequencies tuned to some `base` frequency, such as the output of `xen`
|
||||
* Because `xen` defaults to `220Hz`, so will `withBase`.
|
||||
* but you can specify a different original base with the standard optional array syntax '`:`'
|
||||
* @name withBase
|
||||
* @param {number} base
|
||||
* @param {number} (optional) originalBase
|
||||
* @tags tonal
|
||||
*
|
||||
* @example
|
||||
* i("[0 1 2 3] [3 4] [4 3 2 1]").xen("hexany23").withBase("<220 [300 200]>")
|
||||
* @example
|
||||
* mini([1 / 1, 16 / 15, 9 / 8, 6 / 5, 5 / 4].join(' ')).withBase("220:1")
|
||||
* // mini([1 / 1, 16 / 15, 9 / 8, 6 / 5, 5 / 4].join(' ')).mul(220).freq()
|
||||
*
|
||||
* @returns Pattern
|
||||
*/
|
||||
export const withBase = register('withBase', (b, pat) => {
|
||||
let base;
|
||||
let originalBase = 220;
|
||||
if (Array.isArray(b)) {
|
||||
base = b[0];
|
||||
originalBase = b[1];
|
||||
} else {
|
||||
base = b;
|
||||
}
|
||||
return pat.withHaps((haps) => {
|
||||
haps = haps.map((hap) => {
|
||||
let hVal = hap.value;
|
||||
const isObject = typeof hVal === 'object';
|
||||
let freq = isObject ? hVal.freq : hVal;
|
||||
if (!freq) return hap;
|
||||
freq = (freq * base) / originalBase;
|
||||
hap.value = isObject ? { ...hap.value, freq } : { freq };
|
||||
return hap;
|
||||
});
|
||||
return removeUndefineds(haps);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Frequency transpose. Assumes pattern either has `freq` set, or has values that can be interpreted as frequencies
|
||||
* amt has optional `edoSize` param, defaults to 12.
|
||||
* If haps have edoSize param set, such as from the output of `xen("31edo")`,
|
||||
* `ftrans` will fallback to that instead of 12 as the default.
|
||||
*
|
||||
* Transposes the frequency by `amt` edoSteps
|
||||
* @name ftranspose
|
||||
* @synonyms ftrans, fTrans, ftranspose, fTranspose
|
||||
* @tags tonal
|
||||
* @param {number} amt
|
||||
* @param {number} edoSize (optional)
|
||||
* @returns {Pattern}
|
||||
*
|
||||
* @example
|
||||
* i("0 1 2").xen("12edo").ftrans("7")
|
||||
* // n("0 1 2").scale("A:chromatic").trans("7")
|
||||
* @example
|
||||
* i("0 8 18").xen("31edo").ftrans("<8 -8>")
|
||||
* @example
|
||||
* // to transpose by steps of an edo, use "step:edo" :
|
||||
* i("0 7 8 18").xen("31edo").ftrans("<0 1:31 1:12>")
|
||||
* @example
|
||||
* // it can also work with frequency values directly
|
||||
* freq("200 300 400").ftrans("<0 7:31 7>")
|
||||
*/
|
||||
|
||||
/* f = frequency (Hz)
|
||||
n = edo (steps per octave)
|
||||
x = number of steps
|
||||
if 0\n = f, then x\n = f * 2^(x/n)
|
||||
example: 5edo, 0\5 = 220 Hz, then 2\5 = 220*2^(2/5) = 290.29 Hz */
|
||||
|
||||
export const { ftrans, fTrans, ftranspose, fTranspose } = register(
|
||||
['ftrans', 'fTrans', 'ftranspose', 'fTranspose'],
|
||||
(amt, pat) => {
|
||||
let edoSize;
|
||||
let numSteps;
|
||||
if (Array.isArray(amt)) {
|
||||
edoSize = amt[1];
|
||||
numSteps = amt[0];
|
||||
} else {
|
||||
numSteps = amt;
|
||||
}
|
||||
return pat.withHaps((haps) => {
|
||||
haps = haps.map((hap) => {
|
||||
let hVal = hap.value;
|
||||
const isObject = typeof hVal === 'object';
|
||||
hVal = isObject ? hVal : { freq: hVal };
|
||||
let { freq, ...otherValues } = hVal;
|
||||
if (edoSize == undefined && hap.context.edoSize != undefined) {
|
||||
edoSize = hap.context.edoSize;
|
||||
} else if (edoSize == undefined) {
|
||||
edoSize = 12;
|
||||
}
|
||||
freq = freq * Math.pow(2, numSteps / edoSize);
|
||||
freq = trimFreq(freq);
|
||||
hap.value = isObject ? { ...otherValues, freq } : freq;
|
||||
return hap.setContext({ ...hap.context, edoSize });
|
||||
});
|
||||
return removeUndefineds(haps);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// not sure there's a point to having this and the above, seems like a proto version of the above.
|
||||
const tuning = register('tuning', function (ratios, pat) {
|
||||
return pat.withHap((hap) => {
|
||||
const frequency = xenOffset(ratios, parseNumeral(hap.value));
|
||||
return hap.withValue(() => frequency);
|
||||
|
||||
Generated
+103
-7
@@ -4,6 +4,30 @@ settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
catalogs:
|
||||
default:
|
||||
'@codemirror/autocomplete':
|
||||
specifier: ^6.18.4
|
||||
version: 6.18.4
|
||||
'@codemirror/commands':
|
||||
specifier: ^6.8.0
|
||||
version: 6.8.0
|
||||
'@codemirror/lang-javascript':
|
||||
specifier: ^6.2.2
|
||||
version: 6.2.2
|
||||
'@codemirror/language':
|
||||
specifier: ^6.10.8
|
||||
version: 6.10.8
|
||||
'@codemirror/search':
|
||||
specifier: ^6.5.8
|
||||
version: 6.5.8
|
||||
'@codemirror/state':
|
||||
specifier: ^6.5.1
|
||||
version: 6.5.1
|
||||
'@codemirror/view':
|
||||
specifier: ^6.36.2
|
||||
version: 6.36.2
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -11,6 +35,9 @@ importers:
|
||||
'@strudel/core':
|
||||
specifier: workspace:*
|
||||
version: link:packages/core
|
||||
'@strudel/edo':
|
||||
specifier: workspace:*
|
||||
version: link:packages/edo
|
||||
'@strudel/mini':
|
||||
specifier: workspace:*
|
||||
version: link:packages/mini
|
||||
@@ -93,6 +120,9 @@ importers:
|
||||
'@strudel/draw':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/draw
|
||||
'@strudel/edo':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/edo
|
||||
'@strudel/mini':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/mini
|
||||
@@ -171,25 +201,25 @@ importers:
|
||||
packages/codemirror:
|
||||
dependencies:
|
||||
'@codemirror/autocomplete':
|
||||
specifier: ^6.18.4
|
||||
specifier: 'catalog:'
|
||||
version: 6.18.4
|
||||
'@codemirror/commands':
|
||||
specifier: ^6.8.0
|
||||
specifier: 'catalog:'
|
||||
version: 6.8.0
|
||||
'@codemirror/lang-javascript':
|
||||
specifier: ^6.2.2
|
||||
specifier: 'catalog:'
|
||||
version: 6.2.2
|
||||
'@codemirror/language':
|
||||
specifier: ^6.10.8
|
||||
specifier: 'catalog:'
|
||||
version: 6.10.8
|
||||
'@codemirror/search':
|
||||
specifier: ^6.5.8
|
||||
specifier: 'catalog:'
|
||||
version: 6.5.8
|
||||
'@codemirror/state':
|
||||
specifier: ^6.5.1
|
||||
specifier: 'catalog:'
|
||||
version: 6.5.1
|
||||
'@codemirror/view':
|
||||
specifier: ^6.36.2
|
||||
specifier: 'catalog:'
|
||||
version: 6.36.2
|
||||
'@lezer/highlight':
|
||||
specifier: ^1.2.1
|
||||
@@ -221,6 +251,9 @@ importers:
|
||||
'@tonaljs/tonal':
|
||||
specifier: ^4.10.0
|
||||
version: 4.10.0
|
||||
codemirror-helix:
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||
nanostores:
|
||||
specifier: ^0.11.3
|
||||
version: 0.11.3
|
||||
@@ -280,6 +313,28 @@ importers:
|
||||
specifier: ^6.0.11
|
||||
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||
|
||||
packages/edo:
|
||||
dependencies:
|
||||
'@strudel/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@tonaljs/tonal':
|
||||
specifier: ^4.10.0
|
||||
version: 4.10.0
|
||||
chord-voicings:
|
||||
specifier: ^0.0.1
|
||||
version: 0.0.1
|
||||
webmidi:
|
||||
specifier: ^3.1.12
|
||||
version: 3.1.12
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^6.0.11
|
||||
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||
vitest:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||
|
||||
packages/embed: {}
|
||||
|
||||
packages/gamepad:
|
||||
@@ -446,6 +501,9 @@ importers:
|
||||
'@strudel/draw':
|
||||
specifier: workspace:*
|
||||
version: link:../draw
|
||||
'@strudel/edo':
|
||||
specifier: workspace:*
|
||||
version: link:../edo
|
||||
'@strudel/hydra':
|
||||
specifier: workspace:*
|
||||
version: link:../hydra
|
||||
@@ -621,6 +679,9 @@ importers:
|
||||
'@strudel/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@strudel/edo':
|
||||
specifier: workspace:*
|
||||
version: link:../edo
|
||||
'@strudel/mini':
|
||||
specifier: workspace:*
|
||||
version: link:../mini
|
||||
@@ -702,6 +763,21 @@ importers:
|
||||
'@astrojs/tailwind':
|
||||
specifier: ^5.1.5
|
||||
version: 5.1.5(astro@5.1.9(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@2.79.2)(terser@5.37.0)(typescript@5.7.3)(yaml@2.7.0))(tailwindcss@3.4.17)
|
||||
'@codemirror/commands':
|
||||
specifier: 'catalog:'
|
||||
version: 6.8.0
|
||||
'@codemirror/lang-javascript':
|
||||
specifier: 'catalog:'
|
||||
version: 6.2.2
|
||||
'@codemirror/language':
|
||||
specifier: 'catalog:'
|
||||
version: 6.10.8
|
||||
'@codemirror/state':
|
||||
specifier: 'catalog:'
|
||||
version: 6.5.1
|
||||
'@codemirror/view':
|
||||
specifier: 'catalog:'
|
||||
version: 6.36.2
|
||||
'@docsearch/css':
|
||||
specifier: ^3.8.3
|
||||
version: 3.8.3
|
||||
@@ -735,6 +811,9 @@ importers:
|
||||
'@strudel/draw':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/draw
|
||||
'@strudel/edo':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/edo
|
||||
'@strudel/gamepad':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/gamepad
|
||||
@@ -3551,6 +3630,15 @@ packages:
|
||||
resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
|
||||
codemirror-helix@0.5.0:
|
||||
resolution: {integrity: sha512-hI56hf9VGz53H1YvL6H1GC7HtP6te8vX+MsIHaE9J7Q3PQ6KFapKtIRg6lqSH898ikHWpMCPu42r6HJN0IfVLA==}
|
||||
peerDependencies:
|
||||
'@codemirror/commands': ^6.0.0
|
||||
'@codemirror/language': ^6.0.0
|
||||
'@codemirror/search': ^6.0.0
|
||||
'@codemirror/state': ^6.0.0
|
||||
'@codemirror/view': ^6.0.0
|
||||
|
||||
collapse-white-space@2.1.0:
|
||||
resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
|
||||
|
||||
@@ -11280,6 +11368,14 @@ snapshots:
|
||||
|
||||
cmd-shim@6.0.3: {}
|
||||
|
||||
codemirror-helix@0.5.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2):
|
||||
dependencies:
|
||||
'@codemirror/commands': 6.8.0
|
||||
'@codemirror/language': 6.10.8
|
||||
'@codemirror/search': 6.5.8
|
||||
'@codemirror/state': 6.5.1
|
||||
'@codemirror/view': 6.36.2
|
||||
|
||||
collapse-white-space@2.1.0: {}
|
||||
|
||||
color-convert@2.0.1:
|
||||
|
||||
@@ -10,3 +10,12 @@ onlyBuiltDependencies:
|
||||
- sharp
|
||||
- tree-sitter
|
||||
- tree-sitter-haskell
|
||||
|
||||
catalog:
|
||||
"@codemirror/commands": "^6.8.0"
|
||||
"@codemirror/language": "^6.10.8"
|
||||
"@codemirror/lang-javascript": "^6.2.2"
|
||||
"@codemirror/state": "^6.5.1"
|
||||
"@codemirror/view": "^6.36.2"
|
||||
"@codemirror/autocomplete": "^6.18.4"
|
||||
"@codemirror/search": "^6.5.8"
|
||||
|
||||
@@ -1242,6 +1242,35 @@ exports[`runs examples > example "bank" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "base" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | note:D3 s:saw ]",
|
||||
"[ 1/6 → 1/3 | note:C4 s:saw ]",
|
||||
"[ 1/3 → 1/2 | note:A3 s:saw ]",
|
||||
"[ 1/2 → 2/3 | note:A3 s:saw ]",
|
||||
"[ 2/3 → 5/6 | note:G3 s:saw ]",
|
||||
"[ 5/6 → 1/1 | note:F3 s:saw ]",
|
||||
"[ 1/1 → 7/6 | note:D3 s:saw ]",
|
||||
"[ 7/6 → 4/3 | note:C4 s:saw ]",
|
||||
"[ 4/3 → 3/2 | note:A3 s:saw ]",
|
||||
"[ 3/2 → 5/3 | note:A3 s:saw ]",
|
||||
"[ 5/3 → 11/6 | note:G3 s:saw ]",
|
||||
"[ 11/6 → 2/1 | note:F3 s:saw ]",
|
||||
"[ 2/1 → 13/6 | note:D3 s:saw ]",
|
||||
"[ 13/6 → 7/3 | note:C4 s:saw ]",
|
||||
"[ 7/3 → 5/2 | note:A3 s:saw ]",
|
||||
"[ 5/2 → 8/3 | note:A3 s:saw ]",
|
||||
"[ 8/3 → 17/6 | note:G3 s:saw ]",
|
||||
"[ 17/6 → 3/1 | note:F3 s:saw ]",
|
||||
"[ 3/1 → 19/6 | note:D3 s:saw ]",
|
||||
"[ 19/6 → 10/3 | note:C4 s:saw ]",
|
||||
"[ 10/3 → 7/2 | note:A3 s:saw ]",
|
||||
"[ 7/2 → 11/3 | note:A3 s:saw ]",
|
||||
"[ 11/3 → 23/6 | note:G3 s:saw ]",
|
||||
"[ 23/6 → 4/1 | note:F3 s:saw ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "beat" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/16 | s:bd ]",
|
||||
@@ -3012,15 +3041,6 @@ exports[`runs examples > example "delayfeedback" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "delayfeedback" example index 0 2`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:bd delay:0.25 delayfeedback:0.25 ]",
|
||||
"[ 1/1 → 2/1 | s:bd delay:0.25 delayfeedback:0.5 ]",
|
||||
"[ 2/1 → 3/1 | s:bd delay:0.25 delayfeedback:0.75 ]",
|
||||
"[ 3/1 → 4/1 | s:bd delay:0.25 delayfeedback:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "delayspeed" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
@@ -3071,6 +3091,43 @@ exports[`runs examples > example "delaysync" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "delaytime" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 1/8 → 1/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 1/4 → 3/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 3/8 → 1/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 1/2 → 5/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 5/8 → 3/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 3/4 → 7/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 7/8 → 1/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 1/1 → 9/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 9/8 → 5/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 5/4 → 11/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 11/8 → 3/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 3/2 → 13/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 13/8 → 7/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 7/4 → 15/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 15/8 → 2/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 2/1 → 17/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 17/8 → 9/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 9/4 → 19/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 19/8 → 5/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 5/2 → 21/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 21/8 → 11/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 11/4 → 23/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 23/8 → 3/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 3/1 → 25/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 25/8 → 13/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 13/4 → 27/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 27/8 → 7/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 7/2 → 29/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 29/8 → 15/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 15/4 → 31/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 31/8 → 4/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "density" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:crackle density:0.01 ]",
|
||||
@@ -3959,6 +4016,93 @@ exports[`runs examples > example "echoWith" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "edoScale" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | degree:1 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:130.813 edo:12 ]",
|
||||
"[ 1/6 → 1/3 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
||||
"[ 1/3 → 1/2 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
||||
"[ 1/2 → 2/3 | degree:7 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:246.942 edo:12 ]",
|
||||
"[ 2/3 → 5/6 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
||||
"[ 5/6 → 1/1 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
||||
"[ 1/1 → 7/6 | degree:1 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:130.813 edo:12 ]",
|
||||
"[ 7/6 → 4/3 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
||||
"[ 4/3 → 3/2 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
||||
"[ 3/2 → 5/3 | degree:7 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:246.942 edo:12 ]",
|
||||
"[ 5/3 → 11/6 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
||||
"[ 11/6 → 2/1 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
||||
"[ 2/1 → 13/6 | degree:1 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:130.813 edo:12 ]",
|
||||
"[ 13/6 → 7/3 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
||||
"[ 7/3 → 5/2 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
||||
"[ 5/2 → 8/3 | degree:7 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:246.942 edo:12 ]",
|
||||
"[ 8/3 → 17/6 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
||||
"[ 17/6 → 3/1 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
||||
"[ 3/1 → 19/6 | degree:1 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:130.813 edo:12 ]",
|
||||
"[ 19/6 → 10/3 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
||||
"[ 10/3 → 7/2 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
||||
"[ 7/2 → 11/3 | degree:7 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:246.942 edo:12 ]",
|
||||
"[ 11/3 → 23/6 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
||||
"[ 23/6 → 4/1 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "edoScale" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | degree:1 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:97.999 edo:16 s:piano ]",
|
||||
"[ 0/1 → 1/4 | degree:2 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
||||
"[ 1/4 → 1/2 | degree:5 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:151.135 edo:16 s:piano ]",
|
||||
"[ 1/2 → 3/4 | degree:3 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:127.089 edo:16 s:piano ]",
|
||||
"[ 1/2 → 3/4 | degree:2 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
||||
"[ 3/4 → 1/1 | degree:5 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:151.135 edo:16 s:piano ]",
|
||||
"[ 1/1 → 5/4 | degree:1 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:97.999 edo:16 s:piano ]",
|
||||
"[ 1/1 → 5/4 | degree:2 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
||||
"[ 5/4 → 3/2 | degree:5 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:164.814 edo:16 s:piano ]",
|
||||
"[ 3/2 → 7/4 | degree:3 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:127.089 edo:16 s:piano ]",
|
||||
"[ 3/2 → 7/4 | degree:2 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
||||
"[ 7/4 → 2/1 | degree:5 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:164.814 edo:16 s:piano ]",
|
||||
"[ 2/1 → 9/4 | degree:1 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:97.999 edo:16 s:piano ]",
|
||||
"[ 2/1 → 9/4 | degree:2 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
||||
"[ 9/4 → 5/2 | degree:5 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:151.135 edo:16 s:piano ]",
|
||||
"[ 5/2 → 11/4 | degree:3 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:127.089 edo:16 s:piano ]",
|
||||
"[ 5/2 → 11/4 | degree:2 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
||||
"[ 11/4 → 3/1 | degree:5 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:151.135 edo:16 s:piano ]",
|
||||
"[ 3/1 → 13/4 | degree:1 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:97.999 edo:16 s:piano ]",
|
||||
"[ 3/1 → 13/4 | degree:2 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
||||
"[ 13/4 → 7/2 | degree:5 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:164.814 edo:16 s:piano ]",
|
||||
"[ 7/2 → 15/4 | degree:3 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:127.089 edo:16 s:piano ]",
|
||||
"[ 7/2 → 15/4 | degree:2 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
||||
"[ 15/4 → 4/1 | degree:5 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:164.814 edo:16 s:piano ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "edoScale" example index 2 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:97.999 edo:13 s:piano ]",
|
||||
"[ 1/6 → 1/3 | degree:5 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:167.025 edo:13 s:piano ]",
|
||||
"[ 1/3 → 1/2 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:134.945 edo:13 s:piano ]",
|
||||
"[ 1/2 → 2/3 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:114.998 edo:13 s:piano ]",
|
||||
"[ 2/3 → 5/6 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:134.945 edo:13 s:piano ]",
|
||||
"[ 5/6 → 1/1 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:97.999 edo:13 s:piano ]",
|
||||
"[ 1/1 → 7/6 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:189.995 edo:13 s:piano ]",
|
||||
"[ 7/6 → 4/3 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:153.504 edo:13 s:piano ]",
|
||||
"[ 4/3 → 3/2 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:180.13 edo:13 s:piano ]",
|
||||
"[ 3/2 → 5/3 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:189.995 edo:13 s:piano ]",
|
||||
"[ 5/3 → 11/6 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:130.813 edo:13 s:piano ]",
|
||||
"[ 11/6 → 2/1 | degree:5 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:222.952 edo:13 s:piano ]",
|
||||
"[ 2/1 → 13/6 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:195.998 edo:13 s:piano ]",
|
||||
"[ 13/6 → 7/3 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:142.336 edo:13 s:piano ]",
|
||||
"[ 7/3 → 5/2 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:134.945 edo:13 s:piano ]",
|
||||
"[ 5/2 → 8/3 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:134.945 edo:13 s:piano ]",
|
||||
"[ 8/3 → 17/6 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:142.336 edo:13 s:piano ]",
|
||||
"[ 17/6 → 3/1 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:114.998 edo:13 s:piano ]",
|
||||
"[ 3/1 → 19/6 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:153.504 edo:13 s:piano ]",
|
||||
"[ 19/6 → 10/3 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:153.504 edo:13 s:piano ]",
|
||||
"[ 10/3 → 7/2 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:261.626 edo:13 s:piano ]",
|
||||
"[ 7/2 → 11/3 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:180.13 edo:13 s:piano ]",
|
||||
"[ 11/3 → 23/6 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:180.13 edo:13 s:piano ]",
|
||||
"[ 23/6 → 4/1 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:189.995 edo:13 s:piano ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "end" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:oh end:0.1 ]",
|
||||
@@ -4950,6 +5094,78 @@ exports[`runs examples > example "fscope" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "ftranspose" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/3 | freq:329.6275569 ]",
|
||||
"[ 1/3 → 2/3 | freq:349.2282315 ]",
|
||||
"[ 2/3 → 1/1 | freq:369.9944227 ]",
|
||||
"[ 1/1 → 4/3 | freq:329.6275569 ]",
|
||||
"[ 4/3 → 5/3 | freq:349.2282315 ]",
|
||||
"[ 5/3 → 2/1 | freq:369.9944227 ]",
|
||||
"[ 2/1 → 7/3 | freq:329.6275569 ]",
|
||||
"[ 7/3 → 8/3 | freq:349.2282315 ]",
|
||||
"[ 8/3 → 3/1 | freq:369.9944227 ]",
|
||||
"[ 3/1 → 10/3 | freq:329.6275569 ]",
|
||||
"[ 10/3 → 11/3 | freq:349.2282315 ]",
|
||||
"[ 11/3 → 4/1 | freq:369.9944227 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "ftranspose" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/3 | freq:263.0921203 ]",
|
||||
"[ 1/3 → 2/3 | freq:314.6248353 ]",
|
||||
"[ 2/3 → 1/1 | freq:393.4589706 ]",
|
||||
"[ 1/1 → 4/3 | freq:183.965981 ]",
|
||||
"[ 4/3 → 5/3 | freq:220 ]",
|
||||
"[ 5/3 → 2/1 | freq:275.1244143 ]",
|
||||
"[ 2/1 → 7/3 | freq:263.0921203 ]",
|
||||
"[ 7/3 → 8/3 | freq:314.6248353 ]",
|
||||
"[ 8/3 → 3/1 | freq:393.4589706 ]",
|
||||
"[ 3/1 → 10/3 | freq:183.965981 ]",
|
||||
"[ 10/3 → 11/3 | freq:220 ]",
|
||||
"[ 11/3 → 4/1 | freq:275.1244143 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "ftranspose" example index 2 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | freq:220 ]",
|
||||
"[ 1/4 → 1/2 | freq:257.2747684 ]",
|
||||
"[ 1/2 → 3/4 | freq:263.0921203 ]",
|
||||
"[ 3/4 → 1/1 | freq:329.0139341 ]",
|
||||
"[ 1/1 → 5/4 | freq:224.9745158 ]",
|
||||
"[ 5/4 → 3/2 | freq:263.0921203 ]",
|
||||
"[ 3/2 → 7/4 | freq:269.0410108 ]",
|
||||
"[ 7/4 → 2/1 | freq:336.4534115 ]",
|
||||
"[ 2/1 → 9/4 | freq:233.0818808 ]",
|
||||
"[ 9/4 → 5/2 | freq:272.5731222 ]",
|
||||
"[ 5/2 → 11/4 | freq:278.7363919 ]",
|
||||
"[ 11/4 → 3/1 | freq:348.5781207 ]",
|
||||
"[ 3/1 → 13/4 | freq:220 ]",
|
||||
"[ 13/4 → 7/2 | freq:257.2747684 ]",
|
||||
"[ 7/2 → 15/4 | freq:263.0921203 ]",
|
||||
"[ 15/4 → 4/1 | freq:329.0139341 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "ftranspose" example index 3 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/3 | freq:200 ]",
|
||||
"[ 1/3 → 2/3 | freq:300 ]",
|
||||
"[ 2/3 → 1/1 | freq:400 ]",
|
||||
"[ 1/1 → 4/3 | freq:233.8861531 ]",
|
||||
"[ 4/3 → 5/3 | freq:350.8292297 ]",
|
||||
"[ 5/3 → 2/1 | freq:467.7723062 ]",
|
||||
"[ 2/1 → 7/3 | freq:299.6614154 ]",
|
||||
"[ 7/3 → 8/3 | freq:449.4921231 ]",
|
||||
"[ 8/3 → 3/1 | freq:599.3228308 ]",
|
||||
"[ 3/1 → 10/3 | freq:200 ]",
|
||||
"[ 10/3 → 11/3 | freq:300 ]",
|
||||
"[ 11/3 → 4/1 | freq:400 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "ftype" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:f s:sawtooth lpenv:4 cutoff:500 ftype:0 resonance:1 ]",
|
||||
@@ -5119,6 +5335,15 @@ exports[`runs examples > example "gain" example index 0 1`] = `
|
||||
|
||||
exports[`runs examples > example "gap" example index 0 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "getDur" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:sax cps:null ]",
|
||||
"[ 1/1 → 2/1 | s:sax cps:null ]",
|
||||
"[ 2/1 → 3/1 | s:sax cps:null ]",
|
||||
"[ 3/1 → 4/1 | s:sax cps:null ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "grow" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/10 | s:tha bank:mridangam ]",
|
||||
@@ -5634,6 +5859,43 @@ exports[`runs examples > example "hush" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "i" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | freq:220 ]",
|
||||
"[ 1/8 → 1/4 | freq:252.7136381 ]",
|
||||
"[ 1/4 → 3/8 | freq:290.2917404 ]",
|
||||
"[ 3/8 → 1/2 | freq:333.4576446 ]",
|
||||
"[ 1/2 → 5/8 | freq:383.0422479 ]",
|
||||
"[ 5/8 → 3/4 | freq:440 ]",
|
||||
"[ 3/4 → 7/8 | freq:505.4272762 ]",
|
||||
"[ 7/8 → 1/1 | freq:580.5834807 ]",
|
||||
"[ 1/1 → 9/8 | freq:220 ]",
|
||||
"[ 9/8 → 5/4 | freq:235.7901618 ]",
|
||||
"[ 5/4 → 11/8 | freq:252.7136381 ]",
|
||||
"[ 11/8 → 3/2 | freq:270.8517709 ]",
|
||||
"[ 3/2 → 13/8 | freq:290.2917404 ]",
|
||||
"[ 13/8 → 7/4 | freq:311.1269837 ]",
|
||||
"[ 7/4 → 15/8 | freq:333.4576446 ]",
|
||||
"[ 15/8 → 2/1 | freq:357.3910544 ]",
|
||||
"[ 2/1 → 17/8 | freq:220 ]",
|
||||
"[ 17/8 → 9/4 | freq:230.404707 ]",
|
||||
"[ 9/4 → 19/8 | freq:241.3014955 ]",
|
||||
"[ 19/8 → 5/2 | freq:252.7136381 ]",
|
||||
"[ 5/2 → 21/8 | freq:264.6655079 ]",
|
||||
"[ 21/8 → 11/4 | freq:277.182631 ]",
|
||||
"[ 11/4 → 23/8 | freq:290.2917404 ]",
|
||||
"[ 23/8 → 3/1 | freq:304.0208336 ]",
|
||||
"[ 3/1 → 25/8 | freq:220 ]",
|
||||
"[ 25/8 → 13/4 | freq:275 ]",
|
||||
"[ 13/4 → 27/8 | freq:293.3333333 ]",
|
||||
"[ 27/8 → 7/2 | freq:330 ]",
|
||||
"[ 7/2 → 29/8 | freq:352 ]",
|
||||
"[ 29/8 → 15/4 | freq:440 ]",
|
||||
"[ 15/4 → 31/8 | freq:550 ]",
|
||||
"[ 31/8 → 4/1 | freq:586.6666667 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "inhabit" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:bd ]",
|
||||
@@ -6294,6 +6556,271 @@ exports[`runs examples > example "juxBy" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "juxFlip" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:bd pan:0 ]",
|
||||
"[ 0/1 → 1/8 | s:hh pan:1 ]",
|
||||
"[ 1/8 → 1/4 | s:lt pan:0 ]",
|
||||
"[ 1/8 → 1/4 | s:bd pan:1 ]",
|
||||
"[ 5/16 → 3/8 | s:ht pan:0 ]",
|
||||
"[ 3/8 → 1/2 | s:mt pan:0 ]",
|
||||
"[ 3/8 → 1/2 | s:cp pan:1 ]",
|
||||
"[ 1/2 → 5/8 | s:cp pan:0 ]",
|
||||
"[ 1/2 → 5/8 | s:mt pan:1 ]",
|
||||
"[ 5/8 → 11/16 | s:ht pan:1 ]",
|
||||
"[ 3/4 → 7/8 | s:bd pan:0 ]",
|
||||
"[ 3/4 → 7/8 | s:lt pan:1 ]",
|
||||
"[ 7/8 → 1/1 | s:hh pan:0 ]",
|
||||
"[ 7/8 → 1/1 | s:bd pan:1 ]",
|
||||
"[ 1/1 → 9/8 | s:bd pan:1 ]",
|
||||
"[ 1/1 → 9/8 | s:hh pan:0 ]",
|
||||
"[ 9/8 → 5/4 | s:lt pan:1 ]",
|
||||
"[ 9/8 → 5/4 | s:bd pan:0 ]",
|
||||
"[ 21/16 → 11/8 | s:ht pan:1 ]",
|
||||
"[ 11/8 → 3/2 | s:mt pan:1 ]",
|
||||
"[ 11/8 → 3/2 | s:cp pan:0 ]",
|
||||
"[ 3/2 → 13/8 | s:cp pan:1 ]",
|
||||
"[ 3/2 → 13/8 | s:mt pan:0 ]",
|
||||
"[ 13/8 → 27/16 | s:ht pan:0 ]",
|
||||
"[ 7/4 → 15/8 | s:bd pan:1 ]",
|
||||
"[ 7/4 → 15/8 | s:lt pan:0 ]",
|
||||
"[ 15/8 → 2/1 | s:hh pan:1 ]",
|
||||
"[ 15/8 → 2/1 | s:bd pan:0 ]",
|
||||
"[ 2/1 → 17/8 | s:bd pan:0 ]",
|
||||
"[ 2/1 → 17/8 | s:hh pan:1 ]",
|
||||
"[ 17/8 → 9/4 | s:lt pan:0 ]",
|
||||
"[ 17/8 → 9/4 | s:bd pan:1 ]",
|
||||
"[ 37/16 → 19/8 | s:ht pan:0 ]",
|
||||
"[ 19/8 → 5/2 | s:mt pan:0 ]",
|
||||
"[ 19/8 → 5/2 | s:cp pan:1 ]",
|
||||
"[ 5/2 → 21/8 | s:cp pan:0 ]",
|
||||
"[ 5/2 → 21/8 | s:mt pan:1 ]",
|
||||
"[ 21/8 → 43/16 | s:ht pan:1 ]",
|
||||
"[ 11/4 → 23/8 | s:bd pan:0 ]",
|
||||
"[ 11/4 → 23/8 | s:lt pan:1 ]",
|
||||
"[ 23/8 → 3/1 | s:hh pan:0 ]",
|
||||
"[ 23/8 → 3/1 | s:bd pan:1 ]",
|
||||
"[ 3/1 → 25/8 | s:bd pan:1 ]",
|
||||
"[ 3/1 → 25/8 | s:hh pan:0 ]",
|
||||
"[ 25/8 → 13/4 | s:lt pan:1 ]",
|
||||
"[ 25/8 → 13/4 | s:bd pan:0 ]",
|
||||
"[ 53/16 → 27/8 | s:ht pan:1 ]",
|
||||
"[ 27/8 → 7/2 | s:mt pan:1 ]",
|
||||
"[ 27/8 → 7/2 | s:cp pan:0 ]",
|
||||
"[ 7/2 → 29/8 | s:cp pan:1 ]",
|
||||
"[ 7/2 → 29/8 | s:mt pan:0 ]",
|
||||
"[ 29/8 → 59/16 | s:ht pan:0 ]",
|
||||
"[ 15/4 → 31/8 | s:bd pan:1 ]",
|
||||
"[ 15/4 → 31/8 | s:lt pan:0 ]",
|
||||
"[ 31/8 → 4/1 | s:hh pan:1 ]",
|
||||
"[ 31/8 → 4/1 | s:bd pan:0 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "juxFlip" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:bd pan:0 ]",
|
||||
"[ 1/16 → 1/8 | s:bd pan:1 ]",
|
||||
"[ 1/8 → 1/4 | s:lt pan:0 ]",
|
||||
"[ 3/16 → 1/4 | s:lt pan:1 ]",
|
||||
"[ 5/16 → 3/8 | s:ht pan:0 ]",
|
||||
"[ 11/32 → 3/8 | s:ht pan:1 ]",
|
||||
"[ 3/8 → 1/2 | s:mt pan:0 ]",
|
||||
"[ 7/16 → 1/2 | s:mt pan:1 ]",
|
||||
"[ 1/2 → 5/8 | s:cp pan:0 ]",
|
||||
"[ 9/16 → 5/8 | s:cp pan:1 ]",
|
||||
"[ 3/4 → 7/8 | s:bd pan:0 ]",
|
||||
"[ 13/16 → 7/8 | s:bd pan:1 ]",
|
||||
"[ 7/8 → 1/1 | s:hh pan:0 ]",
|
||||
"[ 15/16 → 1/1 | s:hh pan:1 ]",
|
||||
"[ 1/1 → 9/8 | s:bd pan:1 ]",
|
||||
"[ 17/16 → 9/8 | s:bd pan:0 ]",
|
||||
"[ 9/8 → 5/4 | s:lt pan:1 ]",
|
||||
"[ 19/16 → 5/4 | s:lt pan:0 ]",
|
||||
"[ 21/16 → 11/8 | s:ht pan:1 ]",
|
||||
"[ 43/32 → 11/8 | s:ht pan:0 ]",
|
||||
"[ 11/8 → 3/2 | s:mt pan:1 ]",
|
||||
"[ 23/16 → 3/2 | s:mt pan:0 ]",
|
||||
"[ 3/2 → 13/8 | s:cp pan:1 ]",
|
||||
"[ 25/16 → 13/8 | s:cp pan:0 ]",
|
||||
"[ 7/4 → 15/8 | s:bd pan:1 ]",
|
||||
"[ 29/16 → 15/8 | s:bd pan:0 ]",
|
||||
"[ 15/8 → 2/1 | s:hh pan:1 ]",
|
||||
"[ 31/16 → 2/1 | s:hh pan:0 ]",
|
||||
"[ 2/1 → 17/8 | s:bd pan:0 ]",
|
||||
"[ 33/16 → 17/8 | s:bd pan:1 ]",
|
||||
"[ 17/8 → 9/4 | s:lt pan:0 ]",
|
||||
"[ 35/16 → 9/4 | s:lt pan:1 ]",
|
||||
"[ 37/16 → 19/8 | s:ht pan:0 ]",
|
||||
"[ 75/32 → 19/8 | s:ht pan:1 ]",
|
||||
"[ 19/8 → 5/2 | s:mt pan:0 ]",
|
||||
"[ 39/16 → 5/2 | s:mt pan:1 ]",
|
||||
"[ 5/2 → 21/8 | s:cp pan:0 ]",
|
||||
"[ 41/16 → 21/8 | s:cp pan:1 ]",
|
||||
"[ 11/4 → 23/8 | s:bd pan:0 ]",
|
||||
"[ 45/16 → 23/8 | s:bd pan:1 ]",
|
||||
"[ 23/8 → 3/1 | s:hh pan:0 ]",
|
||||
"[ 47/16 → 3/1 | s:hh pan:1 ]",
|
||||
"[ 3/1 → 25/8 | s:bd pan:1 ]",
|
||||
"[ 49/16 → 25/8 | s:bd pan:0 ]",
|
||||
"[ 25/8 → 13/4 | s:lt pan:1 ]",
|
||||
"[ 51/16 → 13/4 | s:lt pan:0 ]",
|
||||
"[ 53/16 → 27/8 | s:ht pan:1 ]",
|
||||
"[ 107/32 → 27/8 | s:ht pan:0 ]",
|
||||
"[ 27/8 → 7/2 | s:mt pan:1 ]",
|
||||
"[ 55/16 → 7/2 | s:mt pan:0 ]",
|
||||
"[ 7/2 → 29/8 | s:cp pan:1 ]",
|
||||
"[ 57/16 → 29/8 | s:cp pan:0 ]",
|
||||
"[ 15/4 → 31/8 | s:bd pan:1 ]",
|
||||
"[ 61/16 → 31/8 | s:bd pan:0 ]",
|
||||
"[ 31/8 → 4/1 | s:hh pan:1 ]",
|
||||
"[ 63/16 → 4/1 | s:hh pan:0 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "juxFlip" example index 2 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:bd pan:0 ]",
|
||||
"[ 0/1 → 1/8 | s:bd pan:1 ]",
|
||||
"[ 1/8 → 1/4 | s:lt pan:0 ]",
|
||||
"[ 1/8 → 1/4 | s:lt pan:1 ]",
|
||||
"[ 5/16 → 3/8 | s:ht pan:0 ]",
|
||||
"[ 5/16 → 3/8 | s:ht pan:1 ]",
|
||||
"[ 3/8 → 1/2 | s:mt pan:0 ]",
|
||||
"[ 3/8 → 1/2 | s:mt pan:1 ]",
|
||||
"[ 1/2 → 5/8 | s:cp pan:0 ]",
|
||||
"[ 1/2 → 5/8 | s:cp pan:1 ]",
|
||||
"[ 3/4 → 7/8 | s:bd pan:0 ]",
|
||||
"[ 3/4 → 7/8 | s:bd pan:1 ]",
|
||||
"[ 7/8 → 1/1 | s:hh pan:0 ]",
|
||||
"[ 7/8 → 1/1 | s:hh pan:1 ]",
|
||||
"[ 1/1 → 9/8 | s:bd pan:1 ]",
|
||||
"[ 17/16 → 9/8 | s:ht pan:0 ]",
|
||||
"[ 9/8 → 5/4 | s:lt pan:1 ]",
|
||||
"[ 9/8 → 5/4 | s:mt pan:0 ]",
|
||||
"[ 5/4 → 11/8 | s:cp pan:0 ]",
|
||||
"[ 21/16 → 11/8 | s:ht pan:1 ]",
|
||||
"[ 11/8 → 3/2 | s:mt pan:1 ]",
|
||||
"[ 3/2 → 13/8 | s:cp pan:1 ]",
|
||||
"[ 3/2 → 13/8 | s:bd pan:0 ]",
|
||||
"[ 13/8 → 7/4 | s:hh pan:0 ]",
|
||||
"[ 7/4 → 15/8 | s:bd pan:1 ]",
|
||||
"[ 7/4 → 15/8 | s:bd pan:0 ]",
|
||||
"[ 15/8 → 2/1 | s:hh pan:1 ]",
|
||||
"[ 15/8 → 2/1 | s:lt pan:0 ]",
|
||||
"[ 2/1 → 17/8 | s:bd pan:0 ]",
|
||||
"[ 2/1 → 17/8 | s:cp pan:1 ]",
|
||||
"[ 17/8 → 9/4 | s:lt pan:0 ]",
|
||||
"[ 9/4 → 19/8 | s:bd pan:1 ]",
|
||||
"[ 37/16 → 19/8 | s:ht pan:0 ]",
|
||||
"[ 19/8 → 5/2 | s:mt pan:0 ]",
|
||||
"[ 19/8 → 5/2 | s:hh pan:1 ]",
|
||||
"[ 5/2 → 21/8 | s:cp pan:0 ]",
|
||||
"[ 5/2 → 21/8 | s:bd pan:1 ]",
|
||||
"[ 21/8 → 11/4 | s:lt pan:1 ]",
|
||||
"[ 11/4 → 23/8 | s:bd pan:0 ]",
|
||||
"[ 45/16 → 23/8 | s:ht pan:1 ]",
|
||||
"[ 23/8 → 3/1 | s:hh pan:0 ]",
|
||||
"[ 23/8 → 3/1 | s:mt pan:1 ]",
|
||||
"[ 3/1 → 25/8 | s:bd pan:1 ]",
|
||||
"[ 3/1 → 25/8 | s:bd pan:0 ]",
|
||||
"[ 25/8 → 13/4 | s:lt pan:1 ]",
|
||||
"[ 25/8 → 13/4 | s:hh pan:0 ]",
|
||||
"[ 13/4 → 27/8 | s:bd pan:0 ]",
|
||||
"[ 53/16 → 27/8 | s:ht pan:1 ]",
|
||||
"[ 27/8 → 7/2 | s:mt pan:1 ]",
|
||||
"[ 27/8 → 7/2 | s:lt pan:0 ]",
|
||||
"[ 7/2 → 29/8 | s:cp pan:1 ]",
|
||||
"[ 57/16 → 29/8 | s:ht pan:0 ]",
|
||||
"[ 29/8 → 15/4 | s:mt pan:0 ]",
|
||||
"[ 15/4 → 31/8 | s:bd pan:1 ]",
|
||||
"[ 15/4 → 31/8 | s:cp pan:0 ]",
|
||||
"[ 31/8 → 4/1 | s:hh pan:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "juxFlipBy" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:bd pan:0.09999999999999998 ]",
|
||||
"[ 0/1 → 1/8 | s:hh pan:0.9 ]",
|
||||
"[ 1/8 → 1/4 | s:lt pan:0.09999999999999998 ]",
|
||||
"[ 1/8 → 1/4 | s:bd pan:0.9 ]",
|
||||
"[ 5/16 → 3/8 | s:ht pan:0.09999999999999998 ]",
|
||||
"[ 3/8 → 1/2 | s:mt pan:0.09999999999999998 ]",
|
||||
"[ 3/8 → 1/2 | s:cp pan:0.9 ]",
|
||||
"[ 1/2 → 5/8 | s:cp pan:0.09999999999999998 ]",
|
||||
"[ 1/2 → 5/8 | s:mt pan:0.9 ]",
|
||||
"[ 5/8 → 11/16 | s:ht pan:0.9 ]",
|
||||
"[ 3/4 → 7/8 | s:bd pan:0.09999999999999998 ]",
|
||||
"[ 3/4 → 7/8 | s:lt pan:0.9 ]",
|
||||
"[ 7/8 → 1/1 | s:hh pan:0.09999999999999998 ]",
|
||||
"[ 7/8 → 1/1 | s:bd pan:0.9 ]",
|
||||
"[ 1/1 → 9/8 | s:bd pan:0.9 ]",
|
||||
"[ 1/1 → 9/8 | s:hh pan:0.09999999999999998 ]",
|
||||
"[ 9/8 → 5/4 | s:lt pan:0.9 ]",
|
||||
"[ 9/8 → 5/4 | s:bd pan:0.09999999999999998 ]",
|
||||
"[ 21/16 → 11/8 | s:ht pan:0.9 ]",
|
||||
"[ 11/8 → 3/2 | s:mt pan:0.9 ]",
|
||||
"[ 11/8 → 3/2 | s:cp pan:0.09999999999999998 ]",
|
||||
"[ 3/2 → 13/8 | s:cp pan:0.9 ]",
|
||||
"[ 3/2 → 13/8 | s:mt pan:0.09999999999999998 ]",
|
||||
"[ 13/8 → 27/16 | s:ht pan:0.09999999999999998 ]",
|
||||
"[ 7/4 → 15/8 | s:bd pan:0.9 ]",
|
||||
"[ 7/4 → 15/8 | s:lt pan:0.09999999999999998 ]",
|
||||
"[ 15/8 → 2/1 | s:hh pan:0.9 ]",
|
||||
"[ 15/8 → 2/1 | s:bd pan:0.09999999999999998 ]",
|
||||
"[ 2/1 → 17/8 | s:bd pan:0.09999999999999998 ]",
|
||||
"[ 2/1 → 17/8 | s:hh pan:0.9 ]",
|
||||
"[ 17/8 → 9/4 | s:lt pan:0.09999999999999998 ]",
|
||||
"[ 17/8 → 9/4 | s:bd pan:0.9 ]",
|
||||
"[ 37/16 → 19/8 | s:ht pan:0.09999999999999998 ]",
|
||||
"[ 19/8 → 5/2 | s:mt pan:0.09999999999999998 ]",
|
||||
"[ 19/8 → 5/2 | s:cp pan:0.9 ]",
|
||||
"[ 5/2 → 21/8 | s:cp pan:0.09999999999999998 ]",
|
||||
"[ 5/2 → 21/8 | s:mt pan:0.9 ]",
|
||||
"[ 21/8 → 43/16 | s:ht pan:0.9 ]",
|
||||
"[ 11/4 → 23/8 | s:bd pan:0.09999999999999998 ]",
|
||||
"[ 11/4 → 23/8 | s:lt pan:0.9 ]",
|
||||
"[ 23/8 → 3/1 | s:hh pan:0.09999999999999998 ]",
|
||||
"[ 23/8 → 3/1 | s:bd pan:0.9 ]",
|
||||
"[ 3/1 → 25/8 | s:bd pan:0.9 ]",
|
||||
"[ 3/1 → 25/8 | s:hh pan:0.09999999999999998 ]",
|
||||
"[ 25/8 → 13/4 | s:lt pan:0.9 ]",
|
||||
"[ 25/8 → 13/4 | s:bd pan:0.09999999999999998 ]",
|
||||
"[ 53/16 → 27/8 | s:ht pan:0.9 ]",
|
||||
"[ 27/8 → 7/2 | s:mt pan:0.9 ]",
|
||||
"[ 27/8 → 7/2 | s:cp pan:0.09999999999999998 ]",
|
||||
"[ 7/2 → 29/8 | s:cp pan:0.9 ]",
|
||||
"[ 7/2 → 29/8 | s:mt pan:0.09999999999999998 ]",
|
||||
"[ 29/8 → 59/16 | s:ht pan:0.09999999999999998 ]",
|
||||
"[ 15/4 → 31/8 | s:bd pan:0.9 ]",
|
||||
"[ 15/4 → 31/8 | s:lt pan:0.09999999999999998 ]",
|
||||
"[ 31/8 → 4/1 | s:hh pan:0.9 ]",
|
||||
"[ 31/8 → 4/1 | s:bd pan:0.09999999999999998 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "keep" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c s:piano ]",
|
||||
"[ 1/4 → 1/2 | note:a s:piano ]",
|
||||
"[ 1/2 → 3/4 | note:f s:piano ]",
|
||||
"[ 3/4 → 1/1 | note:e s:piano ]",
|
||||
"[ 1/1 → 5/4 | note:c s:piano ]",
|
||||
"[ 5/4 → 3/2 | note:a s:piano ]",
|
||||
"[ 3/2 → 7/4 | note:f s:piano ]",
|
||||
"[ 7/4 → 2/1 | note:e s:piano ]",
|
||||
"[ 2/1 → 9/4 | note:c s:piano ]",
|
||||
"[ 9/4 → 5/2 | note:a s:piano ]",
|
||||
"[ 5/2 → 11/4 | note:f s:piano ]",
|
||||
"[ 11/4 → 3/1 | note:e s:piano ]",
|
||||
"[ 3/1 → 13/4 | note:c s:piano ]",
|
||||
"[ 13/4 → 7/2 | note:a s:piano ]",
|
||||
"[ 7/2 → 15/4 | note:f s:piano ]",
|
||||
"[ 15/4 → 4/1 | note:e s:piano ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "keyDown" example index 0 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "lastOf" example index 0 1`] = `
|
||||
@@ -7429,6 +7956,8 @@ exports[`runs examples > example "midikeys" example index 0 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "midikeys" example index 1 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "midikeys" example index 2 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "midin" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
|
||||
@@ -8823,6 +9352,33 @@ exports[`runs examples > example "pickF" example index 1 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "pickF" example index 2 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:c2 s:square pan:0 ]",
|
||||
"[ 1/8 → 1/4 | note:c2 s:square pan:1 ]",
|
||||
"[ 3/8 → 1/2 | note:c2 s:square pan:0 ]",
|
||||
"[ 1/2 → 9/16 | note:d2 s:square ]",
|
||||
"[ 11/16 → 3/4 | note:d2 s:square ]",
|
||||
"[ 7/8 → 15/16 | note:d2 s:square ]",
|
||||
"[ 1/1 → 9/8 | note:d2 s:square cutoff:800 ]",
|
||||
"[ 11/8 → 3/2 | note:d2 s:square cutoff:800 ]",
|
||||
"[ 3/2 → 25/16 | note:d2 s:square ]",
|
||||
"[ 27/16 → 7/4 | note:d2 s:square ]",
|
||||
"[ 15/8 → 31/16 | note:d2 s:square ]",
|
||||
"[ 2/1 → 17/8 | note:c2 s:square pan:0 ]",
|
||||
"[ 17/8 → 9/4 | note:c2 s:square pan:1 ]",
|
||||
"[ 19/8 → 5/2 | note:c2 s:square pan:0 ]",
|
||||
"[ 5/2 → 41/16 | note:d2 s:square ]",
|
||||
"[ 43/16 → 11/4 | note:d2 s:square ]",
|
||||
"[ 23/8 → 47/16 | note:d2 s:square ]",
|
||||
"[ 3/1 → 25/8 | note:d2 s:square cutoff:800 ]",
|
||||
"[ 27/8 → 7/2 | note:d2 s:square cutoff:800 ]",
|
||||
"[ 7/2 → 57/16 | note:d2 s:square ]",
|
||||
"[ 59/16 → 15/4 | note:d2 s:square ]",
|
||||
"[ 31/8 → 63/16 | note:d2 s:square ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "pickmodRestart" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:C3 s:piano ]",
|
||||
@@ -10974,6 +11530,27 @@ 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 ]",
|
||||
@@ -12102,18 +12679,10 @@ exports[`runs examples > example "stepcat" example index 1 1`] = `
|
||||
|
||||
exports[`runs examples > example "stretch" example index 0 1`] = `
|
||||
[
|
||||
"[ (0/1 → 1/3) ⇝ 1/1 | s:gm_flute stretch:1 ]",
|
||||
"[ 0/1 ⇜ (1/3 → 2/3) ⇝ 1/1 | s:gm_flute stretch:2 ]",
|
||||
"[ 0/1 ⇜ (2/3 → 1/1) | s:gm_flute stretch:0.5 ]",
|
||||
"[ (1/1 → 4/3) ⇝ 2/1 | s:gm_flute stretch:1 ]",
|
||||
"[ 1/1 ⇜ (4/3 → 5/3) ⇝ 2/1 | s:gm_flute stretch:2 ]",
|
||||
"[ 1/1 ⇜ (5/3 → 2/1) | s:gm_flute stretch:0.5 ]",
|
||||
"[ (2/1 → 7/3) ⇝ 3/1 | s:gm_flute stretch:1 ]",
|
||||
"[ 2/1 ⇜ (7/3 → 8/3) ⇝ 3/1 | s:gm_flute stretch:2 ]",
|
||||
"[ 2/1 ⇜ (8/3 → 3/1) | s:gm_flute stretch:0.5 ]",
|
||||
"[ (3/1 → 10/3) ⇝ 4/1 | s:gm_flute stretch:1 ]",
|
||||
"[ 3/1 ⇜ (10/3 → 11/3) ⇝ 4/1 | s:gm_flute stretch:2 ]",
|
||||
"[ 3/1 ⇜ (11/3 → 4/1) | s:gm_flute stretch:0.5 ]",
|
||||
"[ 0/1 → 1/1 | s:gm_flute stretch:2 ]",
|
||||
"[ 1/1 → 2/1 | s:gm_flute stretch:1 ]",
|
||||
"[ 2/1 → 3/1 | s:gm_flute stretch:0 ]",
|
||||
"[ 3/1 → 4/1 | s:gm_flute stretch:-2 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -13188,6 +13757,97 @@ exports[`runs examples > example "tri" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "tune" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | freq:{i:0} ]",
|
||||
"[ 1/6 → 1/3 | freq:{i:1} ]",
|
||||
"[ 1/3 → 1/2 | freq:{i:2} ]",
|
||||
"[ 1/2 → 2/3 | freq:{i:3} ]",
|
||||
"[ 2/3 → 5/6 | freq:{i:4} ]",
|
||||
"[ 5/6 → 1/1 | freq:{i:5} ]",
|
||||
"[ 1/1 → 7/6 | freq:{i:0} ]",
|
||||
"[ 7/6 → 4/3 | freq:{i:1} ]",
|
||||
"[ 4/3 → 3/2 | freq:{i:2} ]",
|
||||
"[ 3/2 → 5/3 | freq:{i:3} ]",
|
||||
"[ 5/3 → 11/6 | freq:{i:4} ]",
|
||||
"[ 11/6 → 2/1 | freq:{i:5} ]",
|
||||
"[ 2/1 → 13/6 | freq:{i:0} ]",
|
||||
"[ 13/6 → 7/3 | freq:{i:1} ]",
|
||||
"[ 7/3 → 5/2 | freq:{i:2} ]",
|
||||
"[ 5/2 → 8/3 | freq:{i:3} ]",
|
||||
"[ 8/3 → 17/6 | freq:{i:4} ]",
|
||||
"[ 17/6 → 3/1 | freq:{i:5} ]",
|
||||
"[ 3/1 → 19/6 | freq:{i:0} ]",
|
||||
"[ 19/6 → 10/3 | freq:{i:1} ]",
|
||||
"[ 10/3 → 7/2 | freq:{i:2} ]",
|
||||
"[ 7/2 → 11/3 | freq:{i:3} ]",
|
||||
"[ 11/3 → 23/6 | freq:{i:4} ]",
|
||||
"[ 23/6 → 4/1 | freq:{i:5} ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "tune" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/12 | freq:{i:4} clip:0.5 room:1 ]",
|
||||
"[ 1/12 → 1/6 | freq:{i:8} clip:0.5 room:1 ]",
|
||||
"[ 1/6 → 1/4 | freq:{i:9} clip:0.5 room:1 ]",
|
||||
"[ 1/4 → 1/3 | freq:{i:10} clip:0.5 room:1 ]",
|
||||
"[ 1/2 → 7/12 | freq:{i:5} clip:0.5 room:1 ]",
|
||||
"[ 7/12 → 2/3 | freq:{i:7} clip:0.5 room:1 ]",
|
||||
"[ 2/3 → 3/4 | freq:{i:9} clip:0.5 room:1 ]",
|
||||
"[ 3/4 → 5/6 | freq:{i:11} clip:0.5 room:1 ]",
|
||||
"[ 1/1 → 13/12 | freq:{i:4} clip:0.5 room:1 ]",
|
||||
"[ 13/12 → 7/6 | freq:{i:8} clip:0.5 room:1 ]",
|
||||
"[ 7/6 → 5/4 | freq:{i:9} clip:0.5 room:1 ]",
|
||||
"[ 5/4 → 4/3 | freq:{i:10} clip:0.5 room:1 ]",
|
||||
"[ 3/2 → 19/12 | freq:{i:5} clip:0.5 room:1 ]",
|
||||
"[ 19/12 → 5/3 | freq:{i:7} clip:0.5 room:1 ]",
|
||||
"[ 5/3 → 7/4 | freq:{i:9} clip:0.5 room:1 ]",
|
||||
"[ 7/4 → 11/6 | freq:{i:11} clip:0.5 room:1 ]",
|
||||
"[ 2/1 → 25/12 | freq:{i:4} clip:0.5 room:1 ]",
|
||||
"[ 25/12 → 13/6 | freq:{i:8} clip:0.5 room:1 ]",
|
||||
"[ 13/6 → 9/4 | freq:{i:9} clip:0.5 room:1 ]",
|
||||
"[ 9/4 → 7/3 | freq:{i:10} clip:0.5 room:1 ]",
|
||||
"[ 5/2 → 31/12 | freq:{i:5} clip:0.5 room:1 ]",
|
||||
"[ 31/12 → 8/3 | freq:{i:7} clip:0.5 room:1 ]",
|
||||
"[ 8/3 → 11/4 | freq:{i:9} clip:0.5 room:1 ]",
|
||||
"[ 11/4 → 17/6 | freq:{i:11} clip:0.5 room:1 ]",
|
||||
"[ 3/1 → 37/12 | freq:{i:4} clip:0.5 room:1 ]",
|
||||
"[ 37/12 → 19/6 | freq:{i:8} clip:0.5 room:1 ]",
|
||||
"[ 19/6 → 13/4 | freq:{i:9} clip:0.5 room:1 ]",
|
||||
"[ 13/4 → 10/3 | freq:{i:10} clip:0.5 room:1 ]",
|
||||
"[ 7/2 → 43/12 | freq:{i:5} clip:0.5 room:1 ]",
|
||||
"[ 43/12 → 11/3 | freq:{i:7} clip:0.5 room:1 ]",
|
||||
"[ 11/3 → 15/4 | freq:{i:9} clip:0.5 room:1 ]",
|
||||
"[ 15/4 → 23/6 | freq:{i:11} clip:0.5 room:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "tune" example index 2 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/5 | freq:{i:0} ]",
|
||||
"[ 1/5 → 2/5 | freq:{i:1} ]",
|
||||
"[ 2/5 → 3/5 | freq:{i:2} ]",
|
||||
"[ 3/5 → 4/5 | freq:{i:3} ]",
|
||||
"[ 4/5 → 1/1 | freq:{i:4} ]",
|
||||
"[ 1/1 → 6/5 | freq:{i:0} ]",
|
||||
"[ 6/5 → 7/5 | freq:{i:1} ]",
|
||||
"[ 7/5 → 8/5 | freq:{i:2} ]",
|
||||
"[ 8/5 → 9/5 | freq:{i:3} ]",
|
||||
"[ 9/5 → 2/1 | freq:{i:4} ]",
|
||||
"[ 2/1 → 11/5 | freq:{i:0} ]",
|
||||
"[ 11/5 → 12/5 | freq:{i:1} ]",
|
||||
"[ 12/5 → 13/5 | freq:{i:2} ]",
|
||||
"[ 13/5 → 14/5 | freq:{i:3} ]",
|
||||
"[ 14/5 → 3/1 | freq:{i:4} ]",
|
||||
"[ 3/1 → 16/5 | freq:{i:0} ]",
|
||||
"[ 16/5 → 17/5 | freq:{i:1} ]",
|
||||
"[ 17/5 → 18/5 | freq:{i:2} ]",
|
||||
"[ 18/5 → 19/5 | freq:{i:3} ]",
|
||||
"[ 19/5 → 4/1 | freq:{i:4} ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "undegrade" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:hh ]",
|
||||
@@ -13961,6 +14621,76 @@ exports[`runs examples > example "when" example index 0 1`] = `
|
||||
|
||||
exports[`runs examples > example "whenKey" example index 0 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "withBase" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/12 | freq:220 ]",
|
||||
"[ 1/12 → 1/6 | freq:293.3333333 ]",
|
||||
"[ 1/6 → 1/4 | freq:302.5 ]",
|
||||
"[ 1/4 → 1/3 | freq:320 ]",
|
||||
"[ 1/3 → 1/2 | freq:320 ]",
|
||||
"[ 1/2 → 2/3 | freq:330 ]",
|
||||
"[ 2/3 → 3/4 | freq:330 ]",
|
||||
"[ 3/4 → 5/6 | freq:320 ]",
|
||||
"[ 5/6 → 11/12 | freq:302.5 ]",
|
||||
"[ 11/12 → 1/1 | freq:293.3333333 ]",
|
||||
"[ 1/1 → 13/12 | freq:300 ]",
|
||||
"[ 13/12 → 7/6 | freq:399.99999995454544 ]",
|
||||
"[ 7/6 → 5/4 | freq:412.5 ]",
|
||||
"[ 5/4 → 4/3 | freq:436.3636363636364 ]",
|
||||
"[ 4/3 → 3/2 | freq:436.3636363636364 ]",
|
||||
"[ 3/2 → 5/3 | freq:300 ]",
|
||||
"[ 5/3 → 7/4 | freq:300 ]",
|
||||
"[ 7/4 → 11/6 | freq:290.90909090909093 ]",
|
||||
"[ 11/6 → 23/12 | freq:275 ]",
|
||||
"[ 23/12 → 2/1 | freq:266.66666663636363 ]",
|
||||
"[ 2/1 → 25/12 | freq:220 ]",
|
||||
"[ 25/12 → 13/6 | freq:293.3333333 ]",
|
||||
"[ 13/6 → 9/4 | freq:302.5 ]",
|
||||
"[ 9/4 → 7/3 | freq:320 ]",
|
||||
"[ 7/3 → 5/2 | freq:320 ]",
|
||||
"[ 5/2 → 8/3 | freq:330 ]",
|
||||
"[ 8/3 → 11/4 | freq:330 ]",
|
||||
"[ 11/4 → 17/6 | freq:320 ]",
|
||||
"[ 17/6 → 35/12 | freq:302.5 ]",
|
||||
"[ 35/12 → 3/1 | freq:293.3333333 ]",
|
||||
"[ 3/1 → 37/12 | freq:300 ]",
|
||||
"[ 37/12 → 19/6 | freq:399.99999995454544 ]",
|
||||
"[ 19/6 → 13/4 | freq:412.5 ]",
|
||||
"[ 13/4 → 10/3 | freq:436.3636363636364 ]",
|
||||
"[ 10/3 → 7/2 | freq:436.3636363636364 ]",
|
||||
"[ 7/2 → 11/3 | freq:300 ]",
|
||||
"[ 11/3 → 15/4 | freq:300 ]",
|
||||
"[ 15/4 → 23/6 | freq:290.90909090909093 ]",
|
||||
"[ 23/6 → 47/12 | freq:275 ]",
|
||||
"[ 47/12 → 4/1 | freq:266.66666663636363 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "withBase" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/5 | freq:220 ]",
|
||||
"[ 1/5 → 2/5 | freq:234.66666666666666 ]",
|
||||
"[ 2/5 → 3/5 | freq:247.5 ]",
|
||||
"[ 3/5 → 4/5 | freq:264 ]",
|
||||
"[ 4/5 → 1/1 | freq:275 ]",
|
||||
"[ 1/1 → 6/5 | freq:220 ]",
|
||||
"[ 6/5 → 7/5 | freq:234.66666666666666 ]",
|
||||
"[ 7/5 → 8/5 | freq:247.5 ]",
|
||||
"[ 8/5 → 9/5 | freq:264 ]",
|
||||
"[ 9/5 → 2/1 | freq:275 ]",
|
||||
"[ 2/1 → 11/5 | freq:220 ]",
|
||||
"[ 11/5 → 12/5 | freq:234.66666666666666 ]",
|
||||
"[ 12/5 → 13/5 | freq:247.5 ]",
|
||||
"[ 13/5 → 14/5 | freq:264 ]",
|
||||
"[ 14/5 → 3/1 | freq:275 ]",
|
||||
"[ 3/1 → 16/5 | freq:220 ]",
|
||||
"[ 16/5 → 17/5 | freq:234.66666666666666 ]",
|
||||
"[ 17/5 → 18/5 | freq:247.5 ]",
|
||||
"[ 18/5 → 19/5 | freq:264 ]",
|
||||
"[ 19/5 → 4/1 | freq:275 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "withValue" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/3 | 10 ]",
|
||||
@@ -14100,6 +14830,106 @@ exports[`runs examples > example "wtphaserand" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "xen" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]",
|
||||
"[ 1/3 → 2/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]",
|
||||
"[ 2/3 → 1/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]",
|
||||
"[ 1/1 → 4/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]",
|
||||
"[ 4/3 → 5/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]",
|
||||
"[ 5/3 → 2/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]",
|
||||
"[ 2/1 → 7/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]",
|
||||
"[ 7/3 → 8/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]",
|
||||
"[ 8/3 → 3/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]",
|
||||
"[ 3/1 → 10/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]",
|
||||
"[ 10/3 → 11/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]",
|
||||
"[ 11/3 → 4/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "xen" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]",
|
||||
"[ 1/3 → 2/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]",
|
||||
"[ 2/3 → 1/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]",
|
||||
"[ 1/1 → 4/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]",
|
||||
"[ 4/3 → 5/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]",
|
||||
"[ 5/3 → 2/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]",
|
||||
"[ 2/1 → 7/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]",
|
||||
"[ 7/3 → 8/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]",
|
||||
"[ 8/3 → 3/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]",
|
||||
"[ 3/1 → 10/3 | freq:220 clip:1 s:piano release:0.1 pan:0.5138888888888888 ]",
|
||||
"[ 10/3 → 11/3 | freq:263.0921203 clip:1 s:piano release:0.1 pan:0.5277777777777778 ]",
|
||||
"[ 11/3 → 4/1 | freq:329.0139341 clip:1 s:piano release:0.1 pan:0.5462962962962963 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "xen" example index 2 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | freq:220 ]",
|
||||
"[ 1/6 → 1/3 | freq:275 ]",
|
||||
"[ 1/3 → 1/2 | freq:293.3333333 ]",
|
||||
"[ 1/2 → 2/3 | freq:330 ]",
|
||||
"[ 2/3 → 5/6 | freq:352 ]",
|
||||
"[ 5/6 → 1/1 | freq:440 ]",
|
||||
"[ 1/1 → 7/6 | freq:220 ]",
|
||||
"[ 7/6 → 4/3 | freq:275 ]",
|
||||
"[ 4/3 → 3/2 | freq:293.3333333 ]",
|
||||
"[ 3/2 → 5/3 | freq:330 ]",
|
||||
"[ 5/3 → 11/6 | freq:352 ]",
|
||||
"[ 11/6 → 2/1 | freq:440 ]",
|
||||
"[ 2/1 → 13/6 | freq:220 ]",
|
||||
"[ 13/6 → 7/3 | freq:275 ]",
|
||||
"[ 7/3 → 5/2 | freq:293.3333333 ]",
|
||||
"[ 5/2 → 8/3 | freq:330 ]",
|
||||
"[ 8/3 → 17/6 | freq:352 ]",
|
||||
"[ 17/6 → 3/1 | freq:440 ]",
|
||||
"[ 3/1 → 19/6 | freq:220 ]",
|
||||
"[ 19/6 → 10/3 | freq:275 ]",
|
||||
"[ 10/3 → 7/2 | freq:293.3333333 ]",
|
||||
"[ 7/2 → 11/3 | freq:330 ]",
|
||||
"[ 11/3 → 23/6 | freq:352 ]",
|
||||
"[ 23/6 → 4/1 | freq:440 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "xen" example index 3 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | freq:220 ]",
|
||||
"[ 1/8 → 1/4 | freq:252.7136381 ]",
|
||||
"[ 1/4 → 3/8 | freq:290.2917404 ]",
|
||||
"[ 3/8 → 1/2 | freq:333.4576446 ]",
|
||||
"[ 1/2 → 5/8 | freq:383.0422479 ]",
|
||||
"[ 5/8 → 3/4 | freq:440 ]",
|
||||
"[ 3/4 → 7/8 | freq:505.4272762 ]",
|
||||
"[ 7/8 → 1/1 | freq:580.5834807 ]",
|
||||
"[ 1/1 → 9/8 | freq:220 ]",
|
||||
"[ 9/8 → 5/4 | freq:235.7901618 ]",
|
||||
"[ 5/4 → 11/8 | freq:252.7136381 ]",
|
||||
"[ 11/8 → 3/2 | freq:270.8517709 ]",
|
||||
"[ 3/2 → 13/8 | freq:290.2917404 ]",
|
||||
"[ 13/8 → 7/4 | freq:311.1269837 ]",
|
||||
"[ 7/4 → 15/8 | freq:333.4576446 ]",
|
||||
"[ 15/8 → 2/1 | freq:357.3910544 ]",
|
||||
"[ 2/1 → 17/8 | freq:220 ]",
|
||||
"[ 17/8 → 9/4 | freq:230.404707 ]",
|
||||
"[ 9/4 → 19/8 | freq:241.3014955 ]",
|
||||
"[ 19/8 → 5/2 | freq:252.7136381 ]",
|
||||
"[ 5/2 → 21/8 | freq:264.6655079 ]",
|
||||
"[ 21/8 → 11/4 | freq:277.182631 ]",
|
||||
"[ 11/4 → 23/8 | freq:290.2917404 ]",
|
||||
"[ 23/8 → 3/1 | freq:304.0208336 ]",
|
||||
"[ 3/1 → 25/8 | freq:220 ]",
|
||||
"[ 25/8 → 13/4 | freq:275 ]",
|
||||
"[ 13/4 → 27/8 | freq:293.3333333 ]",
|
||||
"[ 27/8 → 7/2 | freq:330 ]",
|
||||
"[ 7/2 → 29/8 | freq:352 ]",
|
||||
"[ 29/8 → 15/4 | freq:440 ]",
|
||||
"[ 15/4 → 31/8 | freq:550 ]",
|
||||
"[ 31/8 → 4/1 | freq:586.6666667 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "xfade" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:hh gain:0 ]",
|
||||
|
||||
@@ -20,6 +20,7 @@ const skippedExamples = [
|
||||
'accelerationX',
|
||||
'defaultmidimap',
|
||||
'midimaps',
|
||||
'clearScope',
|
||||
'bmod',
|
||||
];
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { mini, m } from '@strudel/mini/mini.mjs';
|
||||
// import euclid from '@strudel/core/euclid.mjs';
|
||||
//import '@strudel/midi/midi.mjs';
|
||||
import * as tonalHelpers from '@strudel/tonal';
|
||||
import * as edoHelpers from '@strudel/edo';
|
||||
import '@strudel/xen/xen.mjs';
|
||||
// import '@strudel/xen/tune.mjs';
|
||||
// import '@strudel/core/euclid.mjs';
|
||||
@@ -126,6 +127,7 @@ const loadSoundfont = () => {};
|
||||
const loadCsound = () => {};
|
||||
const loadCSound = () => {};
|
||||
const loadcsound = () => {};
|
||||
const getDuration = () => {};
|
||||
|
||||
const midin = () => {
|
||||
return (ccNum) => strudel.ref(() => 0); // returns ref with default value 0
|
||||
@@ -144,6 +146,7 @@ evalScope(
|
||||
toneHelpersMocked,
|
||||
uiHelpersMocked,
|
||||
webaudio,
|
||||
edoHelpers,
|
||||
tonalHelpers,
|
||||
gamepadHelpers,
|
||||
/*
|
||||
@@ -168,6 +171,7 @@ evalScope(
|
||||
loadCSound,
|
||||
loadCsound,
|
||||
loadcsound,
|
||||
getDuration,
|
||||
setcps: id,
|
||||
setcpm: id,
|
||||
Clock: {}, // whatever
|
||||
|
||||
@@ -10,8 +10,11 @@ import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
import AstroPWA from '@vite-pwa/astro';
|
||||
|
||||
const site = process.env.STRUDEL_SITE || `https://strudel.cc/`; // root url without a path
|
||||
const base = process.env.STRUDEL_BASE || '/'; // base path of the strudel site
|
||||
import process from 'node:process';
|
||||
|
||||
const site = process.env.SITE_URL || `https://strudel.cc/`; // root url without a path
|
||||
const base = process.env.BASE_PATH || ''; // base path of the strudel site
|
||||
|
||||
const baseNoTrailing = base.endsWith('/') ? base.slice(0, -1) : base;
|
||||
|
||||
// this rehype plugin fixes relative links
|
||||
@@ -75,6 +78,7 @@ export default defineConfig({
|
||||
urlPattern: ({ url }) =>
|
||||
[
|
||||
/^https:\/\/raw\.githubusercontent\.com\/.*/i,
|
||||
/^https:\/\/strudel\.b-cdn\.net\/.*/i,
|
||||
/^https:\/\/freesound\.org\/.*/i,
|
||||
/^https:\/\/cdn\.freesound\.org\/.*/i,
|
||||
/^https:\/\/shabda\.ndre\.gr\/.*/i,
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
"@astrojs/react": "^4.1.6",
|
||||
"@astrojs/rss": "^4.0.11",
|
||||
"@astrojs/tailwind": "^5.1.5",
|
||||
"@codemirror/commands": "catalog:",
|
||||
"@codemirror/language": "catalog:",
|
||||
"@codemirror/lang-javascript": "catalog:",
|
||||
"@codemirror/state": "catalog:",
|
||||
"@codemirror/view": "catalog:",
|
||||
"@docsearch/css": "^3.8.3",
|
||||
"@docsearch/react": "^3.8.3",
|
||||
"@headlessui/react": "^2.2.0",
|
||||
@@ -28,6 +33,7 @@
|
||||
"@strudel/csound": "workspace:*",
|
||||
"@strudel/desktopbridge": "workspace:*",
|
||||
"@strudel/draw": "workspace:*",
|
||||
"@strudel/edo": "workspace:*",
|
||||
"@strudel/gamepad": "workspace:*",
|
||||
"@strudel/hydra": "workspace:*",
|
||||
"@strudel/midi": "workspace:*",
|
||||
|
||||
@@ -98,13 +98,14 @@ export const SIDEBAR: Sidebar = {
|
||||
{ text: 'Random Modifiers', link: 'learn/random-modifiers' },
|
||||
{ text: 'Conditional Modifiers', link: 'learn/conditional-modifiers' },
|
||||
{ text: 'Accumulation', link: 'learn/accumulation' },
|
||||
{ text: 'LFOs', link: 'learn/lfo' },
|
||||
{ text: 'Tonal Functions', link: 'learn/tonal' },
|
||||
{ text: 'Stepwise Functions', link: 'learn/stepwise' },
|
||||
],
|
||||
Understand: [
|
||||
{ text: 'Coding syntax', link: 'learn/code' },
|
||||
{ text: 'Pitch', link: 'understand/pitch' },
|
||||
{ text: 'Xen Harmonic Functions', link: 'learn/xen' },
|
||||
{ text: 'Xenharmonic Functions', link: 'learn/xen' },
|
||||
{ text: 'Cycles', link: 'understand/cycles' },
|
||||
{ text: 'Voicings', link: 'understand/voicings' },
|
||||
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
---
|
||||
title: Low-frequency oscillators (LFO)
|
||||
layout: ../../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
import { MiniRepl } from '../../docs/MiniRepl';
|
||||
import { JsDoc } from '../../docs/JsDoc';
|
||||
|
||||
# Low frequency oscillators (LFO)
|
||||
|
||||
A low frequency oscillator (or short LFO) is a common way on synthesizers to continuously modulate various signals.
|
||||
|
||||
This documentation is an interactive version of glossing's tutorial https://www.youtube.com/watch?v=11frBA9L638
|
||||
|
||||
## Signals vs LFOs
|
||||
|
||||
In Strudel, there are two ways to modulate:
|
||||
|
||||
1. [signals](/learn/signals/) for pattern-level modulation
|
||||
2. `lfo` (this page) for audio-level modulation
|
||||
|
||||
## Applying an LFO
|
||||
|
||||
Here, the LFO will change the frequency of the `saw`. Put a comment like this `//.lfo()` to see and hear how it changes, and remove the comment again.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
s("saw").lfo()
|
||||
.lpf(800)
|
||||
._spectrum({height: 300, width: 800})`}
|
||||
/>
|
||||
|
||||
By default, the LFO will modulate the control parameter which is right before `.lfo()`:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
s("saw")
|
||||
.lpf(800).lfo()
|
||||
._spectrum({height: 300, width: 800})`}
|
||||
/>
|
||||
|
||||
Here, the LFO will modulate the low pass filter `.lpf`.
|
||||
|
||||
## Moving away from the default
|
||||
|
||||
The following sections explain how pass parameters to `.lfo`. Similar to `._spectrum` above, almost all the configuration of `lfo` lives inside a json object, starting with `{` and ending with `}`.
|
||||
All the parameters (except `id`) are written as `key: value` inside and separated by `,`.
|
||||
|
||||
The reference refers to them as `config.key`, i.e. for the following one as `config.control` but you use them like below.
|
||||
|
||||
## Control
|
||||
|
||||
`control` determines which parameter will be modulated. This allows you to place your `lfo` at different places,
|
||||
not necessarily immediately after the controlled parameter.
|
||||
|
||||
Here, we place `lfo` after the `s`, but it modulates the low pass filter.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
s("saw")
|
||||
.lfo({control:'lpf'})
|
||||
.lpf(800)
|
||||
._spectrum({height: 300, width: 800})`}
|
||||
/>
|
||||
You can even influence parameters which are always present, even if you haven't explicitly written them, like `gain`.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
s("saw")
|
||||
.lfo({control:'gain'})
|
||||
.lpf(800)
|
||||
._spectrum({height: 300, width: 800})`}
|
||||
/>
|
||||
|
||||
`control` has an alias `c`.
|
||||
|
||||
## Rate
|
||||
|
||||
`rate` determines how often the `lfo` oscillates per second:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
s("saw")
|
||||
.lfo({c:'gain', rate:"<2 4>"})
|
||||
.lpf(800)
|
||||
._spectrum({height: 300, width: 800})`}
|
||||
/>
|
||||
The alias of `rate` is `r`.{' '}
|
||||
|
||||
## Sync
|
||||
|
||||
Instead of controlling the frequency with `rate` by setting a frequency in Hz, you can use `sync`
|
||||
to snychronize your lfo with your other patterns.
|
||||
|
||||
`sync` expresses this frequency as "times per cycle"
|
||||
|
||||
Try removing the `sync: "<2 4 8 0.5 >"` from the pattern and notice that there is something not in sync.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
$: s("bd*4").bank("TR909").postgain(0.5)
|
||||
$: s("saw")
|
||||
.lpf(800)
|
||||
.lfo({sync: "<2 4 8 0.5 >"})
|
||||
._spectrum({height: 300, width: 800})`}
|
||||
/>
|
||||
|
||||
You can put patterns into the parameters of `lfo` if you want them to change over time, as seen above.
|
||||
|
||||
## Relative Depth
|
||||
|
||||
`.depth` is relative depth, a value of 1 (the default) means that
|
||||
the value goes above and below by half of the value which is being modulated
|
||||
|
||||
E.g. for depth `1`, it modulates the frequency to oscillate between 32 = (64/2) and 96 (= 64 + 64/2).
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
s("saw").freq(64)
|
||||
.lfo({r: 2, depth: "<1 2 3>"})
|
||||
.lpf(800)
|
||||
._spectrum({height: 300, width: 800})`}
|
||||
/>
|
||||
The `freq` is not needed here, as this is the default frequency, it's just for instructive purposes.
|
||||
|
||||
The aliases of `depth` are `dr` and `dep`.
|
||||
|
||||
## Absolute Depth
|
||||
|
||||
`depthabs` controls the absolute modulation depth. For example you can modulate
|
||||
the low pass filter by exactly 250 Hz up and below:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
s("saw")
|
||||
.lpf(800)
|
||||
.lfo({r: 2, depthabs: "250"})`}
|
||||
/>
|
||||
|
||||
The alias of `depthabs` is `da`.
|
||||
|
||||
## DC offset
|
||||
|
||||
If you don't want to go up or down by the same amount with your modulation, then you can shift the center of the modulation with `dcoffset`.
|
||||
The default value is -0.5, which is the middle point between:
|
||||
"dcoffset = 0: All modulations increase the control parameter (or keep it constant)"
|
||||
"dcoffset = -1: All modulations decrease the control parameter (or keep it constant)"
|
||||
|
||||
Other values will work as well.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
s("saw")
|
||||
.lpf(600)
|
||||
.lfo({r: 2, da: "200", dcoffset: "<-0.5 -1 -0.5 0>"})`}
|
||||
/>
|
||||
The marvellous alias of `dcoffset` is `dc`.
|
||||
|
||||
## Shape
|
||||
|
||||
You can change the shape of the modulation with `shape`. The default is `triangle`, but other shapes are available too:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
s("saw")
|
||||
.lpf(800)
|
||||
.lfo({r: 2, shape: "<triangle sine ramp saw square>"}) `}
|
||||
/>
|
||||
You can add `._spectrum()` to see the shape of the modulations in the spectrum.
|
||||
|
||||
You can also get these shapes by using numbers:
|
||||
|
||||
| Shape | number |
|
||||
| -------- | ------ |
|
||||
| triangle | 0 |
|
||||
| sine | 1 |
|
||||
| ramp | 2 |
|
||||
| saw | 3 |
|
||||
| square | 4 |
|
||||
|
||||
this way, you can use a function like `irand` to generate numbers:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
s("saw")
|
||||
.lpf(800)
|
||||
.lfo({r: 2, shape: irand(4)}) `}
|
||||
/>
|
||||
|
||||
The alias of `shape` is `sh`.
|
||||
|
||||
## Skewing some shapes
|
||||
|
||||
You can influence some of the shapes (`triangle` and `square`) in more detail.
|
||||
|
||||
The default skew is 0.5, it does different things for these two.
|
||||
|
||||
For `triangle`, it skews the top of the triangle to the left or right,
|
||||
where 0 makes it look like `saw` and 1 makes it look like `ramp`.
|
||||
|
||||
For `square`, the skew influences the pulse width (see reference for `pulse` and `.pw`):
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
s("saw")
|
||||
.lpf(800)
|
||||
.lfo({r: 2, sh: "<triangle square>/5",
|
||||
skew: "<0 0.25 0.5 0.75 1>"}) `}
|
||||
/>
|
||||
The alias of `skew` is `sk`.{' '}
|
||||
|
||||
## Curve
|
||||
|
||||
You can change the curves of your lfo and can make it more intense.
|
||||
The default value is 1. Writing numbers greater than one can make it more intense,
|
||||
numbers between 0 and 1 will make it less intense.
|
||||
This will raise the lfo to the power of curve, so larger numbers (such as 10) can have very unexpected results.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
s("saw")
|
||||
.lpf(800)
|
||||
.lfo({r: 2, sh: "<triangle>", curve: "<1.3 1 0.75 1>"}) `}
|
||||
/>
|
||||
## Referencing your lfos with id
|
||||
|
||||
All the lfos are numbered from the first starting with 0, the second having an `id` of 1 and so on.
|
||||
|
||||
You can refer to this in a later call if you want to modify a specific `lfo`.
|
||||
|
||||
The `id` is outside of the config json (which is different from the other parameters of `lfo()`
|
||||
|
||||
Try out how the sound changes when you replace the 0 with a 1.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
s("saw").lfo().lpf(800)
|
||||
.lfo({s: "<4 8 0.5>"})
|
||||
.sometimes(x => x.lfo({dr: "4"},0))`}
|
||||
/>
|
||||
|
||||
You can also name your lfos and refer to them by name, using the `id` parameter
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
s("saw").lfo({}, "lfo_freq_saw").lpf(800)
|
||||
.lfo({s: "<4 8 0.5>"}, "lfo_lpf")
|
||||
.sometimes(x => x.lfo({dr: "4"},"<lfo_lpf lfo_freq_saw>"))`}
|
||||
/>
|
||||
|
||||
## FX index
|
||||
|
||||
If you are using `FX()` to reorder your effects, you dont need to write your lfos inside the `FX`,
|
||||
but instead can refer to them by their FX index (starting with 0)
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
s("saw").FX(
|
||||
distort(3),
|
||||
gain(0.3), // this has the fx index 1
|
||||
lpf(400)
|
||||
).lfo({s: 16, dr:2, c:"gain", fxi: 1})`}
|
||||
/>
|
||||
|
||||
## Modulating other LFOs with Sub-control
|
||||
|
||||
LFOs can modulate other lfos and will modulate their frequency (given by `r` or `s`):
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
s("saw").lpf(400).gain(0.8)
|
||||
.lfo({s: 16, dr:2, c:"gain"})
|
||||
.lfo({s: 0.3, dc:-1, dr: 0.8})`}
|
||||
/>
|
||||
To modulate other parameters of the first lfo (like `skew`, `depth` and so on), we can specify this with `subControl`or
|
||||
its alias `sc`
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`
|
||||
s("saw").lpf(400).gain(0.8)
|
||||
.lfo({s: 4, dr:2, c:"gain"})
|
||||
.lfo({s: 0.3, sc: "skew"})`}
|
||||
/>
|
||||
@@ -1,22 +1,26 @@
|
||||
---
|
||||
title: Xen Harmonic Functions
|
||||
title: Xenharmonic Functions
|
||||
layout: ../../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
import { MiniRepl } from '../../docs/MiniRepl';
|
||||
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.
|
||||
|
||||
### tune(scale)
|
||||
|
||||
{/* TODO (maybe): combine jsdoc things in tune.mjs with here */}
|
||||
|
||||
<JsDoc client:idle name="tune" h={0} />
|
||||
|
||||
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`
|
||||
|
||||
@@ -26,7 +30,7 @@ You can set your root to be a particular note with `getFreq`
|
||||
|
||||
<MiniRepl
|
||||
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'))
|
||||
.freq().clip(.5).room(1)`}
|
||||
/>
|
||||
@@ -35,7 +39,7 @@ Some tunings become more pronounced with a longer reverb decay:
|
||||
|
||||
<MiniRepl
|
||||
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'))
|
||||
.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
|
||||
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))
|
||||
.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
|
||||
client:idle
|
||||
tune={`"<[0 3 1 -] [-1 4 2 8]> ~ ~,<-4 -5>"
|
||||
.transpose(4)
|
||||
tune={`i("<[0 3 1 -] [-1 4 2 8]> ~ ~,<-4 -5>".add(4))
|
||||
.tune("iraq")
|
||||
.mul("<c3 d3 c#3>".fmap(getFreq))
|
||||
.freq().clip(.5).room(1).rfade(9)`}
|
||||
@@ -67,7 +70,7 @@ Take the `sanza` tuning:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"4 5 6 7 8 9".tune("sanza")
|
||||
tune={`i("4 5 6 7 8 9").tune("sanza")
|
||||
.mul(getFreq('c3'))
|
||||
.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.
|
||||
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.
|
||||
As such, some tunings are much better strummed, with the subtle clash of the detuned notes actually making the sound much more magical:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"[0 1 2 3 4 5 6]@0.3 -"
|
||||
.transpose("<2 5 8 1>")
|
||||
tune={`i("[0 1 2 3 4 5 6]@0.3 -"
|
||||
.add("<2 5 8 1>"))
|
||||
.tune("sanza")
|
||||
.mul(getFreq('c3')).freq()
|
||||
.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.
|
||||
|
||||
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
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
title: Movement with Strudel
|
||||
layout: ../../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
import { MiniRepl } from '@src/docs/MiniRepl';
|
||||
import Box from '@components/Box.astro';
|
||||
import QA from '@components/QA';
|
||||
|
||||
# Controlling motors with Strudel
|
||||
|
||||
Strudel is mainly made for making music, but it's possible to pattern other things with it, including motors.
|
||||
|
||||
We're going to use a microcontroller for this, called an "[Inventor 2040W](https://shop.pimoroni.com/products/inventor-2040-w)", which is
|
||||
a [Pico W](https://shop.pimoroni.com/products/inventor-2040-w?variant=40053063155795) with extra ports added including some for controlling motors.
|
||||
|
||||

|
||||
|
||||
## Technical details
|
||||
|
||||
Feel free to gloss over these!
|
||||
|
||||
- The Inventor 2040W connects to the internet wirelessly, and it can power from a battery or USB. Hopefully the batteries last!
|
||||
- It's running [some code](https://github.com/patternclub/alpacalab/blob/main/course/main.py) that listens for messages using an "Internet of Things" network protocol (called MQTT). When it receives a message, it moves a motor.
|
||||
- It connects to a small server (running software called 'mosquitto') on Alex's laptop.
|
||||
- Strudel can send these messages instead of triggering sounds - that's how we use it to pattern movement.
|
||||
|
||||
## First movement
|
||||
|
||||
Let's get a motor running!
|
||||
|
||||
1. Note the letter drawn on a label on the back of the microcontroller.
|
||||
|
||||
2. Plug a battery into your microcontroller.
|
||||
|
||||
3. Plug a motor into 'servo' (not motor) plug numbered 1, with the yellow (lightest) cable closest to the '1', and the brown (darkest) cable outward
|
||||
|
||||
4. Run the below to set up some values, changing the `x` in 'robot('x')` to the letter on your microcontroller.
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
$: move("-60 80").motor("0").robot('x');
|
||||
`}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
|
||||
If you refresh the page, you'll need to change the letter to match your robot again.
|
||||
|
||||
If your motor starts moving unexpectedly, someone else might have put your letter in by mistake!
|
||||
|
||||
Note that in the above, we start counting motors from '0', so motor 1 on the board is motor 0 in the code.
|
||||
|
||||
</Box>
|
||||
|
||||
## Patterning movement
|
||||
|
||||
Many strudel features for playing with sound patterns will work when
|
||||
playing with motor patterns. Try playing with the mininotation in the
|
||||
`move` command:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
$: move("-10 0 10 [20 30]*2").motor("0").slow(2).robot('x')
|
||||
`}
|
||||
/>
|
||||
|
||||
The move instructions are in the range from -90 to 90.
|
||||
|
||||
<box>
|
||||
If your motors stop working at some point, and your code looks right, try pressing the 'reset' button on the
|
||||
microcontroller.
|
||||
</box>
|
||||
|
||||
It's possible to make smooth movements based on different 'waveforms', for example a smooth sinewave:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
$: move(sine.range(-30, 30).segment(16)).motor("0").slow(2).robot('x');
|
||||
`}
|
||||
/>
|
||||
|
||||
The movement is still quite jerky, because the 'segment' command is
|
||||
only taking 16 positions from the sinewave. Try increasing it to 32 or 64. It's best not too much higher than that, as the microcontroller
|
||||
might get overwhelmed with a backlog of instructions!
|
||||
|
||||
<box>
|
||||
Try replacing `sine` with other waveforms: `saw` (sawtooth wave), `tri` (triangular wave) are good, and there is also
|
||||
`rand` (random wave) and `perlin` (a kind of smoothed-out randomness).
|
||||
</box>
|
||||
|
||||
## Patterning more than one motor
|
||||
|
||||
You can pattern the `motor` command separately from the `move` one:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
$: move("-10 0 10 [20 30]*2").motor("0 1").slow(2).robot('x');
|
||||
`}
|
||||
/>
|
||||
|
||||
Alternatively, you can pattern two motors in separate patterns. The below sends the same pattern for the first two motors, but with the second one running slower:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
|
||||
$: move("-10 0 10 [20 30]\*2").motor("0").slow(2).robot('x');
|
||||
|
||||
$: move("-10 0 10 [20 30]\*2").motor("1").slow(3).robot('x');
|
||||
|
||||
`}
|
||||
/>
|
||||
+11
-11
@@ -10,50 +10,50 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#code .cm-scroller {
|
||||
.code-container .cm-scroller {
|
||||
padding-top: 10px !important;
|
||||
height: 100%;
|
||||
font-family: inherit;
|
||||
}
|
||||
#code .cm-content {
|
||||
.code-container .cm-content {
|
||||
padding-bottom: 50vh;
|
||||
}
|
||||
#code .cm-line > * {
|
||||
.code-container .cm-line > * {
|
||||
background: var(--lineBackground);
|
||||
}
|
||||
|
||||
#code .cm-editor {
|
||||
.code-container .cm-editor {
|
||||
background-color: transparent !important;
|
||||
height: 100%;
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
#code .cm-theme {
|
||||
.code-container .cm-theme {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#code .cm-theme-light {
|
||||
.code-container .cm-theme-light {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#code .cm-cursorLayer {
|
||||
.code-container .cm-cursorLayer {
|
||||
animation-name: inherit !important;
|
||||
}
|
||||
|
||||
#code .cm-cursor {
|
||||
.code-container .cm-cursor {
|
||||
border-left: 2px solid currentcolor !important;
|
||||
}
|
||||
|
||||
#code .cm-foldGutter {
|
||||
.code-container .cm-foldGutter {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#code .cm-focused {
|
||||
.code-container .cm-focused {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#code .cm-matchingBracket {
|
||||
.code-container .cm-matchingBracket {
|
||||
text-decoration: underline 0.18rem;
|
||||
text-underline-offset: 0.22rem;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@ export function Code(Props) {
|
||||
|
||||
return (
|
||||
<section
|
||||
className={'text-gray-100 cursor-text pb-0 overflow-auto grow z-10'}
|
||||
id="code"
|
||||
className={'code-container text-gray-100 cursor-text pb-0 overflow-auto grow z-10'}
|
||||
ref={(el) => {
|
||||
containerRef.current = el;
|
||||
if (!editorRef.current) {
|
||||
|
||||
@@ -8,31 +8,3 @@ export function ActionButton({ children, label, labelIsHidden, className, ...but
|
||||
</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 { useSettings, storePrebakeScript } from '../../../settings.mjs';
|
||||
import { SpecialActionInput } from '../button/action-button';
|
||||
import { confirmDialog, SETTING_CHANGE_RELOAD_MSG } from '@src/repl/util.mjs';
|
||||
import { storePrebakeScript } from '../../../settings.mjs';
|
||||
import { confirmDialog } from '@src/repl/util.mjs';
|
||||
|
||||
async function importScript(script) {
|
||||
async function importScript(script, updateEditor) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(script);
|
||||
|
||||
reader.onload = () => {
|
||||
const text = reader.result;
|
||||
storePrebakeScript(text);
|
||||
updateEditor && updateEditor(text);
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
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 (
|
||||
<SpecialActionInput
|
||||
type="file"
|
||||
label="import prebake script"
|
||||
accept=".strudel"
|
||||
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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label className="space-x-1 inline-flex items-center cursor-pointer">
|
||||
<input type="file" accept=".strudel,.js" className="sr-only peer" onChange={handleChange} />
|
||||
<span className="inline-flex items-center peer-hover:opacity-50 text-xs max-w-[300px]">import</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,25 @@ export function LogoButton({ context, isEmbedded }) {
|
||||
|
||||
export function MainPanel({ context, isEmbedded = false, className }) {
|
||||
const { isZen, isButtonRowHidden, fontFamily } = useSettings();
|
||||
let loc = window.location;
|
||||
let ver = 'unofficial';
|
||||
let hot = false;
|
||||
let b = loc.hostname.match(/^(.+)\.(strudel)/);
|
||||
if (/(strudel.cc$)/.test(loc.hostname)) {
|
||||
// if there's no text before 'strudel', it's warm, otherwise use the text before strudel
|
||||
ver = b ? b[1] : 'warm';
|
||||
} else {
|
||||
// match both versions of localhost
|
||||
if (/(localhost)|(127.0.0.1)/.test(loc.hostname)) ver = 'dev';
|
||||
}
|
||||
let pr = ver.match(/pr-([0-9]+)/);
|
||||
if (pr) {
|
||||
pr = pr[1];
|
||||
ver = `hot: ${pr}`;
|
||||
hot = true;
|
||||
pr = `https://codeberg.org/uzu/strudel/pulls/${pr}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
id="header"
|
||||
@@ -72,6 +91,13 @@ export function MainPanel({ context, isEmbedded = false, className }) {
|
||||
<div className="space-x-2 flex items-baseline">
|
||||
<span className="hidden sm:block">strudel</span>
|
||||
<span className="text-sm font-medium hidden sm:block">REPL</span>
|
||||
{!hot ? (
|
||||
<span className="text-sm font-medium hidden sm:block">({ver})</span>
|
||||
) : (
|
||||
<a className="hover:opacity-50" href={pr} target="_blank">
|
||||
<span className="text-sm font-medium hidden sm:block">({ver})</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</h1>
|
||||
|
||||
@@ -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 { PrebakeCodeMirror } from '../../../repl/prebakeCodeMirror.mjs';
|
||||
import { confirmAndReloadPage, isUdels } from '../../util.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
||||
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
||||
import { confirmDialog } from '../../util.mjs';
|
||||
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
|
||||
import { SpecialActionButton } from '../button/action-button.jsx';
|
||||
import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
|
||||
|
||||
function cx(...classes) {
|
||||
// : Array<string | undefined>
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
import { ActionButton } from '../button/action-button.jsx';
|
||||
import { exportScript, ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import cx from '@src/cx.mjs';
|
||||
|
||||
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';
|
||||
@@ -123,7 +121,7 @@ const fontFamilyOptions = {
|
||||
galactico: 'galactico',
|
||||
};
|
||||
|
||||
export function SettingsTab({ started }) {
|
||||
function MainSettingsContent({ started }) {
|
||||
const {
|
||||
theme,
|
||||
keybindings,
|
||||
@@ -149,12 +147,12 @@ export function SettingsTab({ started }) {
|
||||
isTabIndentationEnabled,
|
||||
isMultiCursorEnabled,
|
||||
patternAutoStart,
|
||||
includePrebakeScriptInShare,
|
||||
isBlockBasedEvalEnabled,
|
||||
} = useSettings();
|
||||
const shouldAlwaysSync = isUdels();
|
||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||
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 && (
|
||||
<FormItem label="Audio Output Device">
|
||||
<AudioDeviceSelector
|
||||
@@ -232,20 +230,12 @@ export function SettingsTab({ started }) {
|
||||
/>
|
||||
</FormItem>
|
||||
</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">
|
||||
<ButtonGroup
|
||||
value={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>
|
||||
</FormItem>
|
||||
<FormItem label="Panel Position">
|
||||
@@ -306,6 +296,11 @@ export function SettingsTab({ started }) {
|
||||
onChange={(cbEvent) => settingsMap.setKey('isMultiCursorEnabled', cbEvent.target.checked)}
|
||||
value={isMultiCursorEnabled}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Enable Block-based Evaluation (EXPERIMENTAL)"
|
||||
onChange={(cbEvent) => settingsMap.setKey('isBlockBasedEvalEnabled', cbEvent.target.checked)}
|
||||
value={isBlockBasedEvalEnabled}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Enable flashing on evaluation"
|
||||
onChange={(cbEvent) => settingsMap.setKey('isFlashEnabled', cbEvent.target.checked)}
|
||||
@@ -340,7 +335,7 @@ export function SettingsTab({ started }) {
|
||||
</FormItem>
|
||||
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
|
||||
<FormItem label="Reset Settings">
|
||||
<SpecialActionButton
|
||||
<ActionButton
|
||||
onClick={() => {
|
||||
confirmDialog('Sure?').then((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
|
||||
</SpecialActionButton>
|
||||
</ActionButton>
|
||||
</FormItem>
|
||||
</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 { getDrawContext } from '@strudel/draw';
|
||||
import { evaluate, transpiler } from '@strudel/transpiler';
|
||||
import { transpiler, evaluate } from '@strudel/transpiler';
|
||||
import {
|
||||
getAudioContextCurrentTime,
|
||||
renderPatternAudio,
|
||||
@@ -71,6 +71,7 @@ export function useReplContext() {
|
||||
const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput;
|
||||
const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds;
|
||||
const init = useCallback(() => {
|
||||
setActivePattern(getViewingPatternData().id);
|
||||
const drawTime = [-2, 2];
|
||||
const drawContext = getDrawContext();
|
||||
const editor = new StrudelMirror({
|
||||
@@ -86,12 +87,12 @@ export function useReplContext() {
|
||||
pattern: silence,
|
||||
drawTime,
|
||||
drawContext,
|
||||
prebake: async () =>
|
||||
Promise.all([modulesLoading, presets]).then(() => {
|
||||
if (prebakeScript?.length) {
|
||||
return evaluate(prebakeScript ?? '');
|
||||
}
|
||||
}),
|
||||
prebake: async () => {
|
||||
await Promise.all([modulesLoading, presets]);
|
||||
if (prebakeScript) {
|
||||
return evaluate(prebakeScript, { addReturn: false });
|
||||
}
|
||||
},
|
||||
onUpdateState: (state) => {
|
||||
setReplState({ ...state });
|
||||
},
|
||||
@@ -106,17 +107,24 @@ export function useReplContext() {
|
||||
//post to iframe parent (like Udels) if it exists...
|
||||
window.parent?.postMessage(code);
|
||||
|
||||
setLatestCode(code);
|
||||
window.location.hash = '#' + code2hash(code);
|
||||
setDocumentTitle(code);
|
||||
// Get the full buffer content from the editor instead of just the evaluated block
|
||||
const fullBufferCode = editorRef.current?.code || code;
|
||||
setLatestCode(fullBufferCode);
|
||||
|
||||
try {
|
||||
window.location.hash = '#' + code2hash(fullBufferCode);
|
||||
} catch (e) {
|
||||
console.warn('[useReplContext] Failed to update hash:', e.message);
|
||||
}
|
||||
setDocumentTitle(fullBufferCode);
|
||||
const viewingPatternData = getViewingPatternData();
|
||||
setVersionDefaultsFrom(code);
|
||||
const data = { ...viewingPatternData, code };
|
||||
setVersionDefaultsFrom(fullBufferCode);
|
||||
const data = { ...viewingPatternData, code: fullBufferCode };
|
||||
let id = data.id;
|
||||
const isExamplePattern = viewingPatternData.collection !== userPattern.collection;
|
||||
|
||||
if (isExamplePattern) {
|
||||
const codeHasChanged = code !== viewingPatternData.code;
|
||||
const codeHasChanged = fullBufferCode !== viewingPatternData.code;
|
||||
if (codeHasChanged) {
|
||||
// fork example
|
||||
const newPattern = userPattern.duplicate(data);
|
||||
@@ -168,9 +176,8 @@ export function useReplContext() {
|
||||
useEffect(() => {
|
||||
let editorSettings = {};
|
||||
Object.keys(defaultSettings).forEach((key) => {
|
||||
if (Object.prototype.hasOwnProperty.call(_settings, key)) {
|
||||
editorSettings[key] = _settings[key];
|
||||
}
|
||||
// Don't use hasOwnProperty - nanostore uses proxies so values may not be own properties
|
||||
editorSettings[key] = _settings[key];
|
||||
});
|
||||
editorRef.current?.updateSettings(editorSettings);
|
||||
}, [_settings]);
|
||||
|
||||
@@ -70,6 +70,7 @@ export function loadModules() {
|
||||
let modules = [
|
||||
import('@strudel/core'),
|
||||
import('@strudel/draw'),
|
||||
import('@strudel/edo'),
|
||||
import('@strudel/tonal'),
|
||||
import('@strudel/mini'),
|
||||
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 function confirmAndReloadPage(onSuccess) {
|
||||
confirmDialog(SETTING_CHANGE_RELOAD_MSG).then((r) => {
|
||||
if (r == true) {
|
||||
|
||||
@@ -18,6 +18,20 @@ export const soundFilterType = {
|
||||
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 = {
|
||||
activeFooter: 'intro',
|
||||
keybindings: 'codemirror',
|
||||
@@ -33,6 +47,7 @@ export const defaultSettings = {
|
||||
isPatternHighlightingEnabled: true,
|
||||
isTabIndentationEnabled: false,
|
||||
isMultiCursorEnabled: false,
|
||||
isBlockBasedEvalEnabled: false,
|
||||
theme: 'strudelTheme',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 18,
|
||||
@@ -46,13 +61,14 @@ export const defaultSettings = {
|
||||
isPanelPinned: false,
|
||||
isPanelOpen: true,
|
||||
userPatterns: '{}',
|
||||
prebakeScript: '',
|
||||
prebakeScript: initialPrebakeScript,
|
||||
audioEngineTarget: audioEngineTargets.webaudio,
|
||||
isButtonRowHidden: false,
|
||||
isCSSAnimationDisabled: false,
|
||||
maxPolyphony: 128,
|
||||
multiChannelOrbits: false,
|
||||
includePrebakeScriptInShare: true,
|
||||
settingsTab: 'settings',
|
||||
};
|
||||
|
||||
let search = null;
|
||||
@@ -89,6 +105,7 @@ export const $settings = computed(settingsMap, (state) => {
|
||||
isSyncEnabled: isUdels() ? true : parseBoolean(state.isSyncEnabled),
|
||||
isTabIndentationEnabled: parseBoolean(state.isTabIndentationEnabled),
|
||||
isMultiCursorEnabled: parseBoolean(state.isMultiCursorEnabled),
|
||||
isBlockBasedEvalEnabled: parseBoolean(state.isBlockBasedEvalEnabled),
|
||||
fontSize: Number(state.fontSize),
|
||||
panelPosition: state.activeFooter !== '' && !isUdels() ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is!
|
||||
isPanelPinned: parseBoolean(state.isPanelPinned),
|
||||
@@ -113,6 +130,7 @@ export function useSettings() {
|
||||
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
|
||||
export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', 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);
|
||||
|
||||
|
||||
@@ -207,9 +207,10 @@ export async function exportPatterns() {
|
||||
const userPatterns = userPattern.getAll();
|
||||
const blob = new Blob([JSON.stringify(userPatterns)], { type: 'application/json' });
|
||||
const downloadLink = document.createElement('a');
|
||||
const prefix = window.location.hostname.split('.').join('_');
|
||||
downloadLink.href = window.URL.createObjectURL(blob);
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
downloadLink.download = `strudel_patterns_${date}.json`;
|
||||
downloadLink.download = `${prefix}_patterns_${date}.json`;
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
|
||||
Reference in New Issue
Block a user