mirror of
https://codeberg.org/uzu/strudel
synced 2026-10-04 08:21:59 -04:00
Compare commits
189 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 98e99e8166 | |||
| 8f81463b9c | |||
| 8120fc72ab | |||
| d7fb9fe992 | |||
| 37d524e639 | |||
| d65b24db8f | |||
| f1dd209749 | |||
| d6f0b863f0 | |||
| 5dbbc7b9e1 | |||
| beb16f4040 | |||
| f2be5e4f5e | |||
| c7db8eaad6 | |||
| 723cd5da04 | |||
| 98d19a6643 | |||
| ef106b356f | |||
| ee460dc980 | |||
| d5d6bc6f34 | |||
| d9336aba3a | |||
| 263c9c0253 | |||
| 6ecdc739b4 | |||
| f67e81a03f | |||
| 5cf7571b77 | |||
| b2aeaa56c1 | |||
| 9e3792f6a6 | |||
| f8ed9c3a7c | |||
| c5b42b5555 | |||
| 75f58bc875 | |||
| 3b037deb10 | |||
| 362b2df390 | |||
| 20d925c10c | |||
| 91f37accdd | |||
| 07f01c237f | |||
| 76fa8264cd | |||
| 08dc5e2c5c | |||
| bd54c8003a | |||
| 9e946f1d14 | |||
| f1a541caec | |||
| 47cdf4561d | |||
| 6c8b707d2e | |||
| adcbf9240e | |||
| f4b3fdaacd | |||
| 9bf3249e82 | |||
| 0a0afcf73f | |||
| 6ebf506070 | |||
| eafa2f17ec | |||
| 429682735d | |||
| 04232370c2 | |||
| 4484ee4e80 | |||
| 57598f2aa1 | |||
| ebc25467c0 | |||
| a866cb2189 | |||
| 890d69b23e | |||
| 8e187abf24 | |||
| 2d014b47c8 | |||
| daafee7527 | |||
| c9522a4e49 | |||
| 25d1179985 | |||
| c150752372 | |||
| e248bf85f3 | |||
| 6c51c0261a | |||
| deefde7b50 | |||
| d4f63e8de3 | |||
| 19cb3dedc2 | |||
| a7c3407da7 | |||
| 6870b04fb2 | |||
| 2b2646a768 | |||
| 97ef5bc335 | |||
| 85e6d436ef | |||
| d3e2b7c7b4 | |||
| def1738259 | |||
| 0feeb1e701 | |||
| 813cc6c190 | |||
| 62d4f84e69 | |||
| 49a1e11cd8 | |||
| 941c97da0d | |||
| 95a9d301a1 | |||
| e38811d325 | |||
| 475f17ddfd | |||
| bd68c6a0a7 | |||
| 2635716c87 | |||
| 58f956b57e | |||
| 0ae2c120b3 | |||
| 05a43ef687 | |||
| f21aeb55bd | |||
| 422b03699a | |||
| cb046a3548 | |||
| d83139980b | |||
| 0c61cd7670 | |||
| 13b11efa15 | |||
| fe0be6bc53 | |||
| 4747fab4ed | |||
| f73b395648 | |||
| 9c47efd3cf | |||
| f4066089f4 | |||
| 0e81987c4e | |||
| 23dbe53013 | |||
| 0e4d0484a3 | |||
| 37bf2e4dc7 | |||
| e69761397e | |||
| 10c33f24f5 | |||
| ea0ec7edb9 | |||
| f900642359 | |||
| 4cc63b8b01 | |||
| 26832c0fe7 | |||
| 01abc8ae79 | |||
| 84df683794 | |||
| ca1aca0c00 | |||
| 896ec4c643 | |||
| 1723125099 | |||
| 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 | |||
| 23f6ff9bc4 | |||
| 3e1d02cb3d | |||
| 8a72f21257 | |||
| d5a9019220 | |||
| d0d2459e14 | |||
| a60a54147c | |||
| 6707c7893b | |||
| 7b58bbf12b | |||
| b2cfd3dc3d | |||
| 9f2237e11d | |||
| 7c22b29493 | |||
| c77675c492 | |||
| 5197af1aa4 | |||
| 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
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Build and Deploy to beta (warm.strudel.cc)
|
name: Build and Deploy to warm (strudel.cc)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -37,4 +37,4 @@ jobs:
|
|||||||
apt update && apt install -y rsync
|
apt update && apt install -y rsync
|
||||||
mkdir ~/.ssh
|
mkdir ~/.ssh
|
||||||
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
|
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]
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
@@ -34,4 +34,4 @@ jobs:
|
|||||||
apt update && apt install -y rsync
|
apt update && apt install -y rsync
|
||||||
mkdir ~/.ssh
|
mkdir ~/.ssh
|
||||||
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
|
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
|
||||||
+11
-1
@@ -52,7 +52,17 @@ Strudel is a project handmade by humans, with thought and nuance.
|
|||||||
|
|
||||||
If you have used LLMs (so called 'AI'), please detail that in the pull request. We are still developing our response to the onslaught of LLM technology, but for practical and legal reasons are currently not accepting wholly LLM-generated code. We are also not accepting PRs that add LLM features to strudel itself.
|
If you have used LLMs (so called 'AI'), please detail that in the pull request. We are still developing our response to the onslaught of LLM technology, but for practical and legal reasons are currently not accepting wholly LLM-generated code. We are also not accepting PRs that add LLM features to strudel itself.
|
||||||
|
|
||||||
There are #llm-chat and #llm-share channels on our discord. Please do not discuss or share LLM-related things outside of those channels.
|
There are #llm-chat and #llm-share channels on [our discord](https://discord.com/invite/HGEdXmRkzT). Please do not discuss or share LLM-related things outside of those channels.
|
||||||
|
|
||||||
|
## Creating and sharing a new project using strudel
|
||||||
|
|
||||||
|
Strudel is free/open source software, and we are also happy to see people making use of it within the following terms.
|
||||||
|
|
||||||
|
Please don't use 'strudel' in the name of your project, so people don't assume it's official strudel project. (If you'd like it to be an official strudel project, please check in with the community, e.g. on [the discord](https://discord.com/invite/HGEdXmRkzT).)
|
||||||
|
|
||||||
|
Please respect our AGPL license, which e.g. requires you to share/link to the source code of strudel, any modifications you've made to it, and the source code for the rest of your project if it integrates with strudel. You are also required to maintain Strudel's copyright notices in the source code, and include Strudel's copyright notice in your user interface. This is an ad-hoc summary - please [refer to the license](https://codeberg.org/uzu/strudel/src/branch/main/LICENSE) for full details.
|
||||||
|
|
||||||
|
You are also encouraged to connect with the community and understand our aims and values.
|
||||||
|
|
||||||
## Report a Bug
|
## Report a Bug
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const editor = new StrudelMirror({
|
|||||||
import('@strudel/core'),
|
import('@strudel/core'),
|
||||||
import('@strudel/draw'),
|
import('@strudel/draw'),
|
||||||
import('@strudel/mini'),
|
import('@strudel/mini'),
|
||||||
|
import('@strudel/edo'),
|
||||||
import('@strudel/tonal'),
|
import('@strudel/tonal'),
|
||||||
import('@strudel/webaudio'),
|
import('@strudel/webaudio'),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@strudel/codemirror": "workspace:*",
|
"@strudel/codemirror": "workspace:*",
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
|
"@strudel/edo": "workspace:*",
|
||||||
"@strudel/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
"@strudel/soundfonts": "workspace:*",
|
"@strudel/soundfonts": "workspace:*",
|
||||||
"@strudel/tonal": "workspace:*",
|
"@strudel/tonal": "workspace:*",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export * from './packages/core/index.mjs';
|
|||||||
export * from './packages/csound/index.mjs';
|
export * from './packages/csound/index.mjs';
|
||||||
export * from './packages/desktopbridge/index.mjs';
|
export * from './packages/desktopbridge/index.mjs';
|
||||||
export * from './packages/draw/index.mjs';
|
export * from './packages/draw/index.mjs';
|
||||||
|
export * from './packages/edo/index.mjs';
|
||||||
export * from './packages/embed/index.mjs';
|
export * from './packages/embed/index.mjs';
|
||||||
export * from './packages/hydra/index.mjs';
|
export * from './packages/hydra/index.mjs';
|
||||||
export * from './packages/midi/index.mjs';
|
export * from './packages/midi/index.mjs';
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"homepage": "https://strudel.cc",
|
"homepage": "https://strudel.cc",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/edo": "workspace:*",
|
||||||
"@strudel/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
"@strudel/tonal": "workspace:*",
|
"@strudel/tonal": "workspace:*",
|
||||||
"@strudel/transpiler": "workspace:*",
|
"@strudel/transpiler": "workspace:*",
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
keymap,
|
keymap,
|
||||||
lineNumbers,
|
lineNumbers,
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import {repeatCharKeymap} from './repeatcharacter.mjs';
|
|
||||||
import { persistentAtom } from '@nanostores/persistent';
|
import { persistentAtom } from '@nanostores/persistent';
|
||||||
import { logger, registerControl, repl } from '@strudel/core';
|
import { logger, registerControl, repl } from '@strudel/core';
|
||||||
import { cleanupDraw, cleanupDrawContext, Drawer } from '@strudel/draw';
|
import { cleanupDraw, cleanupDrawContext, Drawer } from '@strudel/draw';
|
||||||
@@ -21,16 +20,11 @@ import { evalBlock } from './block_utilities.mjs';
|
|||||||
import { flash, isFlashEnabled } from './flash.mjs';
|
import { flash, isFlashEnabled } from './flash.mjs';
|
||||||
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
||||||
import { keybindings } from './keybindings.mjs';
|
import { keybindings } from './keybindings.mjs';
|
||||||
|
import { jumpToCharacter } from './labelJump.mjs';
|
||||||
import { getSliderWidgets, sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
import { getSliderWidgets, sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||||
import { activateTheme, initTheme, theme } from './themes.mjs';
|
import { activateTheme, initTheme, theme } from './themes.mjs';
|
||||||
import { isTooltipEnabled } from './tooltip.mjs';
|
import { isTooltipEnabled } from './tooltip.mjs';
|
||||||
import { getActiveWidgets, updateWidgets, widgetPlugin } from './widget.mjs';
|
import { getActiveWidgets, updateWidgets, widgetPlugin } from './widget.mjs';
|
||||||
import {
|
|
||||||
deleteAllInlineBeforeCharacter,
|
|
||||||
InsertCharBeforeChar,
|
|
||||||
jumpToCharacter,
|
|
||||||
jumpToNextCharacter,
|
|
||||||
} from './labelJump.mjs';
|
|
||||||
|
|
||||||
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
||||||
|
|
||||||
@@ -50,9 +44,9 @@ export const extensions = {
|
|||||||
isMultiCursorEnabled: (on) =>
|
isMultiCursorEnabled: (on) =>
|
||||||
on
|
on
|
||||||
? [
|
? [
|
||||||
EditorState.allowMultipleSelections.of(true),
|
EditorState.allowMultipleSelections.of(true),
|
||||||
EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey),
|
EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey),
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
};
|
};
|
||||||
export const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
export const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
||||||
@@ -81,10 +75,6 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS
|
|||||||
decode: JSON.parse,
|
decode: JSON.parse,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ANON_LABEL = '$';
|
|
||||||
const SOLO_LABEL = 'S';
|
|
||||||
const MUTE_LABEL = '_';
|
|
||||||
|
|
||||||
// https://codemirror.net/docs/guide/
|
// https://codemirror.net/docs/guide/
|
||||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo, strudelMirror }) {
|
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo, strudelMirror }) {
|
||||||
const settings = codemirrorSettings.get();
|
const settings = codemirrorSettings.get();
|
||||||
@@ -112,7 +102,6 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
|||||||
syntaxHighlighting(defaultHighlightStyle),
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
EditorView.updateListener.of((v) => onChange(v)),
|
EditorView.updateListener.of((v) => onChange(v)),
|
||||||
drawSelection({ cursorBlinkRate: 0 }),
|
drawSelection({ cursorBlinkRate: 0 }),
|
||||||
repeatCharKeymap,
|
|
||||||
Prec.highest(
|
Prec.highest(
|
||||||
keymap.of([
|
keymap.of([
|
||||||
{
|
{
|
||||||
@@ -149,74 +138,12 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Alt-w',
|
key: 'Alt-w',
|
||||||
run: (view) => jumpToNextCharacter(view, ANON_LABEL, 1),
|
run: (view) => jumpToCharacter(view, '$', 1),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Alt-q',
|
key: 'Alt-q',
|
||||||
run: (view) => {
|
run: (view) => jumpToCharacter(view, '$', -1),
|
||||||
return jumpToNextCharacter(view, ANON_LABEL, -1);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
// clear all muted
|
|
||||||
{
|
|
||||||
key: `Alt-Ctrl-0`,
|
|
||||||
run: (view) => {
|
|
||||||
return deleteAllInlineBeforeCharacter(view, MUTE_LABEL + ANON_LABEL);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// clear all solod
|
|
||||||
{
|
|
||||||
key: `Alt-Shift-0`,
|
|
||||||
run: (view) => {
|
|
||||||
return deleteAllInlineBeforeCharacter(view, SOLO_LABEL + ANON_LABEL);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// clear all solo and mute
|
|
||||||
{
|
|
||||||
key: `Ctrl-Shift-0`,
|
|
||||||
run: (view) => {
|
|
||||||
return deleteAllInlineBeforeCharacter(view, ANON_LABEL);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...Array.from({ length: 9 }).map((_, i) => {
|
|
||||||
let num = i + 1;
|
|
||||||
return {
|
|
||||||
key: `Alt-${num}`,
|
|
||||||
run: (view) => {
|
|
||||||
return jumpToCharacter(view, ANON_LABEL, i);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
// handle solo toggles 1-9
|
|
||||||
...Array.from({ length: 9 }).map((_, i) => {
|
|
||||||
let num = i + 1;
|
|
||||||
return {
|
|
||||||
key: `Alt-Shift-${num}`,
|
|
||||||
run: (view) => {
|
|
||||||
return InsertCharBeforeChar(view, ANON_LABEL, SOLO_LABEL, i);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
// handle mute toggles 1-9
|
|
||||||
...Array.from({ length: 9 }).map((_, i) => {
|
|
||||||
let num = i + 1;
|
|
||||||
return {
|
|
||||||
key: `Alt-Ctrl-${num}`,
|
|
||||||
run: (view) => {
|
|
||||||
return InsertCharBeforeChar(view, ANON_LABEL, MUTE_LABEL, i);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
// Handle clearing mutes and solos 1-9
|
|
||||||
...Array.from({ length: 9 }).map((_, i) => {
|
|
||||||
let num = i + 1;
|
|
||||||
return {
|
|
||||||
key: `Ctrl-Shift-${num}`,
|
|
||||||
run: (view) => {
|
|
||||||
return InsertCharBeforeChar(view, ANON_LABEL, '', i);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
/* {
|
/* {
|
||||||
key: 'Ctrl-Shift-.',
|
key: 'Ctrl-Shift-.',
|
||||||
run: () => (onPanic ? onPanic() : onStop?.()),
|
run: () => (onPanic ? onPanic() : onStop?.()),
|
||||||
@@ -303,7 +230,7 @@ export class StrudelMirror {
|
|||||||
afterEval: (options) => {
|
afterEval: (options) => {
|
||||||
// remember for when highlighting is toggled on
|
// remember for when highlighting is toggled on
|
||||||
this.miniLocations = options.meta?.miniLocations || [];
|
this.miniLocations = options.meta?.miniLocations || [];
|
||||||
this.widgets = options.meta?.widgets;
|
this.widgets = options.meta?.widgets || [];
|
||||||
|
|
||||||
const sliders = this.widgets.filter((w) => w.type === 'slider');
|
const sliders = this.widgets.filter((w) => w.type === 'slider');
|
||||||
const widgets = this.widgets.filter((w) => w.type !== 'slider');
|
const widgets = this.widgets.filter((w) => w.type !== 'slider');
|
||||||
@@ -366,6 +293,9 @@ export class StrudelMirror {
|
|||||||
// Handle global evaluation requests (e.g., from Vim :w)
|
// Handle global evaluation requests (e.g., from Vim :w)
|
||||||
this.onEvaluateRequest = (e) => {
|
this.onEvaluateRequest = (e) => {
|
||||||
try {
|
try {
|
||||||
|
if (e.detail.view !== this.editor) {
|
||||||
|
return; // ignore events from other editors
|
||||||
|
}
|
||||||
// Evaluate current editor on repl-evaluate
|
// Evaluate current editor on repl-evaluate
|
||||||
logger('[repl] evaluate via event');
|
logger('[repl] evaluate via event');
|
||||||
this.evaluate();
|
this.evaluate();
|
||||||
@@ -380,6 +310,9 @@ export class StrudelMirror {
|
|||||||
// Toggle comments requested from Vim (gc)
|
// Toggle comments requested from Vim (gc)
|
||||||
this.onToggleComment = (e) => {
|
this.onToggleComment = (e) => {
|
||||||
try {
|
try {
|
||||||
|
if (e.detail.view !== this.editor) {
|
||||||
|
return; // ignore events from other editors
|
||||||
|
}
|
||||||
// Honor selections; toggleLineComment handles both selections and
|
// Honor selections; toggleLineComment handles both selections and
|
||||||
// single line
|
// single line
|
||||||
toggleLineComment(this.editor);
|
toggleLineComment(this.editor);
|
||||||
@@ -420,6 +353,9 @@ export class StrudelMirror {
|
|||||||
// Listen for global stop requests (e.g., from Vim :q)
|
// Listen for global stop requests (e.g., from Vim :q)
|
||||||
onStopRequest = (e) => {
|
onStopRequest = (e) => {
|
||||||
try {
|
try {
|
||||||
|
if (e.detail.view !== this.editor) {
|
||||||
|
return; // ignore events from other editors
|
||||||
|
}
|
||||||
this.stop();
|
this.stop();
|
||||||
e?.cancelable && e.preventDefault?.();
|
e?.cancelable && e.preventDefault?.();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -508,14 +444,16 @@ export class StrudelMirror {
|
|||||||
this.setFontSize(value);
|
this.setFontSize(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setCode(code) {
|
replaceCode(code, from, to) {
|
||||||
const changes = {
|
const changes = { from, to, insert: code };
|
||||||
from: 0,
|
|
||||||
to: this.editor.state.doc.length,
|
|
||||||
insert: code,
|
|
||||||
};
|
|
||||||
this.editor.dispatch({ changes });
|
this.editor.dispatch({ changes });
|
||||||
}
|
}
|
||||||
|
insertCode(code, position) {
|
||||||
|
this.replaceCode(code, position, position);
|
||||||
|
}
|
||||||
|
setCode(code) {
|
||||||
|
this.replaceCode(code, 0, this.editor.state.doc.length);
|
||||||
|
}
|
||||||
// used for debugging but could serve other purposes
|
// used for debugging but could serve other purposes
|
||||||
getActiveWidgets() {
|
getActiveWidgets() {
|
||||||
return getActiveWidgets(this.editor);
|
return getActiveWidgets(this.editor);
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ export * from './slider.mjs';
|
|||||||
export * from './themes.mjs';
|
export * from './themes.mjs';
|
||||||
export * from './widget.mjs';
|
export * from './widget.mjs';
|
||||||
export { Vim } from './keybindings.mjs';
|
export { Vim } from './keybindings.mjs';
|
||||||
|
export * from './basicSetup.mjs';
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ try {
|
|||||||
// internal actions and works with current selections/visual mode.
|
// internal actions and works with current selections/visual mode.
|
||||||
try {
|
try {
|
||||||
Vim.defineAction('strudelToggleComment', (cm) => {
|
Vim.defineAction('strudelToggleComment', (cm) => {
|
||||||
const view = cm?.view || cm;
|
const view = cm.cm6;
|
||||||
try {
|
try {
|
||||||
const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
|
const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
|
||||||
document.dispatchEvent(ev);
|
document.dispatchEvent(ev);
|
||||||
@@ -112,15 +112,16 @@ try {
|
|||||||
|
|
||||||
// :q to pause/stop
|
// :q to pause/stop
|
||||||
Vim.defineEx('quit', 'q', (cm) => {
|
Vim.defineEx('quit', 'q', (cm) => {
|
||||||
const view = cm?.view || cm;
|
const view = cm.cm6;
|
||||||
|
logger('[vim] :q — stopping repl');
|
||||||
replStop(view);
|
replStop(view);
|
||||||
});
|
});
|
||||||
|
|
||||||
// :w to evaluate
|
// :w to evaluate
|
||||||
Vim.defineEx('write', 'w', (cm) => {
|
Vim.defineEx('write', 'w', (cm) => {
|
||||||
const view = cm?.view || cm; // CM6 Vim passes either an object with view or the view itself
|
const view = cm.cm6;
|
||||||
try {
|
try {
|
||||||
view?.focus?.();
|
view.focus?.();
|
||||||
// Let the app know this came from Vim :w
|
// Let the app know this came from Vim :w
|
||||||
try {
|
try {
|
||||||
logger('[vim] :w — evaluating code');
|
logger('[vim] :w — evaluating code');
|
||||||
|
|||||||
@@ -1,54 +1,18 @@
|
|||||||
import { EditorSelection } from '@codemirror/state';
|
import { EditorSelection } from '@codemirror/state';
|
||||||
import { SearchCursor } from '@codemirror/search';
|
import { SearchCursor } from '@codemirror/search';
|
||||||
import { EditorView } from '@codemirror/view';
|
|
||||||
import { syntaxTree } from '@codemirror/language';
|
|
||||||
|
|
||||||
/**
|
export function jumpToCharacter(view, character, direction = 1) {
|
||||||
* gets all of the positions of a character in a document, excluding commented out lines
|
|
||||||
* @param { EditorState} state
|
|
||||||
* @param {String} character
|
|
||||||
* @returns {number[]}
|
|
||||||
*/
|
|
||||||
function getCharacterPositions(state, character) {
|
|
||||||
const cursor = new SearchCursor(state.doc, character);
|
|
||||||
|
|
||||||
const characterPositions = [];
|
|
||||||
while (!cursor.next().done) {
|
|
||||||
|
|
||||||
const linestartpos = state.doc.lineAt(cursor.value.to).from
|
|
||||||
if (!isLineCommentedOut(state, linestartpos)) {
|
|
||||||
|
|
||||||
characterPositions.push(cursor.value.to);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return characterPositions;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLineCommentedOut(state, pos) {
|
|
||||||
|
|
||||||
const line = state.doc.lineAt(pos);
|
|
||||||
// remove white space
|
|
||||||
pos = line.from + line.text.search(/\S/)
|
|
||||||
|
|
||||||
const tree = syntaxTree(state);
|
|
||||||
const node = tree.resolveInner(pos, 1)
|
|
||||||
return node.name.includes("Comment")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* jump to the next character in a document
|
|
||||||
* @param {EditorView} view
|
|
||||||
* @param {String} character
|
|
||||||
* @param {number} direction 0 or 1
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
export function jumpToNextCharacter(view, character, direction = 1) {
|
|
||||||
const { state, dispatch } = view;
|
const { state, dispatch } = view;
|
||||||
const pos = state.selection.main.head;
|
const pos = state.selection.main.head;
|
||||||
|
const cursor = new SearchCursor(state.doc, character);
|
||||||
|
|
||||||
|
let characterPositions = [];
|
||||||
let jumpPos;
|
let jumpPos;
|
||||||
const characterPositions = getCharacterPositions(state, character);
|
while (!cursor.next().done) {
|
||||||
|
characterPositions.push(cursor.value.to);
|
||||||
|
}
|
||||||
if (!characterPositions.length) {
|
if (!characterPositions.length) {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
if (direction > 0) {
|
if (direction > 0) {
|
||||||
jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience
|
jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience
|
||||||
@@ -57,97 +21,11 @@ export function jumpToNextCharacter(view, character, direction = 1) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (jumpPos == null) {
|
if (jumpPos == null) {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
const selection = EditorSelection.cursor(jumpPos - 1);
|
|
||||||
dispatch({
|
dispatch({
|
||||||
selection,
|
selection: EditorSelection.cursor(jumpPos - 1),
|
||||||
effects: EditorView.scrollIntoView(
|
scrollIntoView: true,
|
||||||
selection.head,
|
|
||||||
{ y: "start" }
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {EditorView} view
|
|
||||||
* @param {String} character
|
|
||||||
* @param {number} index the instance of the character
|
|
||||||
* @returns {true}
|
|
||||||
*/
|
|
||||||
export function jumpToCharacter(view, character, index) {
|
|
||||||
const { state, dispatch } = view;
|
|
||||||
const characterPositions = getCharacterPositions(state, character);
|
|
||||||
const pos = characterPositions.at(index) ?? characterPositions.at(-1);
|
|
||||||
if (pos == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selection = EditorSelection.cursor(pos - 1);
|
|
||||||
dispatch({
|
|
||||||
selection,
|
|
||||||
effects: EditorView.scrollIntoView(
|
|
||||||
selection.head,
|
|
||||||
{ y: "start" }
|
|
||||||
)
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {EditorView} view
|
|
||||||
* @param {String} character
|
|
||||||
* @returns {true}
|
|
||||||
*/
|
|
||||||
export function deleteAllInlineBeforeCharacter(view, character) {
|
|
||||||
const { state, dispatch } = view;
|
|
||||||
const characterPositions = getCharacterPositions(state, character);
|
|
||||||
|
|
||||||
const changes = [];
|
|
||||||
characterPositions.forEach((pos) => {
|
|
||||||
const line = state.doc.lineAt(pos);
|
|
||||||
if (state.doc.sliceString(line.from, line.from + 2) === COMMENT_STRING) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
changes.push({
|
|
||||||
from: line.from,
|
|
||||||
to: pos - 1,
|
|
||||||
insert: '',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
dispatch({ changes });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {EditorView} view
|
|
||||||
* @param {String} character
|
|
||||||
* @param {String} character2
|
|
||||||
* @param {number} index
|
|
||||||
* @returns {true}
|
|
||||||
*/
|
|
||||||
export function InsertCharBeforeChar(view, character, character2, index) {
|
|
||||||
const { state, dispatch } = view;
|
|
||||||
|
|
||||||
const changes = [];
|
|
||||||
const characterPositions = getCharacterPositions(state, character);
|
|
||||||
const labelpos = characterPositions.at(index) ?? characterPositions.at(-1);
|
|
||||||
const line = state.doc.lineAt(labelpos);
|
|
||||||
|
|
||||||
//delete preceeding characters
|
|
||||||
changes.push({
|
|
||||||
from: line.from,
|
|
||||||
to: labelpos - 1,
|
|
||||||
insert: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
changes.push({
|
|
||||||
insert: character2,
|
|
||||||
from: line.from,
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch({ changes });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -32,13 +32,13 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.18.4",
|
"@codemirror/autocomplete": "catalog:",
|
||||||
"@codemirror/commands": "^6.8.0",
|
"@codemirror/commands": "catalog:",
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "catalog:",
|
||||||
"@codemirror/language": "^6.10.8",
|
"@codemirror/language": "catalog:",
|
||||||
"@codemirror/search": "^6.5.8",
|
"@codemirror/search": "catalog:",
|
||||||
"@codemirror/state": "^6.5.1",
|
"@codemirror/state": "catalog:",
|
||||||
"@codemirror/view": "^6.36.2",
|
"@codemirror/view": "catalog:",
|
||||||
"@lezer/highlight": "^1.2.1",
|
"@lezer/highlight": "^1.2.1",
|
||||||
"@nanostores/persistent": "^0.10.2",
|
"@nanostores/persistent": "^0.10.2",
|
||||||
"@replit/codemirror-emacs": "^6.1.0",
|
"@replit/codemirror-emacs": "^6.1.0",
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import { Compartment } from "@codemirror/state";
|
|
||||||
import { keymap } from "@codemirror/view";
|
|
||||||
|
|
||||||
const repeatMode = new Compartment();
|
|
||||||
|
|
||||||
function repeatPreviousChar(times) {
|
|
||||||
return ({ state, dispatch }) => {
|
|
||||||
const { from, empty } = state.selection.main;
|
|
||||||
if (!empty || from === 0) return false;
|
|
||||||
|
|
||||||
const prevChar = state.doc.sliceString(from - 1, from);
|
|
||||||
const text = Array(times).fill(prevChar).join(" ");
|
|
||||||
|
|
||||||
dispatch(state.update({
|
|
||||||
changes: {
|
|
||||||
from,
|
|
||||||
insert: " " + text
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function exitRepeatMode(view) {
|
|
||||||
view.dispatch({
|
|
||||||
effects: repeatMode.reconfigure([])
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const digitBindings = Array.from({ length: 9 }, (_, i) => ({
|
|
||||||
key: String(i + 1),
|
|
||||||
run(view) {
|
|
||||||
repeatPreviousChar(i + 1)(view);
|
|
||||||
exitRepeatMode(view);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const repeatCharKeymap = [
|
|
||||||
|
|
||||||
repeatMode.of([]),
|
|
||||||
|
|
||||||
keymap.of([
|
|
||||||
{
|
|
||||||
key: "Alt-r",
|
|
||||||
run(view) {
|
|
||||||
view.dispatch({
|
|
||||||
effects: repeatMode.reconfigure(keymap.of(digitBindings))
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
|
||||||
];
|
|
||||||
@@ -1906,13 +1906,19 @@ export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback',
|
|||||||
*/
|
*/
|
||||||
export const { delayspeed } = registerControl('delayspeed');
|
export const { delayspeed } = registerControl('delayspeed');
|
||||||
|
|
||||||
/*
|
/**
|
||||||
|
* Sets the time of the delay effect in seconds.
|
||||||
|
*
|
||||||
* @name delaytime
|
* @name delaytime
|
||||||
* @tags orbit, superdough, supradough
|
* @tags orbit, superdough, supradough
|
||||||
* @param {number | Pattern} delaytime sets the time of the delay effect.
|
* @param {number | Pattern} delay in seconds
|
||||||
* @synonyms delayt, dt
|
* @synonyms delayt, dt
|
||||||
* @example
|
* @example
|
||||||
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
|
* 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');
|
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
|
||||||
|
|
||||||
@@ -2346,11 +2352,18 @@ export const { offset } = registerControl('offset');
|
|||||||
**/
|
**/
|
||||||
export const { octaves } = registerControl('octaves');
|
export const { octaves } = registerControl('octaves');
|
||||||
/**
|
/**
|
||||||
* Remove anchor note from the voicing. Useful for melody harmonization
|
* How the voicing is aligned to the anchor
|
||||||
|
* - `below`: top note <= anchor
|
||||||
|
* - `duck`: top note <= anchor, anchor excluded
|
||||||
|
* - `above`: bottom note >= anchor
|
||||||
|
* - `root`: bottom note is the lowest root of the chord >= anchor
|
||||||
|
*
|
||||||
|
* - `oldabove` : old (buggy) behavior of above, kept for legacy reason
|
||||||
|
* - `oldroot` : old (buggy) behavior of root, kept for legacy reason
|
||||||
*
|
*
|
||||||
* @name mode
|
* @name mode
|
||||||
* @tags tonal
|
* @tags tonal
|
||||||
* @param {string | Pattern} modeName one of {below | above | duck | root}
|
* @param {string | Pattern} modeName one of {below | above | duck | root | oldabove | oldroot}
|
||||||
* @example
|
* @example
|
||||||
* mode("<below above duck root>").chord("C").voicing()
|
* mode("<below above duck root>").chord("C").voicing()
|
||||||
*
|
*
|
||||||
@@ -2576,13 +2589,17 @@ export const { compressorRelease } = registerControl('compressorRelease');
|
|||||||
export const { speed } = registerControl('speed');
|
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
|
* @name stretch
|
||||||
* @tags pitch, samples
|
* @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
|
* @example
|
||||||
* s("gm_flute").stretch("1 2 .5")
|
* s("gm_flute").stretch("<2 1 0 -2>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { stretch } = registerControl('stretch');
|
export const { stretch } = registerControl('stretch');
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { errorLogger, logger } from './logger.mjs';
|
|||||||
export class Cyclist {
|
export class Cyclist {
|
||||||
constructor({
|
constructor({
|
||||||
interval,
|
interval,
|
||||||
onTrigger,
|
onSchedule,
|
||||||
onToggle,
|
onToggle,
|
||||||
onError,
|
onError,
|
||||||
getTime,
|
getTime,
|
||||||
@@ -63,12 +63,7 @@ export class Cyclist {
|
|||||||
if (hap.hasOnset()) {
|
if (hap.hasOnset()) {
|
||||||
const targetTime =
|
const targetTime =
|
||||||
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
|
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
|
||||||
const duration = hap.duration / this.cps;
|
onSchedule?.(hap, targetTime, this.cps);
|
||||||
// the following line is dumb and only here for backwards compatibility
|
|
||||||
// see https://codeberg.org/uzu/strudel/pulls/1004
|
|
||||||
const deadline = targetTime - phase;
|
|
||||||
// this onTrigger has another signature
|
|
||||||
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
|
|
||||||
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
|
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
|
||||||
this.cps = hap.value.cps;
|
this.cps = hap.value.cps;
|
||||||
this.num_ticks_since_cps_change = 0;
|
this.num_ticks_since_cps_change = 0;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { logger } from './logger.mjs';
|
|||||||
import { ClockCollator, cycleToSeconds } from './util.mjs';
|
import { ClockCollator, cycleToSeconds } from './util.mjs';
|
||||||
|
|
||||||
export class NeoCyclist {
|
export class NeoCyclist {
|
||||||
constructor({ onTrigger, onToggle, getTime }) {
|
constructor({ onSchedule, onToggle, getTime }) {
|
||||||
this.started = false;
|
this.started = false;
|
||||||
this.cps = 0.5;
|
this.cps = 0.5;
|
||||||
this.getTime = getTime; // get absolute time
|
this.getTime = getTime; // get absolute time
|
||||||
@@ -43,8 +43,7 @@ export class NeoCyclist {
|
|||||||
if (hap.hasOnset()) {
|
if (hap.hasOnset()) {
|
||||||
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
|
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
|
||||||
const targetTime = timeUntilTrigger + currentTime + this.latency;
|
const targetTime = timeUntilTrigger + currentTime + this.latency;
|
||||||
const duration = cycleToSeconds(hap.duration, this.cps);
|
onSchedule?.(hap, targetTime, this.cps);
|
||||||
onTrigger?.(hap, 0, duration, this.cps, targetTime);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+182
-37
@@ -27,6 +27,7 @@ import {
|
|||||||
import drawLine from './drawLine.mjs';
|
import drawLine from './drawLine.mjs';
|
||||||
import { errorLogger, logger } from './logger.mjs';
|
import { errorLogger, logger } from './logger.mjs';
|
||||||
import { strudelScope } from './evaluate.mjs';
|
import { strudelScope } from './evaluate.mjs';
|
||||||
|
import { getTime } from './schedulerState.mjs';
|
||||||
|
|
||||||
let stringParser;
|
let stringParser;
|
||||||
|
|
||||||
@@ -872,23 +873,41 @@ export class Pattern {
|
|||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// Context methods - ones that deal with metadata
|
// Context methods - ones that deal with metadata
|
||||||
|
|
||||||
onTrigger(onTrigger, dominant = true) {
|
/**
|
||||||
|
* Attaches a function that is called for each hap onset.
|
||||||
|
* @name onSchedule
|
||||||
|
* @memberof Pattern
|
||||||
|
* @example
|
||||||
|
* s("bd sd").onSchedule((hap, time, cps) => {
|
||||||
|
* console.log('hap scheduled', time, cps);
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
onSchedule(onSchedule, dominant = true) {
|
||||||
return this.withHap((hap) =>
|
return this.withHap((hap) =>
|
||||||
hap.setContext({
|
hap.setContext({
|
||||||
...hap.context,
|
...hap.context,
|
||||||
onTrigger: (...args) => {
|
onSchedule: (scheduleHap, t, cps) => {
|
||||||
// run previously set trigger, if it exists
|
// run previously set trigger, if it exists
|
||||||
hap.context.onTrigger?.(...args);
|
hap.context.onSchedule?.(scheduleHap, t, cps);
|
||||||
onTrigger(...args);
|
onSchedule(scheduleHap, t, cps);
|
||||||
},
|
},
|
||||||
// if dominantTrigger is set to true, the default output (webaudio) will be disabled
|
// if dominantScheduler is set to true, the default output (webaudio) will be disabled
|
||||||
// when using multiple triggers, you cannot flip this flag to false again!
|
// when using multiple triggers, you cannot flip this flag to false again!
|
||||||
// example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same
|
// example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same
|
||||||
dominantTrigger: hap.context.dominantTrigger || dominant,
|
dominantScheduler: hap.context.dominantScheduler || dominant,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// legacy:
|
||||||
|
onTrigger(onTrigger, dominant = true) {
|
||||||
|
console.warn('onTrigger is deprecated.. use onSchedule(hap, time, cps) instead');
|
||||||
|
return this.onSchedule((hap, t, cps) => {
|
||||||
|
const currentTime = getTime();
|
||||||
|
return onTrigger(hap, currentTime, cps, t);
|
||||||
|
}, dominant);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the content of the current event to the console (visible in the side menu).
|
* Writes the content of the current event to the console (visible in the side menu).
|
||||||
* @tags visualization
|
* @tags visualization
|
||||||
@@ -898,7 +917,7 @@ export class Pattern {
|
|||||||
* s("bd sd").log()
|
* s("bd sd").log()
|
||||||
*/
|
*/
|
||||||
log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) {
|
log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) {
|
||||||
return this.onTrigger((...args) => {
|
return this.onSchedule((...args) => {
|
||||||
logger(func(...args), undefined, getData(...args));
|
logger(func(...args), undefined, getData(...args));
|
||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
@@ -1002,7 +1021,7 @@ export const arpWith = register('arpWith', (func, pat) => {
|
|||||||
* */
|
* */
|
||||||
export const arp = register(
|
export const arp = register(
|
||||||
'arp',
|
'arp',
|
||||||
(indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[i % haps.length])),
|
(indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[_mod(i, haps.length)])),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1056,7 +1075,55 @@ function _composeOp(a, b, func) {
|
|||||||
|
|
||||||
// pattern composers
|
// pattern composers
|
||||||
const 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],
|
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],
|
keep: [(a) => a],
|
||||||
keepif: [(a, b) => (b ? a : undefined)],
|
keepif: [(a, b) => (b ? a : undefined)],
|
||||||
|
|
||||||
@@ -1110,7 +1177,6 @@ const COMPOSERS = {
|
|||||||
div: [numeralArgs((a, b) => a / b)],
|
div: [numeralArgs((a, b) => a / b)],
|
||||||
mod: [numeralArgs(_mod)],
|
mod: [numeralArgs(_mod)],
|
||||||
pow: [numeralArgs(Math.pow)],
|
pow: [numeralArgs(Math.pow)],
|
||||||
log2: [numeralArgs(Math.log2)],
|
|
||||||
band: [numeralArgs((a, b) => a & b)],
|
band: [numeralArgs((a, b) => a & b)],
|
||||||
bor: [numeralArgs((a, b) => a | b)],
|
bor: [numeralArgs((a, b) => a | b)],
|
||||||
bxor: [numeralArgs((a, b) => a ^ b)],
|
bxor: [numeralArgs((a, b) => a ^ b)],
|
||||||
@@ -1482,7 +1548,9 @@ export function slowcat(...pats) {
|
|||||||
// Array test here is to avoid infinite recursions..
|
// Array test here is to avoid infinite recursions..
|
||||||
pats = pats.map((pat) => (Array.isArray(pat) ? fastcat(...pat) : reify(pat)));
|
pats = pats.map((pat) => (Array.isArray(pat) ? fastcat(...pat) : reify(pat)));
|
||||||
|
|
||||||
if (pats.length == 1) {
|
if (!pats.length) {
|
||||||
|
return silence;
|
||||||
|
} else if (pats.length == 1) {
|
||||||
return pats[0];
|
return pats[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1490,10 +1558,6 @@ export function slowcat(...pats) {
|
|||||||
const span = state.span;
|
const span = state.span;
|
||||||
const pat_n = _mod(span.begin.sam(), pats.length);
|
const pat_n = _mod(span.begin.sam(), pats.length);
|
||||||
const pat = pats[pat_n];
|
const pat = pats[pat_n];
|
||||||
if (!pat) {
|
|
||||||
// pat_n can be negative, if the span is in the past..
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
// A bit of maths to make sure that cycles from constituent patterns aren't skipped.
|
// A bit of maths to make sure that cycles from constituent patterns aren't skipped.
|
||||||
// For example if three patterns are slowcat-ed, the fourth cycle of the result should
|
// For example if three patterns are slowcat-ed, the fourth cycle of the result should
|
||||||
// be the second (rather than fourth) cycle from the first pattern.
|
// be the second (rather than fourth) cycle from the first pattern.
|
||||||
@@ -1510,11 +1574,14 @@ export function slowcat(...pats) {
|
|||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
*/
|
*/
|
||||||
export function slowcatPrime(...pats) {
|
export function slowcatPrime(...pats) {
|
||||||
|
if (!pats.length) {
|
||||||
|
return silence;
|
||||||
|
}
|
||||||
pats = pats.map(reify);
|
pats = pats.map(reify);
|
||||||
const query = function (state) {
|
const query = function (state) {
|
||||||
const pat_n = Math.floor(state.span.begin) % pats.length;
|
const pat_n = _mod(Math.floor(state.span.begin), pats.length);
|
||||||
const pat = pats[pat_n]; // can be undefined for same cases e.g. /#cHVyZSg0MikKICAuZXZlcnkoMyxhZGQoNykpCiAgLmxhdGUoLjUp
|
const pat = pats[pat_n];
|
||||||
return pat?.query(state) || [];
|
return pat.query(state);
|
||||||
};
|
};
|
||||||
return new Pattern(query).splitQueries();
|
return new Pattern(query).splitQueries();
|
||||||
}
|
}
|
||||||
@@ -1694,6 +1761,12 @@ export const func = curry((a, b) => reify(b).func(a));
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export function register(name, func, patternify = true, preserveSteps = false, join = (x) => x.innerJoin()) {
|
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)) {
|
if (Array.isArray(name)) {
|
||||||
const result = {};
|
const result = {};
|
||||||
for (const name_item of name) {
|
for (const name_item of name) {
|
||||||
@@ -1816,6 +1889,8 @@ export const floor = register('floor', function (pat) {
|
|||||||
return pat.asNumber().fmap((v) => Math.floor(v));
|
return pat.asNumber().fmap((v) => Math.floor(v));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const log2 = register('log2', (pat) => pat.asNumber().fmap((v) => Math.log2(v)));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assumes a numerical pattern. Returns a new pattern with all values set to
|
* Assumes a numerical pattern. Returns a new pattern with all values set to
|
||||||
* their mathematical ceiling. E.g. `3.2` replaced with `4`, and `-4.2`
|
* their mathematical ceiling. E.g. `3.2` replaced with `4`, and `-4.2`
|
||||||
@@ -2497,6 +2572,20 @@ export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func,
|
|||||||
return stack(left, right).setSteps(__steps ? lcm(left._steps, right._steps) : undefined);
|
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.
|
* The jux function creates strange stereo effects, by applying a function to a pattern, but only in the right-hand channel.
|
||||||
* @tags temporal, superdough
|
* @tags temporal, superdough
|
||||||
@@ -2511,6 +2600,21 @@ export const jux = register('jux', function (func, pat) {
|
|||||||
return pat._juxBy(1, 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.
|
* Superimpose and offset multiple times, applying the given function each time.
|
||||||
* @tags temporal, functional
|
* @tags temporal, functional
|
||||||
@@ -3275,6 +3379,11 @@ Pattern.prototype.shrinklist = function (amount) {
|
|||||||
|
|
||||||
export const shrinklist = (amount, pat) => pat.shrinklist(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*
|
* *Experimental*
|
||||||
*
|
*
|
||||||
@@ -3545,26 +3654,6 @@ export const slice = register(
|
|||||||
false, // turns off auto-patternification
|
false, // turns off auto-patternification
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* make something happen on event time
|
|
||||||
* uses browser timeout which is innacurate for audio tasks
|
|
||||||
* @name onTriggerTime
|
|
||||||
* @tags external_io
|
|
||||||
* @memberof Pattern
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* s("bd!8").onTriggerTime((hap) => {console.log(hap)})
|
|
||||||
*/
|
|
||||||
Pattern.prototype.onTriggerTime = function (func) {
|
|
||||||
return this.onTrigger((hap, currentTime, _cps, targetTime) => {
|
|
||||||
const diff = targetTime - currentTime;
|
|
||||||
window.setTimeout(() => {
|
|
||||||
func(hap);
|
|
||||||
}, diff * 1000);
|
|
||||||
}, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
|
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
|
||||||
* @name splice
|
* @name splice
|
||||||
@@ -4043,3 +4132,59 @@ Pattern.prototype.worklet = function (src, ...inputs) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const worklet = (...args) => pure({}).worklet(...args);
|
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();
|
||||||
|
};
|
||||||
|
|||||||
+19
-15
@@ -7,7 +7,7 @@ import {
|
|||||||
setIsStarted,
|
setIsStarted,
|
||||||
setPattern as exposeSchedulerPattern,
|
setPattern as exposeSchedulerPattern,
|
||||||
setTime,
|
setTime,
|
||||||
setTriggerFunc,
|
setSchedulerFunc,
|
||||||
} from './schedulerState.mjs';
|
} from './schedulerState.mjs';
|
||||||
import { evalScope } from './evaluate.mjs';
|
import { evalScope } from './evaluate.mjs';
|
||||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||||
@@ -55,7 +55,7 @@ export function repl({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const schedulerOptions = {
|
const schedulerOptions = {
|
||||||
onTrigger: getTrigger({ defaultOutput, getTime }),
|
onSchedule: getScheduler({ defaultOutput }),
|
||||||
getTime,
|
getTime,
|
||||||
onToggle: (started) => {
|
onToggle: (started) => {
|
||||||
updateState({ started });
|
updateState({ started });
|
||||||
@@ -73,7 +73,7 @@ export function repl({
|
|||||||
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
|
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
|
||||||
const scheduler =
|
const scheduler =
|
||||||
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
|
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
|
||||||
setTriggerFunc(schedulerOptions.onTrigger);
|
setSchedulerFunc(schedulerOptions.onSchedule);
|
||||||
setCpsFunc(() => scheduler.cps);
|
setCpsFunc(() => scheduler.cps);
|
||||||
let pPatterns = {};
|
let pPatterns = {};
|
||||||
let anonymousIndex = 0;
|
let anonymousIndex = 0;
|
||||||
@@ -307,8 +307,12 @@ export function repl({
|
|||||||
|
|
||||||
let allTransforms = [];
|
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.
|
* 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("bd - cp sd")
|
||||||
* $: sound("hh*8")
|
* $: sound("hh*8")
|
||||||
@@ -328,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.
|
/** 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("bd - cp sd")
|
||||||
* $: sound("hh*8")
|
* $: sound("hh*8")
|
||||||
@@ -553,20 +560,17 @@ export function repl({
|
|||||||
return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state };
|
return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTrigger =
|
export const getScheduler =
|
||||||
({ getTime, defaultOutput }) =>
|
({ defaultOutput }) =>
|
||||||
async (hap, deadline, duration, cps, t) => {
|
async (hap, t, cps) => {
|
||||||
// ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
|
|
||||||
// TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
|
|
||||||
try {
|
try {
|
||||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
if (!hap.context.onSchedule || !hap.context.dominantScheduler) {
|
||||||
await defaultOutput(hap, deadline, duration, cps, t);
|
await defaultOutput(hap, t, cps);
|
||||||
}
|
}
|
||||||
if (hap.context.onTrigger) {
|
if (hap.context.onSchedule) {
|
||||||
// call signature of output / onTrigger is different...
|
await hap.context.onSchedule(hap, t, cps);
|
||||||
await hap.context.onTrigger(hap, getTime(), cps, t);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorLogger(err, 'getTrigger');
|
errorLogger(err, 'getScheduler');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
let time;
|
let time;
|
||||||
let cpsFunc;
|
let cpsFunc;
|
||||||
let pattern;
|
let pattern;
|
||||||
let triggerFunc;
|
let schedulerFunc;
|
||||||
let isStarted;
|
let isStarted;
|
||||||
export function getTime() {
|
export function getTime() {
|
||||||
if (!time) {
|
if (!time) {
|
||||||
@@ -36,12 +36,12 @@ export function getPattern() {
|
|||||||
return pattern;
|
return pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setTriggerFunc(func) {
|
export function setSchedulerFunc(func) {
|
||||||
triggerFunc = func;
|
schedulerFunc = func;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTriggerFunc() {
|
export function getSchedulerFunc() {
|
||||||
return triggerFunc;
|
return schedulerFunc;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setIsStarted(val) {
|
export function setIsStarted(val) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import { Hap } from './hap.mjs';
|
import { Hap } from './hap.mjs';
|
||||||
import { Pattern, fastcat, pure, register, reify, silence, stack, sequenceP } from './pattern.mjs';
|
import { Pattern, fastcat, pure, register, reify, silence, stack, sequenceP } from './pattern.mjs';
|
||||||
|
import { _mod } from './util.mjs';
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
|
|
||||||
import { id, keyAlias, getCurrentKeyboardState } from './util.mjs';
|
import { id, keyAlias, getCurrentKeyboardState } from './util.mjs';
|
||||||
@@ -33,7 +34,7 @@ export const signal = (func) => {
|
|||||||
* .scale('C major')
|
* .scale('C major')
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const saw = signal((t) => t % 1);
|
export const saw = signal((t) => _mod(t, 1));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A sawtooth signal between -1 and 1 (like `saw`, but bipolar).
|
* A sawtooth signal between -1 and 1 (like `saw`, but bipolar).
|
||||||
@@ -56,7 +57,7 @@ export const saw2 = saw.toBipolar();
|
|||||||
* .scale('C major')
|
* .scale('C major')
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const isaw = signal((t) => 1 - (t % 1));
|
export const isaw = signal((t) => 1 - _mod(t, 1));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A sawtooth signal between 1 and -1 (like `saw2`, but flipped).
|
* A sawtooth signal between 1 and -1 (like `saw2`, but flipped).
|
||||||
@@ -113,7 +114,7 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
|
|||||||
* n(square.segment(4).range(0,7)).scale("C:minor")
|
* n(square.segment(4).range(0,7)).scale("C:minor")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const square = signal((t) => Math.floor((t * 2) % 2));
|
export const square = signal((t) => Math.floor(_mod(t * 2, 2)));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A square signal between -1 and 1 (like `square`, but bipolar).
|
* A square signal between -1 and 1 (like `square`, but bipolar).
|
||||||
@@ -123,6 +124,22 @@ export const square = signal((t) => Math.floor((t * 2) % 2));
|
|||||||
*/
|
*/
|
||||||
export const square2 = square.toBipolar();
|
export const square2 = square.toBipolar();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A square signal between 1 and 0 (like `square` but flipped).
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
|
*/
|
||||||
|
export const isquare = signal((t) => 1 - Math.floor(_mod(t * 2, 2)));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A square signal between 1 and -1 (like `isquare`, but bipolar).
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
* @tags generators
|
||||||
|
*/
|
||||||
|
export const isquare2 = isquare.toBipolar();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A triangle signal between 0 and 1.
|
* A triangle signal between 0 and 1.
|
||||||
*
|
*
|
||||||
@@ -316,7 +333,7 @@ export const run = (n) => saw.range(0, n).round().segment(n);
|
|||||||
* // "hh".s().struct("1 0 1")
|
* // "hh".s().struct("1 0 1")
|
||||||
*/
|
*/
|
||||||
export const binary = (n) => {
|
export const binary = (n) => {
|
||||||
const nBits = reify(n).log2(0).floor().add(1);
|
const nBits = reify(n).log2().floor().add(1);
|
||||||
return binaryN(n, nBits);
|
return binaryN(n, nBits);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -348,7 +365,7 @@ export const binaryN = (n, nBits = 16) => {
|
|||||||
* .partials(binaryL(irand(4096).add(1)))
|
* .partials(binaryL(irand(4096).add(1)))
|
||||||
*/
|
*/
|
||||||
export const binaryL = (n) => {
|
export const binaryL = (n) => {
|
||||||
const nBits = reify(n).log2(0).floor().add(1);
|
const nBits = reify(n).log2().floor().add(1);
|
||||||
return binaryNL(n, nBits);
|
return binaryNL(n, nBits);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -389,12 +406,14 @@ export const randL = (n) => {
|
|||||||
export const randrun = (n) => {
|
export const randrun = (n) => {
|
||||||
return signal((t, controls) => {
|
return signal((t, controls) => {
|
||||||
// Without adding 0.5, the first cycle is always 0,1,2,3,...
|
// Without adding 0.5, the first cycle is always 0,1,2,3,...
|
||||||
const rands = getRandsAtTime(t.floor().add(0.5), n, controls.randSeed);
|
let rands = getRandsAtTime(t.floor().add(0.5), n, controls.randSeed);
|
||||||
|
// Support n = 1
|
||||||
|
if (!Array.isArray(rands)) rands = [rands];
|
||||||
const nums = rands
|
const nums = rands
|
||||||
.map((n, i) => [n, i])
|
.map((n, i) => [n, i])
|
||||||
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
|
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
|
||||||
.map((x) => x[1]);
|
.map((x) => x[1]);
|
||||||
const i = t.cyclePos().mul(n).floor() % n;
|
const i = _mod(t.cyclePos().mul(n).floor(), n);
|
||||||
return nums[i];
|
return nums[i];
|
||||||
})._segment(n);
|
})._segment(n);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function triggerSpeech(words, lang, voice) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const speak = register('speak', function (lang, voice, pat) {
|
export const speak = register('speak', function (lang, voice, pat) {
|
||||||
return pat.onTrigger((hap) => {
|
return pat.onSchedule((hap) => {
|
||||||
triggerSpeech(hap.value, lang, voice);
|
triggerSpeech(hap.value, lang, voice);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
fastcat,
|
fastcat,
|
||||||
firstOf,
|
firstOf,
|
||||||
slowcat,
|
slowcat,
|
||||||
|
slowcatPrime,
|
||||||
cat,
|
cat,
|
||||||
sequence,
|
sequence,
|
||||||
palindrome,
|
palindrome,
|
||||||
@@ -45,6 +46,7 @@ import {
|
|||||||
rev,
|
rev,
|
||||||
time,
|
time,
|
||||||
run,
|
run,
|
||||||
|
randrun,
|
||||||
binaryN,
|
binaryN,
|
||||||
pick,
|
pick,
|
||||||
stackLeft,
|
stackLeft,
|
||||||
@@ -53,6 +55,7 @@ import {
|
|||||||
stepcat,
|
stepcat,
|
||||||
sometimes,
|
sometimes,
|
||||||
expand,
|
expand,
|
||||||
|
arp,
|
||||||
} from '../index.mjs';
|
} from '../index.mjs';
|
||||||
|
|
||||||
import { log, logValues } from '../pattern.mjs';
|
import { log, logValues } from '../pattern.mjs';
|
||||||
@@ -544,6 +547,9 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('slowcat()', () => {
|
describe('slowcat()', () => {
|
||||||
|
it('Can be empty', () => {
|
||||||
|
expect(slowcat().firstCycle()).toStrictEqual([]);
|
||||||
|
});
|
||||||
it('Can concatenate things slowly', () => {
|
it('Can concatenate things slowly', () => {
|
||||||
expect(
|
expect(
|
||||||
slowcat('a', 'b')
|
slowcat('a', 'b')
|
||||||
@@ -576,6 +582,38 @@ describe('Pattern', () => {
|
|||||||
sameFirst(slowcat('a', ['b', 'c']).fast(4), sequence('a', ['b', 'c']).fast(2));
|
sameFirst(slowcat('a', ['b', 'c']).fast(4), sequence('a', ['b', 'c']).fast(2));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('slowcatPrime()', () => {
|
||||||
|
it('Can be empty', () => {
|
||||||
|
expect(slowcatPrime().firstCycle()).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
it('Can slowcat patterns swapping back and forth skipping the expected notes', () => {
|
||||||
|
expect(
|
||||||
|
slowcatPrime(fastcat(0, 1, 2, 3).slow(2), fastcat(4, 5, 6, 7).slow(2))
|
||||||
|
.fast(4)
|
||||||
|
.firstCycle()
|
||||||
|
.map((a) => a.value),
|
||||||
|
).toStrictEqual([0, 1, 6, 7, 0, 1, 6, 7]);
|
||||||
|
});
|
||||||
|
it('Can go into negative time', () => {
|
||||||
|
expect(
|
||||||
|
slowcatPrime(fastcat(0, 1, 2, 3).slow(2), fastcat(4, 5, 6, 7).slow(2))
|
||||||
|
.fast(4)
|
||||||
|
.late(8)
|
||||||
|
.firstCycle()
|
||||||
|
.map((a) => a.value),
|
||||||
|
).toStrictEqual([0, 1, 6, 7, 0, 1, 6, 7]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('arp()', () => {
|
||||||
|
it('It wraps around with both positive and negative indices', () => {
|
||||||
|
expect(
|
||||||
|
stack('a', 'b', 'c')
|
||||||
|
.arp(fastcat(-3, -2, -1, 0, 1, 2, 3, 4))
|
||||||
|
.firstCycle()
|
||||||
|
.map((a) => a.value),
|
||||||
|
).toStrictEqual(['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b']);
|
||||||
|
});
|
||||||
|
});
|
||||||
describe('rev()', () => {
|
describe('rev()', () => {
|
||||||
it('Can reverse things', () => {
|
it('Can reverse things', () => {
|
||||||
expect(
|
expect(
|
||||||
@@ -945,6 +983,17 @@ describe('Pattern', () => {
|
|||||||
it('Can run', () => {
|
it('Can run', () => {
|
||||||
expect(run(4).firstCycle()).toStrictEqual(sequence(0, 1, 2, 3).firstCycle());
|
expect(run(4).firstCycle()).toStrictEqual(sequence(0, 1, 2, 3).firstCycle());
|
||||||
});
|
});
|
||||||
|
it('Can go into negative time', () => {
|
||||||
|
expect(run(4).late(1).firstCycle()).toStrictEqual(sequence(0, 1, 2, 3).firstCycle());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('randrun', () => {
|
||||||
|
it('Can randrun', () => {
|
||||||
|
expect(randrun(4).firstCycle()).toStrictEqual(sequence(2, 1, 3, 0).firstCycle());
|
||||||
|
});
|
||||||
|
it('Can go into negative time', () => {
|
||||||
|
expect(randrun(4).late(1).firstCycle()).toStrictEqual(sequence(1, 2, 0, 3).firstCycle());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('binaryN', () => {
|
describe('binaryN', () => {
|
||||||
it('Can make a binary pattern from a decimal', () => {
|
it('Can make a binary pattern from a decimal', () => {
|
||||||
@@ -1314,7 +1363,7 @@ describe('Pattern', () => {
|
|||||||
|
|
||||||
// Force a trigger
|
// Force a trigger
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
hap.context?.onTrigger?.(hap);
|
hap.context?.onSchedule?.(hap);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockConsoleLog).toHaveBeenCalledWith(
|
expect(mockConsoleLog).toHaveBeenCalledWith(
|
||||||
@@ -1332,7 +1381,7 @@ describe('Pattern', () => {
|
|||||||
|
|
||||||
// Force a trigger
|
// Force a trigger
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
hap.context?.onTrigger?.(hap);
|
hap.context?.onSchedule?.(hap);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockConsoleLog).toHaveBeenCalledWith(
|
expect(mockConsoleLog).toHaveBeenCalledWith(
|
||||||
|
|||||||
@@ -8,8 +8,24 @@ import Fraction from 'fraction.js';
|
|||||||
|
|
||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
|
||||||
import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs';
|
import {
|
||||||
import { fastcat, sequence, State, TimeSpan, Hap } from '../index.mjs';
|
saw,
|
||||||
|
saw2,
|
||||||
|
isaw,
|
||||||
|
isaw2,
|
||||||
|
tri,
|
||||||
|
tri2,
|
||||||
|
itri,
|
||||||
|
itri2,
|
||||||
|
square,
|
||||||
|
square2,
|
||||||
|
isquare,
|
||||||
|
isquare2,
|
||||||
|
per,
|
||||||
|
perx,
|
||||||
|
cyclesPer,
|
||||||
|
} from '../signal.mjs';
|
||||||
|
import { fastcat, sequence, State, TimeSpan, Hap, note } from '../index.mjs';
|
||||||
|
|
||||||
const st = (begin, end) => new State(ts(begin, end));
|
const st = (begin, end) => new State(ts(begin, end));
|
||||||
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
|
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
|
||||||
@@ -24,17 +40,61 @@ const sameFirst = (a, b) => {
|
|||||||
|
|
||||||
describe('signal()', () => {
|
describe('signal()', () => {
|
||||||
it('Can make saw/saw2', () => {
|
it('Can make saw/saw2', () => {
|
||||||
expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(
|
expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0.25, 0.5, 0.75).firstCycle());
|
||||||
sequence(0, 1 / 4, 1 / 2, 3 / 4).firstCycle(),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle());
|
expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle());
|
||||||
});
|
});
|
||||||
it('Can make isaw/isaw2', () => {
|
it('Can make isaw/isaw2', () => {
|
||||||
expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle());
|
expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle());
|
||||||
|
|
||||||
expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle());
|
expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle());
|
||||||
});
|
});
|
||||||
|
it('Can make tri/tri2', () => {
|
||||||
|
expect(tri.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0.5, 1, 0.5).firstCycle());
|
||||||
|
expect(tri2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, 0, 1, 0).firstCycle());
|
||||||
|
});
|
||||||
|
it('Can make itri/itri2', () => {
|
||||||
|
expect(itri.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, 0.5).firstCycle());
|
||||||
|
expect(itri2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0, -1, 0).firstCycle());
|
||||||
|
});
|
||||||
|
it('Can make square/square2', () => {
|
||||||
|
expect(square.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0, 1, 1).firstCycle());
|
||||||
|
expect(square2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -1, 1, 1).firstCycle());
|
||||||
|
});
|
||||||
|
it('Can make isquare/isquare2', () => {
|
||||||
|
expect(isquare.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 1, 0, 0).firstCycle());
|
||||||
|
expect(isquare2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 1, -1, -1).firstCycle());
|
||||||
|
});
|
||||||
|
it('Can go into negative time', () => {
|
||||||
|
expect(saw.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(0, 0.25, 0.5, 0.75).firstCycle(),
|
||||||
|
);
|
||||||
|
expect(saw2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(-1, -0.5, 0, 0.5).firstCycle(),
|
||||||
|
);
|
||||||
|
expect(isaw.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(1, 0.75, 0.5, 0.25).firstCycle(),
|
||||||
|
);
|
||||||
|
expect(isaw2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(1, 0.5, 0, -0.5).firstCycle(),
|
||||||
|
);
|
||||||
|
expect(tri.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(0, 0.5, 1, 0.5).firstCycle(),
|
||||||
|
);
|
||||||
|
expect(tri2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, 0, 1, 0).firstCycle());
|
||||||
|
expect(itri.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(1, 0.5, 0, 0.5).firstCycle(),
|
||||||
|
);
|
||||||
|
expect(itri2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0, -1, 0).firstCycle());
|
||||||
|
expect(square.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(0, 0, 1, 1).firstCycle());
|
||||||
|
expect(square2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(-1, -1, 1, 1).firstCycle(),
|
||||||
|
);
|
||||||
|
expect(isquare.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(1, 1, 0, 0).firstCycle(),
|
||||||
|
);
|
||||||
|
expect(isquare2.late(1).struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(1, 1, -1, -1).firstCycle(),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('cyclesPer', () => {
|
describe('cyclesPer', () => {
|
||||||
@@ -59,3 +119,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());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export const getFrequency = (hap) => {
|
|||||||
if (value.freq) {
|
if (value.freq) {
|
||||||
return value.freq;
|
return value.freq;
|
||||||
}
|
}
|
||||||
return getFreq(value.note || value.n || value.value);
|
return getFreq(value.note ?? value.n ?? value.value);
|
||||||
}
|
}
|
||||||
if (typeof value === 'number' && context.type !== 'frequency') {
|
if (typeof value === 'number' && context.type !== 'frequency') {
|
||||||
value = midiToFreq(hap.value);
|
value = midiToFreq(hap.value);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => {
|
|||||||
instrument = instrument || 'triangle';
|
instrument = instrument || 'triangle';
|
||||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||||
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
|
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
|
||||||
return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
|
return pat.onSchedule((hap, targetTime) => {
|
||||||
if (!_csound) {
|
if (!_csound) {
|
||||||
logger('[csound] not loaded yet', 'warning');
|
logger('[csound] not loaded yet', 'warning');
|
||||||
return;
|
return;
|
||||||
@@ -36,9 +36,10 @@ export const csound = register('csound', (instrument, pat) => {
|
|||||||
const controls = Object.entries({ ...hap.value, freq })
|
const controls = Object.entries({ ...hap.value, freq })
|
||||||
.flat()
|
.flat()
|
||||||
.join('/');
|
.join('/');
|
||||||
|
|
||||||
// TODO: find out how to send a precise ctx based time
|
// TODO: find out how to send a precise ctx based time
|
||||||
// http://www.csounds.com/manual/html/i.html
|
// http://www.csounds.com/manual/html/i.html
|
||||||
const timeOffset = targetTime - currentTime; // latency ?
|
const timeOffset = targetTime - getAudioContext().currentTime; // latency ?
|
||||||
//const timeOffset = time_deprecate - getAudioContext().currentTime
|
//const timeOffset = time_deprecate - getAudioContext().currentTime
|
||||||
const params = [
|
const params = [
|
||||||
`"${instrument}"`, // p1: instrument name
|
`"${instrument}"`, // p1: instrument name
|
||||||
@@ -144,7 +145,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
|
|||||||
p1 = `"${instrument}"`;
|
p1 = `"${instrument}"`;
|
||||||
}
|
}
|
||||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||||
return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
|
return pat.onSchedule((hap, targetTime) => {
|
||||||
if (!_csound) {
|
if (!_csound) {
|
||||||
logger('[csound] not loaded yet', 'warning');
|
logger('[csound] not loaded yet', 'warning');
|
||||||
return;
|
return;
|
||||||
@@ -152,6 +153,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
|
|||||||
if (typeof hap.value !== 'object') {
|
if (typeof hap.value !== 'object') {
|
||||||
throw new Error('csound only support objects as hap values');
|
throw new Error('csound only support objects as hap values');
|
||||||
}
|
}
|
||||||
|
const currentTime = getAudioContext().currentTime;
|
||||||
// Time in seconds counting from now.
|
// Time in seconds counting from now.
|
||||||
const p2 = targetTime - currentTime;
|
const p2 = targetTime - currentTime;
|
||||||
const p3 = hap.duration.valueOf() + 0;
|
const p3 = hap.duration.valueOf() + 0;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getAudioContext } from '@strudel/webaudio/index.mjs';
|
||||||
import { Invoke } from './utils.mjs';
|
import { Invoke } from './utils.mjs';
|
||||||
import { Pattern, getEventOffsetMs, noteToMidi } from '@strudel/core';
|
import { Pattern, getEventOffsetMs, noteToMidi } from '@strudel/core';
|
||||||
|
|
||||||
@@ -6,7 +7,8 @@ const OFF_MESSAGE = 0x80;
|
|||||||
const CC_MESSAGE = 0xb0;
|
const CC_MESSAGE = 0xb0;
|
||||||
|
|
||||||
Pattern.prototype.midi = function (output) {
|
Pattern.prototype.midi = function (output) {
|
||||||
return this.onTrigger((hap, currentTime, cps, targetTime) => {
|
return this.onSchedule((hap, targetTime, cps) => {
|
||||||
|
const currentTime = getAudioContext().currentTime;
|
||||||
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
|
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
|
||||||
//magic number to get audio engine to line up, can probably be calculated somehow
|
//magic number to get audio engine to line up, can probably be calculated somehow
|
||||||
const latencyMs = 34;
|
const latencyMs = 34;
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Pattern, ClockCollator } from '@strudel/core';
|
import { Pattern, ClockCollator } from '@strudel/core';
|
||||||
import { parseControlsFromHap } from 'node_modules/@strudel/osc/osc.mjs';
|
import { parseControlsFromHap } from '@strudel/osc/osc.mjs';
|
||||||
import { Invoke } from './utils.mjs';
|
import { Invoke } from './utils.mjs';
|
||||||
|
import { getAudioContext } from '@strudel/webaudio/index.mjs';
|
||||||
|
|
||||||
const collator = new ClockCollator({});
|
const collator = new ClockCollator({});
|
||||||
|
|
||||||
export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) {
|
export async function oscSchedulerTauri(hap, targetTime, cps = 1) {
|
||||||
const controls = parseControlsFromHap(hap, cps);
|
const controls = parseControlsFromHap(hap, cps);
|
||||||
const params = [];
|
const params = [];
|
||||||
|
const currentTime = getAudioContext().currentTime;
|
||||||
const timestamp = collator.calculateTimestamp(currentTime, targetTime);
|
const timestamp = collator.calculateTimestamp(currentTime, targetTime);
|
||||||
|
|
||||||
Object.keys(controls).forEach((key) => {
|
Object.keys(controls).forEach((key) => {
|
||||||
@@ -32,5 +34,5 @@ export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
Pattern.prototype.osc = function () {
|
Pattern.prototype.osc = function () {
|
||||||
return this.onTrigger(oscTriggerTauri);
|
return this.onSchedule(oscSchedulerTauri);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/webaudio": "workspace:*",
|
||||||
"@tauri-apps/api": "^2.2.0"
|
"@tauri-apps/api": "^2.2.0"
|
||||||
},
|
},
|
||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
dough.mjs
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/osc/osc.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, noteToMidi } from '@strudel/core';
|
||||||
|
import { Dough, doughsamples } from 'dough-synth';
|
||||||
|
import { getAudioContext, ensureMinimalOutput } from '@strudel/webaudio';
|
||||||
|
import wasm from 'dough-synth/dough.wasm?url';
|
||||||
|
import workletCode from 'dough-synth/dough.js?raw';
|
||||||
|
|
||||||
|
Object.assign(globalThis, { doughsamples });
|
||||||
|
|
||||||
|
let D;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* initializes dough ahead of time. you don't need to use this, but if you do, you can await it, so dough is ready before the pattern starts.
|
||||||
|
*
|
||||||
|
* @name initDough
|
||||||
|
* @tags external_io
|
||||||
|
* @memberof Pattern
|
||||||
|
* @example
|
||||||
|
* await initDough()
|
||||||
|
* $: chord("<Dm9 Dm11 Dm7>").offset("<-1 0 1 0>").voicing()
|
||||||
|
* .phaser(3).fm(1.4).fmh(1.01)
|
||||||
|
* .dough()
|
||||||
|
*/
|
||||||
|
export function initDough() {
|
||||||
|
if (!D || D.audioContext !== getAudioContext()) {
|
||||||
|
D = new Dough({
|
||||||
|
wasm,
|
||||||
|
workletCode,
|
||||||
|
audioContext: getAudioContext(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return D.ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function doughScheduler(hap, targetTime, cps = 1) {
|
||||||
|
const offset = D.context_offset?.[0];
|
||||||
|
if (!offset) {
|
||||||
|
return; // not ready
|
||||||
|
}
|
||||||
|
hap.ensureObjectValue();
|
||||||
|
const event = {
|
||||||
|
dough: 'play',
|
||||||
|
...hap.value,
|
||||||
|
time: targetTime - offset,
|
||||||
|
duration: hap.duration / cps,
|
||||||
|
};
|
||||||
|
if (typeof event.note === 'string') {
|
||||||
|
event.note = noteToMidi(event.note);
|
||||||
|
}
|
||||||
|
D.evaluate(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Uses dough as the audio engine. more info at https://dough.strudel.cc
|
||||||
|
*
|
||||||
|
* @name dough
|
||||||
|
* @tags external_io
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* $: chord("<Dm9 Dm11 Dm7>").offset("<-1 0 1 0>").voicing()
|
||||||
|
* .phaser(3).fm(1.4).fmh(1.01)
|
||||||
|
* .dough()
|
||||||
|
*/
|
||||||
|
export const dough = register('dough', (pat) => {
|
||||||
|
initDough();
|
||||||
|
ensureMinimalOutput();
|
||||||
|
return pat.onSchedule(doughScheduler);
|
||||||
|
});
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "@strudel/dough",
|
||||||
|
"version": "1.3.2",
|
||||||
|
"description": "dough synth integration for strudel",
|
||||||
|
"main": "dough.mjs",
|
||||||
|
"type": "module",
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "dist/index.mjs"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://codeberg.org/uzu/strudel.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"tidalcycles",
|
||||||
|
"strudel",
|
||||||
|
"pattern",
|
||||||
|
"livecoding",
|
||||||
|
"algorave"
|
||||||
|
],
|
||||||
|
"author": "Felix Roos <flix91@gmail.com>",
|
||||||
|
"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:*",
|
||||||
|
"@strudel/webaudio": "workspace:*",
|
||||||
|
"dough-synth": "0.2.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^6.0.11"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,11 +54,42 @@ export function pitchwheel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (edo) {
|
if (edo) {
|
||||||
|
edo = haps.length >= 1 && haps[0].value && haps[0].value.edo ? haps[0].value.edo : edo;
|
||||||
|
root = haps.length >= 1 && haps[0].value && haps[0].value.root ? haps[0].value.root : root;
|
||||||
|
const degreeIndexes =
|
||||||
|
haps.length >= 1 && haps[0].value && haps[0].value.degreeIndexes ? haps[0].value.degreeIndexes : null;
|
||||||
|
const intLabels = haps.length >= 1 && haps[0].value && haps[0].value.intLabels ? haps[0].value.intLabels : null;
|
||||||
|
ctx.font = '20px sans-serif';
|
||||||
|
const label = `${edo} EDO`;
|
||||||
|
// Draw EDO label:
|
||||||
|
ctx.fillText(label, centerX + radius - ctx.measureText(label).width + 15, centerY + radius);
|
||||||
Array.from({ length: edo }, (_, i) => {
|
Array.from({ length: edo }, (_, i) => {
|
||||||
const angle = freq2angle(root * Math.pow(2, i / edo), root);
|
const angle = freq2angle(root * Math.pow(2, i / edo), root);
|
||||||
|
|
||||||
const [x, y] = circlePos(centerX, centerY, radius, angle);
|
const [x, y] = circlePos(centerX, centerY, radius, angle);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
|
// Draw interval label for degree i when it exists:
|
||||||
|
if (degreeIndexes === null || degreeIndexes.includes(i)) {
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
|
||||||
|
if (intLabels !== null) {
|
||||||
|
const degree = degreeIndexes.indexOf(i);
|
||||||
|
if (intLabels[degree]) {
|
||||||
|
if (angle < 0.32 && angle > 0.125) {
|
||||||
|
ctx.fillText(intLabels[degree], x - 34, y);
|
||||||
|
} else {
|
||||||
|
if (angle < 0.1 && angle > -1.125) {
|
||||||
|
ctx.fillText(intLabels[degree], x - 7, y - 12);
|
||||||
|
} else {
|
||||||
|
ctx.fillText(intLabels[degree], x + 9, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.globalAlpha = 0.15;
|
||||||
|
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
|
||||||
|
}
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
});
|
});
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# @strudel/edo
|
||||||
|
|
||||||
|
This package adds EDO scale functions to strudel Patterns.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i @strudel/edo --save
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { n } from '@strudel/core';
|
||||||
|
import '@strudel/edo';
|
||||||
|
|
||||||
|
// E.g. edoScale for Gorgo-6 scale, 16 EDO, LLsLLLs
|
||||||
|
// base note C3, large step size 3, small step size 1:
|
||||||
|
// C3:LLsLLL:3:1
|
||||||
|
const [baseNote, sequence, largeStep, smallStep] = ['C3', 'LLsLLL', 3, 1]
|
||||||
|
const pattern = n("0 2 4 6 4 2").edoScale([baseNote, sequence, largeStep, smallStep]);
|
||||||
|
|
||||||
|
const events = pattern.firstCycle().map((e) => e.show());
|
||||||
|
console.log(events);
|
||||||
|
```
|
||||||
|
|
||||||
|
yields:
|
||||||
|
```
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 |
|
||||||
|
{
|
||||||
|
\"degree\":1,
|
||||||
|
\"degreeIndexes\":[0,3,6,7,10,13],
|
||||||
|
\"intLabels\":[null,\"S2\",\"d4\",\"N4\",\"s6\",\"s7\",\"P8\"],
|
||||||
|
\"root\":\"130.8128\",
|
||||||
|
\"freq\":130.813,
|
||||||
|
\"edo\":16
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
]
|
||||||
|
```
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
edo.mjs - Equal division of the octave (EDO) scale functions for strudel
|
||||||
|
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/edo.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { register, pure, noteToMidi, isNote, tokenizeNote } from '@strudel/core';
|
||||||
|
import { EdoScale } from './edoscale.mjs';
|
||||||
|
import { Intervals } from './intervals.mjs';
|
||||||
|
import { Pitches } from './pitches.mjs';
|
||||||
|
|
||||||
|
const pitchesCache = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns numbers into notes in the given EDO scale (zero indexed).
|
||||||
|
*
|
||||||
|
* An EDO scale definition looks like this:
|
||||||
|
*
|
||||||
|
* e.g. C:LLsLLLs:2:1 <- this is the C major scale, 12 EDO
|
||||||
|
*
|
||||||
|
* e.g. C:LLsLLL:3:1 <- this is the Gorgo 6 note scale, 16 EDO
|
||||||
|
*
|
||||||
|
* An EDO scale, e.g. C:LLsLLLs:2:1, consists of a root note (e.g. C)
|
||||||
|
* followed by semicolon (':')
|
||||||
|
* and then a [Large/small step notation sequence](https://en.xen.wiki/w/MOS_scale)
|
||||||
|
* (e.g. LLsLLLs)
|
||||||
|
* followed by semicolon, then the large step size (e.g. 2)
|
||||||
|
* followed by semicolon, then the small step size (e.g. 1).
|
||||||
|
*
|
||||||
|
* The number of divisions of the octave is calculated as the sum
|
||||||
|
* of the steps in the EDO scale definition.
|
||||||
|
*
|
||||||
|
* e.g. C:LLsLLLs:2:1 is 2+2+1+2+2+2+1 = 12 EDO, 7 note scale
|
||||||
|
*
|
||||||
|
* e.g. C:LLsLLL:3:1 is 3+3+1+3+3+3 = 16 EDO, 6 note scale
|
||||||
|
*
|
||||||
|
* The root note defaults to octave 3, if no octave number is given.
|
||||||
|
*
|
||||||
|
* @name edoScale
|
||||||
|
* @param {string} scale Definition of EDO scale.
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* n("0 2 4 6 4 2").edoScale("C:LLsLLLs:2:1")
|
||||||
|
* @example
|
||||||
|
* n("[0,7] 4 [2,7] 4")
|
||||||
|
* .edoScale("G2:<LLsLLL LLLLsL>:3:1")
|
||||||
|
* .s("piano")._pitchwheel()
|
||||||
|
* @example
|
||||||
|
* n(rand.range(0,5).segment(6))
|
||||||
|
* .edoScale("<G2 C3>:LLsLL:3:1")
|
||||||
|
* .s("piano")._pitchwheel()
|
||||||
|
*/
|
||||||
|
export const edoScale = register(
|
||||||
|
'edoScale',
|
||||||
|
function (scaleDefinition, pat) {
|
||||||
|
// console.log(scaleDefinition);
|
||||||
|
|
||||||
|
// if (Array.isArray(scale)) {
|
||||||
|
const key = scaleDefinition.flat().join(':');
|
||||||
|
// }
|
||||||
|
// console.log(scaleDefinition);
|
||||||
|
let pitches;
|
||||||
|
if (pitchesCache.has(key)) {
|
||||||
|
pitches = pitchesCache.get(key);
|
||||||
|
} else {
|
||||||
|
// console.log({ key });
|
||||||
|
const [base_note, sequence, large, small] = scaleDefinition;
|
||||||
|
const root_octave = tokenizeNote(base_note)[2] || 3;
|
||||||
|
// console.log({ root_octave });
|
||||||
|
const scale = new EdoScale(large, small, sequence);
|
||||||
|
const intervals = new Intervals(scale);
|
||||||
|
// console.log({ intervals });
|
||||||
|
pitches = new Pitches(scale, intervals, 440, noteToMidi(base_note), root_octave);
|
||||||
|
pitchesCache.set(key, pitches);
|
||||||
|
// console.log({ base_note: noteToMidi(base_note) });
|
||||||
|
// console.log({ sequence });
|
||||||
|
// console.log({ edivisions: scale.edivisions });
|
||||||
|
// console.log({ intervals });
|
||||||
|
// console.log({ pat });
|
||||||
|
// console.log({ pitches: pitches });
|
||||||
|
}
|
||||||
|
return pat
|
||||||
|
.fmap((value) => {
|
||||||
|
const isObject = typeof value === 'object';
|
||||||
|
const n = isObject ? value.n : value;
|
||||||
|
if (isObject) {
|
||||||
|
delete value.n; // remove n so it won't cause trouble
|
||||||
|
}
|
||||||
|
if (isNote(n)) {
|
||||||
|
// legacy..
|
||||||
|
return pure(n);
|
||||||
|
}
|
||||||
|
const deg = (typeof n === 'string' ? parseInt(n, 10) : Number.isInteger(n) ? n : Math.round(n)) + 1;
|
||||||
|
|
||||||
|
const [oct, degree] = pitches.octdeg(deg);
|
||||||
|
const freq = pitches.octdegfreq(oct, degree);
|
||||||
|
const note = pitches.octdegmidi(oct, degree);
|
||||||
|
const edo = pitches.scale.edivisions;
|
||||||
|
const root = pitches.base_freq;
|
||||||
|
const degreeIndexes = pitches.scale.divisions;
|
||||||
|
const intLabels = pitches.intervals.intLabels;
|
||||||
|
// const color = 'red';
|
||||||
|
value = pure(isObject ? { ...value, degree, degreeIndexes, intLabels, root, freq, edo } : note);
|
||||||
|
// value = pure(isObject ? { ...value, key } : note);
|
||||||
|
// value = pure(isObject ? { ...value, edo } : note);
|
||||||
|
// console.log({ value });
|
||||||
|
return value;
|
||||||
|
})
|
||||||
|
.outerJoin()
|
||||||
|
.withHap((hap) => hap.setContext({ ...hap.context, scaleDefinition }));
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
true, // preserve tactus
|
||||||
|
);
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/*
|
||||||
|
edoscale.mjs - EdoScale defines equal division of the octave (EDO) scale in Ls notation
|
||||||
|
- Port of pitfalls/lib/Scale.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/Scale.lua>
|
||||||
|
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/edoscale.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
const M = 2;
|
||||||
|
const L = 1;
|
||||||
|
const S = 0;
|
||||||
|
const LABELS = ['s', 'L', 'M'];
|
||||||
|
|
||||||
|
export class EdoScale {
|
||||||
|
constructor(large, small, sequence, medium) {
|
||||||
|
this.stepbackup = [L, L, S, L, L, L, S, L, L, L, S, L, L, L, S, L];
|
||||||
|
this.large = large;
|
||||||
|
this.medium = medium || large;
|
||||||
|
this.small = small;
|
||||||
|
this.divisions = [];
|
||||||
|
this.edivisions = null;
|
||||||
|
this.sequence = null;
|
||||||
|
this.tonic = 1;
|
||||||
|
this.mode = 1;
|
||||||
|
this.max_steps = 12;
|
||||||
|
this.min_steps = 3;
|
||||||
|
this.setSequence(sequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMedium() {
|
||||||
|
return this.step.some((_, i) => this.step[this.offset(i)] === M);
|
||||||
|
}
|
||||||
|
|
||||||
|
stepSize(i) {
|
||||||
|
return LABELS[this.step[this.offset(i)]];
|
||||||
|
}
|
||||||
|
|
||||||
|
sequence() {
|
||||||
|
return this.step.map((_, i) => this.stepSize(i)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
stepValue(i) {
|
||||||
|
const step = this.step[this.offset(i)];
|
||||||
|
return step === L ? this.large : step === M ? this.medium : this.small;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset(i) {
|
||||||
|
if (this.mode === 1) {
|
||||||
|
return i;
|
||||||
|
} else {
|
||||||
|
const offset = (this.mode - 1 + i) % this.length;
|
||||||
|
return offset === 0 ? this.length : offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static setMaxSteps(max) {
|
||||||
|
this.max_steps = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
static setMinSteps(min) {
|
||||||
|
this.min_steps = min;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSequence(sequence) {
|
||||||
|
if (this.sequence !== sequence) {
|
||||||
|
this.sequence = sequence;
|
||||||
|
this.length = sequence.length;
|
||||||
|
this.step = [];
|
||||||
|
for (let i = 0; i < sequence.length; i++) {
|
||||||
|
const char = sequence[i];
|
||||||
|
this.step[i] = char === 'L' ? L : char === 'M' ? M : S;
|
||||||
|
}
|
||||||
|
this.updateEdo();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLarge(l) {
|
||||||
|
if (this.large !== l) {
|
||||||
|
this.large = l;
|
||||||
|
this.updateEdo();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setMedium(m) {
|
||||||
|
if (this.medium !== m) {
|
||||||
|
this.medium = m;
|
||||||
|
this.updateEdo();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSmall(s) {
|
||||||
|
if (this.small !== s) {
|
||||||
|
this.small = s;
|
||||||
|
this.updateEdo();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setMode(mode) {
|
||||||
|
this.mode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTonic(tonic) {
|
||||||
|
this.tonic = tonic;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeMode(d) {
|
||||||
|
const orig = this.mode;
|
||||||
|
this.mode = Math.max(1, Math.min(this.mode + d, this.length));
|
||||||
|
return orig !== this.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeTonic(d) {
|
||||||
|
const orig = this.tonic;
|
||||||
|
this.tonic = Math.max(1, Math.min(this.tonic + d, this.edivisions));
|
||||||
|
return orig !== this.tonic;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEdo() {
|
||||||
|
const orig = this.edivisions;
|
||||||
|
this.edivisions = this.step.reduce((sum, _, i) => {
|
||||||
|
this.divisions[i] = sum;
|
||||||
|
return sum + this.stepValue(i);
|
||||||
|
}, 0);
|
||||||
|
// console.log(this.divisions);
|
||||||
|
const changed = orig !== this.edivisions;
|
||||||
|
if (changed) {
|
||||||
|
this.tonic = Math.max(1, Math.min(this.tonic, this.edivisions));
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeStep(d, i) {
|
||||||
|
const index = this.offset(i);
|
||||||
|
const orig = this.step[index];
|
||||||
|
this.step[index] = Math.max(S, Math.min(this.step[index] + d, M));
|
||||||
|
this.stepbackup[index] = this.step[index];
|
||||||
|
const changed = orig !== this.step[index];
|
||||||
|
if (changed) {
|
||||||
|
this.updateEdo();
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeLarge(d) {
|
||||||
|
const orig = this.large;
|
||||||
|
this.setLarge(Math.max(this.small + 1, Math.min(this.large + d, this.large + 1)));
|
||||||
|
const changed = this.large !== orig;
|
||||||
|
if (changed) {
|
||||||
|
if (this.large <= this.medium) {
|
||||||
|
this.setMedium(Math.max(this.small + 1, Math.min(this.large - 1, this.large)));
|
||||||
|
}
|
||||||
|
this.updateEdo();
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeMedium(d) {
|
||||||
|
const orig = this.medium;
|
||||||
|
this.setMedium(Math.max(this.small + 1, Math.min(this.medium + d, this.large - 1)));
|
||||||
|
const changed = this.medium !== orig;
|
||||||
|
if (changed) {
|
||||||
|
this.updateEdo();
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeSmall(d) {
|
||||||
|
const orig = this.small;
|
||||||
|
const value = this.small + d;
|
||||||
|
|
||||||
|
if (this.hasMedium()) {
|
||||||
|
this.setSmall(Math.max(1, Math.min(value, this.medium - 1)));
|
||||||
|
} else {
|
||||||
|
this.setSmall(Math.max(1, Math.min(value, this.large - 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const changed = this.small !== orig;
|
||||||
|
if (changed) {
|
||||||
|
if (this.small >= this.medium) {
|
||||||
|
this.setMedium(Math.max(this.small + 1, Math.min(this.large)));
|
||||||
|
}
|
||||||
|
this.updateEdo();
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeLength(d) {
|
||||||
|
const orig = this.length;
|
||||||
|
if (d === 1) {
|
||||||
|
this.length = Math.min(this.length + 1, this.max_steps);
|
||||||
|
} else if (d === -1) {
|
||||||
|
this.length = Math.max(this.length - 1, this.min_steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
const changed = this.length !== orig;
|
||||||
|
if (changed) {
|
||||||
|
this.mode = 1;
|
||||||
|
if (d === 1) {
|
||||||
|
this.step[this.length] = this.stepbackup[this.length] || L;
|
||||||
|
} else if (d === -1 && this.length >= this.min_steps) {
|
||||||
|
this.step.pop();
|
||||||
|
}
|
||||||
|
this.updateEdo();
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import './edo.mjs';
|
||||||
|
|
||||||
|
export * from './edo.mjs';
|
||||||
|
|
||||||
|
export const packageName = '@strudel/edo';
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
intervals.mjs - defines Intervals for equal division of the octave (EDO) scale
|
||||||
|
- Port of pitfalls/lib/Intervals.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/Intervals.lua>
|
||||||
|
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/intervals.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import ratiointervals from './ratios.mjs';
|
||||||
|
|
||||||
|
function ratio(division, edivisions) {
|
||||||
|
return division === 0 ? 1 : Math.pow(2, division / edivisions);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Intervals {
|
||||||
|
constructor(scale) {
|
||||||
|
this.scale = scale;
|
||||||
|
this.intLabels = [];
|
||||||
|
this.intNoms = [];
|
||||||
|
this.intRatios = [];
|
||||||
|
this.uniqLabels = [];
|
||||||
|
this.intErrors = [];
|
||||||
|
this.ratios = [];
|
||||||
|
|
||||||
|
const BLANK = '';
|
||||||
|
let division = 0;
|
||||||
|
const labToErr = {};
|
||||||
|
const labToInd = {};
|
||||||
|
|
||||||
|
this.ratios[0] = 1;
|
||||||
|
|
||||||
|
for (let i = 0; i < scale.length; i++) {
|
||||||
|
division += scale.stepValue(i);
|
||||||
|
this.ratios[i + 1] = ratio(division, scale.edivisions);
|
||||||
|
|
||||||
|
if (i < scale.length) {
|
||||||
|
const nearest = ratiointervals.nearestInterval(this.ratios[i + 1]);
|
||||||
|
const closeness = nearest[0];
|
||||||
|
const ratio = nearest[1];
|
||||||
|
const intLabel = ratiointervals.key(ratio);
|
||||||
|
this.intLabels[i + 1] = intLabel;
|
||||||
|
this.intErrors[i + 1] = closeness;
|
||||||
|
this.intNoms[i + 1] = ratio ? ratiointervals.nom(ratio) : 0;
|
||||||
|
this.intRatios[i + 1] = ratio ? `${ratiointervals.nom(ratio)}/${ratiointervals.denom(ratio)}` : '';
|
||||||
|
this.uniqLabels[i + 1] = BLANK;
|
||||||
|
|
||||||
|
if (intLabel && intLabel !== 'P1' && intLabel !== 'P8') {
|
||||||
|
if (!labToErr[intLabel]) {
|
||||||
|
this.uniqLabels[i + 1] = intLabel;
|
||||||
|
labToInd[intLabel] = i + 1;
|
||||||
|
labToErr[intLabel] = closeness;
|
||||||
|
} else if (closeness < labToErr[intLabel]) {
|
||||||
|
this.uniqLabels[labToInd[intLabel]] = BLANK;
|
||||||
|
this.uniqLabels[i + 1] = intLabel;
|
||||||
|
labToInd[intLabel] = i + 1;
|
||||||
|
labToErr[intLabel] = closeness;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ratio(i) {
|
||||||
|
return this.ratios[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
intervalLabel(i) {
|
||||||
|
return this.intLabels[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
intervalNominator(i) {
|
||||||
|
return this.intNoms[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
intervalRatio(i) {
|
||||||
|
return this.intRatios[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
uniqIntervalLabel(i) {
|
||||||
|
return this.uniqLabels[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
intervalError(i) {
|
||||||
|
return this.intErrors[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
nearestDegreeTo(r, threshold) {
|
||||||
|
let min = 1;
|
||||||
|
let degree = null;
|
||||||
|
|
||||||
|
for (const [i, v] of Object.entries(this.ratios)) {
|
||||||
|
const diff = Math.abs((r - v) / r);
|
||||||
|
if (diff < min) {
|
||||||
|
min = diff;
|
||||||
|
degree = parseInt(i, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threshold == null) {
|
||||||
|
return degree;
|
||||||
|
} else {
|
||||||
|
return min < threshold ? degree : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "@strudel/edo",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Equal division of the octave (EDO) scale functions for strudel",
|
||||||
|
"main": "index.mjs",
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "dist/index.mjs"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"test": "vitest run",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://codeberg.org/uzu/strudel.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"tidalcycles",
|
||||||
|
"strudel",
|
||||||
|
"pattern",
|
||||||
|
"livecoding",
|
||||||
|
"algorave"
|
||||||
|
],
|
||||||
|
"author": "Rob McKinnon <rob@movingflow>",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://codeberg.org/uzu/strudel/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"@strudel/core": "workspace:*",
|
||||||
|
"@tonaljs/tonal": "^4.10.0",
|
||||||
|
"chord-voicings": "^0.0.1",
|
||||||
|
"webmidi": "^3.1.12"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^6.0.11",
|
||||||
|
"vitest": "^3.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
pitches.mjs - defines Pitches for equal division of the octave (EDO) scale
|
||||||
|
- Port of pitfalls/lib/Pitches.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/Pitches.lua>
|
||||||
|
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/pitches.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function ratio(division, edivisions) {
|
||||||
|
return division === 0 ? 1 : Math.pow(2, division / edivisions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function gets frequency based on index, edo, octave, and base frequency
|
||||||
|
function get_freq(base_freq, edo, index, oct, base_octave) {
|
||||||
|
let f = base_freq * ratio(index - 1, edo);
|
||||||
|
if (oct < base_octave) {
|
||||||
|
f /= Math.pow(2, base_octave - oct);
|
||||||
|
} else if (oct > base_octave) {
|
||||||
|
f *= Math.pow(2, oct - base_octave);
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert MIDI number to Hz using a given tuning multiplier
|
||||||
|
function midi_to_hz(n, tuning) {
|
||||||
|
return tuning * Math.pow(2, (n - 69) / 12.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const denom = Math.log(2);
|
||||||
|
function hz_to_midi(freq, tuning) {
|
||||||
|
return 12.0 * (Math.log(freq / tuning) / denom) + 69;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Pitches {
|
||||||
|
constructor(scale, intervals, tuning, midi_start, root_octave) {
|
||||||
|
this.scale = scale;
|
||||||
|
this.intervals = intervals;
|
||||||
|
this.base_freq = midi_to_hz(midi_start, tuning);
|
||||||
|
this.root_octave = root_octave;
|
||||||
|
this.freqs = {};
|
||||||
|
this.midis = {};
|
||||||
|
this.degrees = {};
|
||||||
|
this.octdegfreqs = {};
|
||||||
|
this.octdegmidis = {};
|
||||||
|
let index = 0;
|
||||||
|
let f = null;
|
||||||
|
|
||||||
|
for (let oct = 0; oct <= 8; oct++) {
|
||||||
|
this.octdegfreqs[oct] = {};
|
||||||
|
this.octdegmidis[oct] = {};
|
||||||
|
f = get_freq(this.base_freq, scale.edivisions, scale.tonic, oct, this.root_octave);
|
||||||
|
for (let deg = 0; deg < scale.length; deg++) {
|
||||||
|
index = index + 1;
|
||||||
|
this.freqs[index] = parseFloat((f * intervals.ratio(deg)).toFixed(3));
|
||||||
|
this.midis[index] = parseFloat(hz_to_midi(f * intervals.ratio(deg), tuning).toFixed(4));
|
||||||
|
this.degrees[index] = deg;
|
||||||
|
this.octdegfreqs[oct][deg + 1] = this.freqs[index];
|
||||||
|
this.octdegmidis[oct][deg + 1] = this.midis[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.base_freq = parseFloat(this.base_freq).toFixed(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
base_freq() {
|
||||||
|
return this.base_freq;
|
||||||
|
}
|
||||||
|
|
||||||
|
degree(index) {
|
||||||
|
return this.degrees[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
freq(index) {
|
||||||
|
return this.freqs[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
octdeg(deg) {
|
||||||
|
const higherOcatve = deg > this.scale.length;
|
||||||
|
const octave = this.root_octave + (higherOcatve ? Math.floor((deg - 1) / this.scale.length) : 0);
|
||||||
|
const degree = higherOcatve ? (deg % this.scale.length === 0 ? this.scale.length : deg % this.scale.length) : deg;
|
||||||
|
// console.log([octave, degree]);
|
||||||
|
return [octave, degree];
|
||||||
|
}
|
||||||
|
|
||||||
|
octdegfreq(oct, deg) {
|
||||||
|
if (this.octdegfreqs[oct]) {
|
||||||
|
return this.octdegfreqs[oct][deg];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
octdegmidi(oct, deg) {
|
||||||
|
if (this.octdegmidis[oct]) {
|
||||||
|
return this.octdegmidis[oct][deg];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
ratios.mjs - lists whole number ratios for pitch intervals
|
||||||
|
- Port of pitfalls/lib/ratios.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/ratios.lua>
|
||||||
|
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/ratios.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
const ratiointervals = {};
|
||||||
|
|
||||||
|
// FJS Calculators - https://misotanni.github.io/fjs/en/calc.html
|
||||||
|
// List of pitch intervals - https://en.wikipedia.org/wiki/List_of_pitch_intervals
|
||||||
|
// Gallery of just intervals - https://en.xen.wiki/w/Gallery_of_just_intervals
|
||||||
|
// Two letter codes changed to make different interval labels more unique.
|
||||||
|
ratiointervals.list = new Map([
|
||||||
|
[1, ['P1', 'P1', 'P1', 1, 1]], // unison P1
|
||||||
|
[16 / 15, ['m2', 'm2', 'm2_5', 16, 15]], // minor second m2
|
||||||
|
[15 / 14, ['A1', 'A1', 'A1^5_7', 15, 14]], // augmented unison
|
||||||
|
[13 / 12, ['t2', 'm2', 'm2^13', 13, 12]], // tridecimal neutral second
|
||||||
|
[12 / 11, ['N2', 'M2', 'M2_11', 12, 11]], // undecimal neutral second
|
||||||
|
[11 / 10, ['n2', 'm2', 'm2^11_5', 11, 10]], // undecimal submajor second
|
||||||
|
[10 / 9, ['T2', 'M2', 'M2^5', 10, 9]], // classic (whole) tone
|
||||||
|
[9 / 8, ['M2', 'M2', 'M2', 9, 8]], // major second M2
|
||||||
|
[8 / 7, ['S2', 'M2', 'M2_7', 8, 7]], // septimal major second
|
||||||
|
[7 / 6, ['s3', 'm3', 'm3^7', 7, 6]], // septimal minor third
|
||||||
|
[19 / 16, ['o3', 'm3', 'm3^19', 19, 16]], // otonal minor third
|
||||||
|
[6 / 5, ['m3', 'm3', 'm3_5', 6, 5]], // minor third m3
|
||||||
|
[17 / 14, ['t3', 'm3', 'm3^17_7', 17, 14]], // septendecimal supraminor third
|
||||||
|
[11 / 9, ['n3', 'm3', 'm3^11', 11, 9]], // undecimal neutral third
|
||||||
|
[5 / 4, ['M3', 'M3', 'M3^5', 5, 4]], // major third M3
|
||||||
|
[9 / 7, ['S3', 'M3', 'M3_7', 9, 7]], // septimal major third SM3
|
||||||
|
[13 / 10, ['d4', 'd4', 'd4^13_5', 13, 10]], // Barbados third
|
||||||
|
[4 / 3, ['P4', 'P4', 'P4', 4, 3]], // perfect fourth P4
|
||||||
|
[19 / 14, ['N4', 'P4', 'P4^19_7', 19, 14]], // undevicesimal wide fourth
|
||||||
|
[11 / 8, ['n4', 'P4', 'P4^11', 11, 8]], // super-fourth
|
||||||
|
[25 / 18, ['a4', 'A4', 'A4^5,5', 25, 18]], // classic augmented fourth
|
||||||
|
[7 / 5, ['sT', 'd5', 'd5^7_5', 7, 5]], // lesser septimal tritone
|
||||||
|
[45 / 32, ['A4', 'A4', 'A4^5', 45, 32]], // just augmented fourth
|
||||||
|
[17 / 12, ['d5', 'd5', 'd5^17', 17, 12]], // larger septendecimal tritone
|
||||||
|
[10 / 7, ['ST', 'A4', 'A4^5_7', 10, 7]], // greater septimal tritone
|
||||||
|
[13 / 9, ['t5', 'd5', 'd5^13', 13, 9]], // tridecimal diminished fifth
|
||||||
|
[3 / 2, ['P5', 'P5', 'P5', 3, 2]], // perfect fifth P5
|
||||||
|
[14 / 9, ['s6', 'M6', 'm6^7', 14, 9]], // subminor sixth or septimal sixth
|
||||||
|
[25 / 16, ['a5', 'A5', 'A5^5,5', 25, 16]], // classic augmented fifth
|
||||||
|
[11 / 7, ['A5', 'P5', 'P5^11_7', 11, 7]], // undecimal minor sixth
|
||||||
|
[8 / 5, ['m6', 'm6', 'm6_5', 8, 5]], // minor sixth m6
|
||||||
|
[13 / 8, ['N6', 'm6', 'm6^13', 13, 8]], // tridecimal neutral sixth
|
||||||
|
[18 / 11, ['n6', 'M6', 'M6_11', 18, 11]], // undecimal neutral sixth
|
||||||
|
[5 / 3, ['M6', 'M6', 'M6^5', 5, 3]], // just major sixth M6
|
||||||
|
[128 / 75, ['d7', 'd7', 'd7_5,5', 128, 75]], // diminished seventh
|
||||||
|
[17 / 10, ['T6', 'd7', 'd7^17_5', 17, 10]], // septendecimal diminished seventh
|
||||||
|
[12 / 7, ['S6', 'M6', 'M6_7', 12, 7]], // septimal major sixth
|
||||||
|
[7 / 4, ['s7', 'm7', 'm7^7', 7, 4]], // septimal minor seventh
|
||||||
|
[16 / 9, ['m7', 'm7', 'm7', 16, 9]], // lesser minor seventh
|
||||||
|
[9 / 5, ['g7', 'm7', 'm7_5', 9, 5]], // greater just minor seventh
|
||||||
|
[11 / 6, ['n7', 'm7', 'm7^11', 11, 6]], // undecimal neutral seventh
|
||||||
|
[13 / 7, ['N7', 'm7', 'm7^13_7', 13, 7]], // tridecimal neutral seventh
|
||||||
|
[15 / 8, ['M7', 'M7', 'M7^5', 15, 8]], // major seventh
|
||||||
|
[17 / 9, ['T7', 'd8', 'd8^17', 17, 9]], // large septendecimal major seventh
|
||||||
|
[19 / 10, ['d8', 'd8', 'd8^19_5', 19, 10]], // large undevicesimal major seventh
|
||||||
|
[2, ['P8', 'P8', 'P8', 2, 1]], // octave P8
|
||||||
|
]);
|
||||||
|
|
||||||
|
ratiointervals.key = function (ratio) {
|
||||||
|
return ratio == null ? '' : ratiointervals.list.get(ratio)?.[0] || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
ratiointervals.label = function (ratio) {
|
||||||
|
return ratio == null ? '' : ratiointervals.list.get(ratio)?.[1] || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
ratiointervals.fjs = function (ratio) {
|
||||||
|
return ratio == null ? '' : ratiointervals.list.get(ratio)?.[2] || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
ratiointervals.nom = function (ratio) {
|
||||||
|
return ratio == null ? null : ratiointervals.list.get(ratio)?.[3] || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
ratiointervals.denom = function (ratio) {
|
||||||
|
return ratio == null ? null : ratiointervals.list.get(ratio)?.[4] || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
ratiointervals.nearestInterval = function (v) {
|
||||||
|
let min = 1;
|
||||||
|
let match = null;
|
||||||
|
for (const [ratio, _labels] of ratiointervals.list) {
|
||||||
|
const diff = Math.abs((ratio - v) / ratio);
|
||||||
|
if (diff < min) {
|
||||||
|
min = diff;
|
||||||
|
match = ratio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return min < 0.01 ? [min, match] : [null, null];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ratiointervals;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
edo.test.mjs - tests of edo.mjs
|
||||||
|
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tonal/test/tonal.test.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import '../edo.mjs';
|
||||||
|
import { n } from '@strudel/core';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
describe('edoScale', () => {
|
||||||
|
it('Should run tonal functions ', () => {
|
||||||
|
// base note A3 = 220Hz
|
||||||
|
let baseNote = 'A3';
|
||||||
|
let root = 220;
|
||||||
|
let freq = 220;
|
||||||
|
let pattern = n('0').edoScale([baseNote, 'LLsLLLs', 2, 1]);
|
||||||
|
let cycle = pattern.firstCycleValues[0];
|
||||||
|
expect(cycle.freq).toEqual(freq);
|
||||||
|
expect(cycle.edo).toEqual(12);
|
||||||
|
expect(cycle.degree).toEqual(1);
|
||||||
|
expect(parseFloat(cycle.root).toFixed(0)).toEqual(root.toFixed(0));
|
||||||
|
|
||||||
|
// base note A4 = 440Hz
|
||||||
|
baseNote = 'A4';
|
||||||
|
root = 440;
|
||||||
|
freq = 880;
|
||||||
|
pattern = n('7').edoScale([baseNote, 'LLsLLLs', 2, 1]);
|
||||||
|
cycle = pattern.firstCycleValues[0];
|
||||||
|
expect(cycle.freq).toEqual(freq);
|
||||||
|
expect(cycle.edo).toEqual(12);
|
||||||
|
expect(cycle.degree).toEqual(1);
|
||||||
|
expect(parseFloat(cycle.root).toFixed(0)).toEqual(root.toFixed(0));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
edoscale.test.mjs - tests of EdoScale from edoscale.mjs
|
||||||
|
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/test/edoscale.test.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import '../edoscale.mjs'; // need to import this to add prototypes
|
||||||
|
import { EdoScale } from '../edoscale.mjs';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
describe('EdoScale', () => {
|
||||||
|
it('updates edivisions', () => {
|
||||||
|
let scale = new EdoScale(2, 1, ['L', 'L', 's', 'L', 'L', 'L', 's']);
|
||||||
|
expect(scale.stepSize(0)).toEqual('L');
|
||||||
|
expect(scale.stepValue(0)).toEqual(2);
|
||||||
|
|
||||||
|
expect(scale.stepSize(2)).toEqual('s');
|
||||||
|
expect(scale.stepValue(2)).toEqual(1);
|
||||||
|
|
||||||
|
expect(scale.edivisions).toEqual(12);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
ratios.test.mjs - tests of ratios.mjs
|
||||||
|
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/test/ratios.test.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import ratiointervals from '../ratios.mjs';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
describe('ratiointervals', () => {
|
||||||
|
it('updates edivisions', () => {
|
||||||
|
let ratio = 9 / 7;
|
||||||
|
expect(ratiointervals.key(ratio)).toEqual('S3');
|
||||||
|
expect(ratiointervals.label(ratio)).toEqual('M3');
|
||||||
|
expect(ratiointervals.fjs(ratio)).toEqual('M3_7');
|
||||||
|
expect(ratiointervals.nom(ratio)).toEqual(9);
|
||||||
|
expect(ratiointervals.denom(ratio)).toEqual(7);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { dependencies } from './package.json';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'index.mjs'),
|
||||||
|
formats: ['es'],
|
||||||
|
fileName: (ext) => ({ es: 'index.mjs' })[ext],
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: [...Object.keys(dependencies)],
|
||||||
|
},
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
});
|
||||||
+82
-71
@@ -13,7 +13,7 @@ import {
|
|||||||
getIsStarted,
|
getIsStarted,
|
||||||
getPattern,
|
getPattern,
|
||||||
getTime,
|
getTime,
|
||||||
getTriggerFunc,
|
getSchedulerFunc,
|
||||||
isPattern,
|
isPattern,
|
||||||
logger,
|
logger,
|
||||||
ref,
|
ref,
|
||||||
@@ -21,8 +21,8 @@ import {
|
|||||||
} from '@strudel/core';
|
} from '@strudel/core';
|
||||||
import { noteToMidi, getControlName } from '@strudel/core';
|
import { noteToMidi, getControlName } from '@strudel/core';
|
||||||
import { Note } from 'webmidi';
|
import { Note } from 'webmidi';
|
||||||
import { getAudioContext } from '@strudel/webaudio';
|
import { getAudioContext, getClockBridge } from '@strudel/webaudio';
|
||||||
import { scheduleAtTime } from '../superdough/helpers.mjs';
|
import { scheduleAtTime, ensureMinimalOutput } from '../superdough/helpers.mjs';
|
||||||
import { getMidiDeviceNamesString, getDevice } from './util.mjs';
|
import { getMidiDeviceNamesString, getDevice } from './util.mjs';
|
||||||
import { MidiInput } from './input.mjs';
|
import { MidiInput } from './input.mjs';
|
||||||
|
|
||||||
@@ -171,6 +171,23 @@ function normalize(value = 0, min = 0, max = 1, exp = 1) {
|
|||||||
return Math.pow(normalized, exp);
|
return Math.pow(normalized, exp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isFirefox = navigator?.userAgent?.includes('Firefox');
|
||||||
|
// call fn either directly with given time (non-firefox) or after scheduleAtTime with undefined (firefox)
|
||||||
|
// the scheduleAtTime approach is still jittery, but the best we can be on firefox
|
||||||
|
// firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=2062997
|
||||||
|
function timedSend(timeMs, fn) {
|
||||||
|
if (isFirefox) {
|
||||||
|
const audioTime = getClockBridge().getAudioContextTime(timeMs);
|
||||||
|
if (!audioTime) {
|
||||||
|
logger('[midi]: skip event, not ready');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scheduleAtTime(() => fn(undefined), audioTime);
|
||||||
|
} else {
|
||||||
|
fn(timeMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function mapCC(mapping, value) {
|
function mapCC(mapping, value) {
|
||||||
return Object.keys(value)
|
return Object.keys(value)
|
||||||
.filter((key) => !!mapping[getControlName(key)])
|
.filter((key) => !!mapping[getControlName(key)])
|
||||||
@@ -182,7 +199,7 @@ function mapCC(mapping, value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sends a cc message to the given device on the given channel
|
// sends a cc message to the given device on the given channel
|
||||||
function sendCC(ccn, ccv, device, midichan, targetTime) {
|
function sendCC(ccn, ccv, device, midichan, timeMs) {
|
||||||
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
|
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
|
||||||
throw new Error('expected ccv to be a number between 0 and 1');
|
throw new Error('expected ccv to be a number between 0 and 1');
|
||||||
}
|
}
|
||||||
@@ -190,23 +207,19 @@ function sendCC(ccn, ccv, device, midichan, targetTime) {
|
|||||||
throw new Error('expected ccn to be a number or a string');
|
throw new Error('expected ccn to be a number or a string');
|
||||||
}
|
}
|
||||||
const scaled = Math.round(ccv * 127);
|
const scaled = Math.round(ccv * 127);
|
||||||
scheduleAtTime(() => {
|
timedSend(timeMs, (timeMs) => device.sendControlChange(ccn, scaled, { channels: midichan, time: timeMs }));
|
||||||
device.sendControlChange(ccn, scaled, midichan);
|
|
||||||
}, targetTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sends a program change message to the given device on the given channel
|
// sends a program change message to the given device on the given channel
|
||||||
function sendProgramChange(progNum, device, midichan, targetTime) {
|
function sendProgramChange(progNum, device, midichan, timeMs) {
|
||||||
if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) {
|
if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) {
|
||||||
throw new Error('expected progNum (program change) to be a number between 0 and 127');
|
throw new Error('expected progNum (program change) to be a number between 0 and 127');
|
||||||
}
|
}
|
||||||
scheduleAtTime(() => {
|
timedSend(timeMs, (timeMs) => device.sendProgramChange(progNum, { channels: midichan, time: timeMs }));
|
||||||
device.sendProgramChange(progNum, midichan);
|
|
||||||
}, targetTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sends a sysex message to the given device on the given channel
|
// sends a sysex message to the given device on the given channel
|
||||||
function sendSysex(sysexid, sysexdata, device, targetTime) {
|
function sendSysex(sysexid, sysexdata, device, timeMs) {
|
||||||
if (Array.isArray(sysexid)) {
|
if (Array.isArray(sysexid)) {
|
||||||
if (!sysexid.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
if (!sysexid.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
||||||
throw new Error('all sysexid bytes must be integers between 0 and 255');
|
throw new Error('all sysexid bytes must be integers between 0 and 255');
|
||||||
@@ -221,13 +234,11 @@ function sendSysex(sysexid, sysexdata, device, targetTime) {
|
|||||||
if (!sysexdata.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
if (!sysexdata.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
||||||
throw new Error('all sysex bytes must be integers between 0 and 255');
|
throw new Error('all sysex bytes must be integers between 0 and 255');
|
||||||
}
|
}
|
||||||
scheduleAtTime(() => {
|
timedSend(timeMs, (timeMs) => device.sendSysex(sysexid, sysexdata, { time: timeMs }));
|
||||||
device.sendSysex(sysexid, sysexdata);
|
|
||||||
}, targetTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sends a NRPN message to the given device on the given channel
|
// sends a NRPN message to the given device on the given channel
|
||||||
function sendNRPN(nrpnn, nrpv, device, midichan, targetTime) {
|
function sendNRPN(nrpnn, nrpv, device, midichan, timeMs) {
|
||||||
if (Array.isArray(nrpnn)) {
|
if (Array.isArray(nrpnn)) {
|
||||||
if (!nrpnn.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
if (!nrpnn.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
||||||
throw new Error('all nrpnn bytes must be integers between 0 and 255');
|
throw new Error('all nrpnn bytes must be integers between 0 and 255');
|
||||||
@@ -235,34 +246,29 @@ function sendNRPN(nrpnn, nrpv, device, midichan, targetTime) {
|
|||||||
} else if (!Number.isInteger(nrpv) || nrpv < 0 || nrpv > 255) {
|
} else if (!Number.isInteger(nrpv) || nrpv < 0 || nrpv > 255) {
|
||||||
throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers');
|
throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers');
|
||||||
}
|
}
|
||||||
scheduleAtTime(() => {
|
|
||||||
device.sendNRPN(nrpnn, nrpv, midichan);
|
timedSend(timeMs, (timeMs) => device.sendNrpnValue(nrpnn, nrpv, { channels: midichan, time: timeMs }));
|
||||||
}, targetTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sends a pitch bend message to the given device on the given channel
|
// sends a pitch bend message to the given device on the given channel
|
||||||
function sendPitchBend(midibend, device, midichan, targetTime) {
|
function sendPitchBend(midibend, device, midichan, timeMs) {
|
||||||
if (typeof midibend !== 'number' || midibend < -1 || midibend > 1) {
|
if (typeof midibend !== 'number' || midibend < -1 || midibend > 1) {
|
||||||
throw new Error('expected midibend to be a number between -1 and 1');
|
throw new Error('expected midibend to be a number between -1 and 1');
|
||||||
}
|
}
|
||||||
scheduleAtTime(() => {
|
timedSend(timeMs, (timeMs) => device.sendPitchBend(midibend, { channels: midichan, time: timeMs }));
|
||||||
device.sendPitchBend(midibend, midichan);
|
|
||||||
}, targetTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sends a channel aftertouch message to the given device on the given channel
|
// sends a channel aftertouch message to the given device on the given channel
|
||||||
function sendAftertouch(miditouch, device, midichan, targetTime) {
|
function sendAftertouch(miditouch, device, midichan, timeMs) {
|
||||||
if (typeof miditouch !== 'number' || miditouch < 0 || miditouch > 1) {
|
if (typeof miditouch !== 'number' || miditouch < 0 || miditouch > 1) {
|
||||||
throw new Error('expected miditouch to be a number between 0 and 1');
|
throw new Error('expected miditouch to be a number between 0 and 1');
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleAtTime(() => {
|
timedSend(timeMs, (timeMs) => device.sendChannelAftertouch(miditouch, { channels: midichan, time: timeMs }));
|
||||||
device.sendChannelAftertouch(miditouch, midichan);
|
|
||||||
}, targetTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sends a note message to the given device on the given channel
|
// sends a note message to the given device on the given channel
|
||||||
function sendNote(note, velocity, duration, device, midichan, targetTime) {
|
function sendNote(note, velocity, duration, device, midichan, timeMs) {
|
||||||
if (note == null || note === '') {
|
if (note == null || note === '') {
|
||||||
throw new Error('note cannot be null or empty');
|
throw new Error('note cannot be null or empty');
|
||||||
}
|
}
|
||||||
@@ -273,11 +279,10 @@ function sendNote(note, velocity, duration, device, midichan, targetTime) {
|
|||||||
throw new Error('duration must be a positive number');
|
throw new Error('duration must be a positive number');
|
||||||
}
|
}
|
||||||
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
||||||
const midiNote = new Note(midiNumber, { attack: velocity, duration });
|
const midiNote = new Note(midiNumber, { attack: velocity });
|
||||||
|
|
||||||
scheduleAtTime(() => {
|
timedSend(timeMs, (timeMs) => device.sendNoteOn(midiNote, { channels: midichan, time: timeMs }));
|
||||||
device.playNote(midiNote, midichan);
|
timedSend(timeMs + duration, (timeMs) => device.sendNoteOff(midiNote, { channels: midichan, time: timeMs }));
|
||||||
}, targetTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -287,8 +292,6 @@ function sendNote(note, velocity, duration, device, midichan, targetTime) {
|
|||||||
* @param {object} options Additional MIDI configuration options
|
* @param {object} options Additional MIDI configuration options
|
||||||
* @example
|
* @example
|
||||||
* note("c4").midichan(1).midi('IAC Driver Bus 1')
|
* note("c4").midichan(1).midi('IAC Driver Bus 1')
|
||||||
* @example
|
|
||||||
* note("c4").midichan(1).midi('IAC Driver Bus 1', { controller: true, latency: 50 })
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Pattern.prototype.midi = function (midiport, options = {}) {
|
Pattern.prototype.midi = function (midiport, options = {}) {
|
||||||
@@ -314,7 +317,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
|||||||
let midiConfig = {
|
let midiConfig = {
|
||||||
// Default configuration values
|
// Default configuration values
|
||||||
isController: false, // Disable sending notes for midi controllers
|
isController: false, // Disable sending notes for midi controllers
|
||||||
noteOffsetMs: 10, // Default note-off offset to prevent glitching in ms
|
noteOffsetMs: isFirefox ? 10 : 1, // Default note-off offset to prevent glitching in ms. firefox needs more slack
|
||||||
midichannel: 1, // Default MIDI channel
|
midichannel: 1, // Default MIDI channel
|
||||||
velocity: 0.9, // Default velocity
|
velocity: 0.9, // Default velocity
|
||||||
gain: 1, // Default gain
|
gain: 1, // Default gain
|
||||||
@@ -337,11 +340,19 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
|||||||
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
|
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.onTrigger((hap, _currentTime, cps, targetTime) => {
|
ensureMinimalOutput();
|
||||||
|
|
||||||
|
return this.sortHapsByPart().onSchedule((hap, targetTime, cps) => {
|
||||||
if (!WebMidi.enabled) {
|
if (!WebMidi.enabled) {
|
||||||
logger('Midi not enabled');
|
logger('Midi not enabled');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const timeMs = getClockBridge().getPerformanceTime(targetTime);
|
||||||
|
if (!timeMs) {
|
||||||
|
logger('[midi] clockbridge not ready');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
hap.ensureObjectValue();
|
hap.ensureObjectValue();
|
||||||
|
|
||||||
// midi event values from hap with configurable defaults
|
// midi event values from hap with configurable defaults
|
||||||
@@ -379,7 +390,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
|||||||
// if midimap is set, send a cc messages from defined controls
|
// if midimap is set, send a cc messages from defined controls
|
||||||
if (midicontrolMap.has(midimap)) {
|
if (midicontrolMap.has(midimap)) {
|
||||||
const ccs = mapCC(midicontrolMap.get(midimap), hap.value);
|
const ccs = mapCC(midicontrolMap.get(midimap), hap.value);
|
||||||
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, targetTime));
|
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeMs));
|
||||||
} else if (midimap !== 'default') {
|
} else if (midimap !== 'default') {
|
||||||
// Add warning when a non-existent midimap is specified
|
// Add warning when a non-existent midimap is specified
|
||||||
logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`);
|
logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`);
|
||||||
@@ -387,16 +398,20 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
|||||||
|
|
||||||
// Handle note
|
// Handle note
|
||||||
if (note !== undefined && !midiConfig.isController) {
|
if (note !== undefined && !midiConfig.isController) {
|
||||||
// note off messages will often a few ms arrive late,
|
// note off time is calculated early, together with note on time
|
||||||
// try to prevent glitching by subtracting noteOffsetMs from the duration length
|
// when the note off is due, the clock might have drifted, and the next note on message might happen before the note off
|
||||||
const duration = (hap.duration.valueOf() / cps) * 1000 - midiConfig.noteOffsetMs;
|
// this would lead to the next note being cut off
|
||||||
|
// this is why we make notes shorter by noteOffsetMs, so note offs happen earlier than the note ons after
|
||||||
|
const hapDuration = (hap.duration.valueOf() / cps) * 1000;
|
||||||
|
const offset = Math.min(midiConfig.noteOffsetMs, hapDuration / 2);
|
||||||
|
const duration = hapDuration - offset;
|
||||||
|
|
||||||
sendNote(note, velocity, duration, device, midichan, targetTime);
|
sendNote(note, velocity, duration, device, midichan, timeMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle program change
|
// Handle program change
|
||||||
if (progNum !== undefined) {
|
if (progNum !== undefined) {
|
||||||
sendProgramChange(progNum, device, midichan, targetTime);
|
sendProgramChange(progNum, device, midichan, timeMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle sysex
|
// Handle sysex
|
||||||
@@ -406,63 +421,53 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
|||||||
// if sysexid is an array the first byte is 0x00
|
// if sysexid is an array the first byte is 0x00
|
||||||
|
|
||||||
if (sysexid !== undefined && sysexdata !== undefined) {
|
if (sysexid !== undefined && sysexdata !== undefined) {
|
||||||
sendSysex(sysexid, sysexdata, device, targetTime);
|
sendSysex(sysexid, sysexdata, device, timeMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle control change
|
// Handle control change
|
||||||
if (ccv !== undefined && ccn !== undefined) {
|
if (ccv !== undefined && ccn !== undefined) {
|
||||||
sendCC(ccn, ccv, device, midichan, targetTime);
|
sendCC(ccn, ccv, device, midichan, timeMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle NRPN non-registered parameter number
|
// Handle NRPN non-registered parameter number
|
||||||
if (nrpnn !== undefined && nrpv !== undefined) {
|
if (nrpnn !== undefined && nrpv !== undefined) {
|
||||||
sendNRPN(nrpnn, nrpv, device, midichan, targetTime);
|
sendNRPN(nrpnn, nrpv, device, midichan, timeMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle midibend
|
// Handle midibend
|
||||||
if (midibend !== undefined) {
|
if (midibend !== undefined) {
|
||||||
sendPitchBend(midibend, device, midichan, targetTime);
|
sendPitchBend(midibend, device, midichan, timeMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle miditouch
|
// Handle miditouch
|
||||||
if (miditouch !== undefined) {
|
if (miditouch !== undefined) {
|
||||||
sendAftertouch(miditouch, device, midichan, targetTime);
|
sendAftertouch(miditouch, device, midichan, timeMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle midicmd
|
// Handle midicmd
|
||||||
if (hap.whole.begin + 0 === 0) {
|
if (hap.whole.begin + 0 === 0) {
|
||||||
// we need to start here because we have the timing info
|
// we need to start here because we have the timing info
|
||||||
scheduleAtTime(() => {
|
timedSend(timeMs, (timeMs) => device.sendStart({ time: timeMs }));
|
||||||
device.sendStart();
|
|
||||||
}, targetTime);
|
|
||||||
}
|
}
|
||||||
if (['clock', 'midiClock'].includes(midicmd)) {
|
if (['clock', 'midiClock'].includes(midicmd)) {
|
||||||
scheduleAtTime(() => {
|
timedSend(timeMs, (timeMs) => device.sendClock({ time: timeMs }));
|
||||||
device.sendClock();
|
|
||||||
}, targetTime);
|
|
||||||
} else if (['start'].includes(midicmd)) {
|
} else if (['start'].includes(midicmd)) {
|
||||||
scheduleAtTime(() => {
|
timedSend(timeMs, (timeMs) => device.sendStart({ time: timeMs }));
|
||||||
device.sendStart();
|
|
||||||
}, targetTime);
|
|
||||||
} else if (['stop'].includes(midicmd)) {
|
} else if (['stop'].includes(midicmd)) {
|
||||||
scheduleAtTime(() => {
|
timedSend(timeMs, (timeMs) => device.sendStop({ time: timeMs }));
|
||||||
device.sendStop();
|
|
||||||
}, targetTime);
|
|
||||||
} else if (['continue'].includes(midicmd)) {
|
} else if (['continue'].includes(midicmd)) {
|
||||||
scheduleAtTime(() => {
|
timedSend(timeMs, (timeMs) => device.sendContinue({ time: timeMs }));
|
||||||
device.sendContinue();
|
|
||||||
}, targetTime);
|
|
||||||
} else if (Array.isArray(midicmd)) {
|
} else if (Array.isArray(midicmd)) {
|
||||||
if (midicmd[0] === 'progNum') {
|
if (midicmd[0] === 'progNum') {
|
||||||
sendProgramChange(midicmd[1], device, midichan, targetTime);
|
sendProgramChange(midicmd[1], device, midichan, timeMs);
|
||||||
} else if (midicmd[0] === 'cc') {
|
} else if (midicmd[0] === 'cc') {
|
||||||
if (midicmd.length === 2) {
|
if (midicmd.length === 2) {
|
||||||
sendCC(midicmd[0], midicmd[1] / 127, device, midichan, targetTime);
|
sendCC(midicmd[0], midicmd[1] / 127, device, midichan, timeMs);
|
||||||
}
|
}
|
||||||
} else if (midicmd[0] === 'sysex') {
|
} else if (midicmd[0] === 'sysex') {
|
||||||
if (midicmd.length === 3) {
|
if (midicmd.length === 3) {
|
||||||
const [_, id, data] = midicmd;
|
const [_, id, data] = midicmd;
|
||||||
sendSysex(id, data, device, targetTime);
|
sendSysex(id, data, device, timeMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -537,6 +542,9 @@ export async function midin(input) {
|
|||||||
* The note length is fixed as Superdough is not currently set up for undetermined
|
* The note length is fixed as Superdough is not currently set up for undetermined
|
||||||
* note durations
|
* 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
|
* @name midikeys
|
||||||
* @tags external_io, midi
|
* @tags external_io, midi
|
||||||
* @param {string | number} input MIDI device name or index defaulting to 0
|
* @param {string | number} input MIDI device name or index defaulting to 0
|
||||||
@@ -552,14 +560,18 @@ export async function midin(input) {
|
|||||||
* .s("saw")
|
* .s("saw")
|
||||||
* .add(note(rand.mul(0.3)))
|
* .add(note(rand.mul(0.3)))
|
||||||
* .lpf(1000).lpe(2).room(0.5)
|
* .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 kHaps = {};
|
||||||
const kListeners = {};
|
const kListeners = {};
|
||||||
|
|
||||||
function _triggerKeyboard(input, cps, now, latencyCycles) {
|
function _triggerKeyboard(input, cps, now, latencyCycles) {
|
||||||
const pattern = getPattern();
|
const pattern = getPattern();
|
||||||
const trigger = getTriggerFunc();
|
const schedule = getSchedulerFunc();
|
||||||
if (!pattern || !trigger) {
|
if (!pattern || !schedule) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const t = now + latencyCycles;
|
const t = now + latencyCycles;
|
||||||
@@ -576,8 +588,7 @@ function _triggerKeyboard(input, cps, now, latencyCycles) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const t = ctxNow + (hap.whole.begin - now) / cps;
|
const t = ctxNow + (hap.whole.begin - now) / cps;
|
||||||
const duration = hap.duration / cps;
|
schedule(hap, t, cps);
|
||||||
trigger(hap, t - ctxNow, duration, cps, t);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -612,7 +623,7 @@ export async function midikeys(input) {
|
|||||||
noteoff ||= noteon && velocity === 0; // handle devices which may use velocity = 0 to signal noteoff
|
noteoff ||= noteon && velocity === 0; // handle devices which may use velocity = 0 to signal noteoff
|
||||||
const key = `${input}_${note}`;
|
const key = `${input}_${note}`;
|
||||||
const cps = getCps() ?? 0.5;
|
const cps = getCps() ?? 0.5;
|
||||||
const triggerAvailable = !!(getPattern() && getTriggerFunc());
|
const triggerAvailable = !!(getPattern() && getSchedulerFunc());
|
||||||
const latencySeconds = triggerAvailable ? 0.01 : 0.06; // avoid missing notes due to cyclist / trigger latency
|
const latencySeconds = triggerAvailable ? 0.01 : 0.06; // avoid missing notes due to cyclist / trigger latency
|
||||||
const now = getTime();
|
const now = getTime();
|
||||||
const t = now + latencySeconds * cps;
|
const t = now + latencySeconds * cps;
|
||||||
@@ -633,7 +644,7 @@ export async function midikeys(input) {
|
|||||||
*/
|
*/
|
||||||
return;
|
return;
|
||||||
} else {
|
} 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, {}));
|
kHaps[input].push(new Hap(span, span, value, {}));
|
||||||
if (!noteoff && triggerAvailable) {
|
if (!noteoff && triggerAvailable) {
|
||||||
|
|||||||
+38
-33
@@ -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/>.
|
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, getTime, createParams } from '@strudel/core';
|
||||||
import Paho from 'paho-mqtt';
|
import Paho from 'paho-mqtt';
|
||||||
|
|
||||||
const connections = {};
|
const connections = {};
|
||||||
@@ -61,7 +61,6 @@ Pattern.prototype.mqtt = function (
|
|||||||
const props = {
|
const props = {
|
||||||
onSuccess: onConnect,
|
onSuccess: onConnect,
|
||||||
onFailure: onFailure,
|
onFailure: onFailure,
|
||||||
useSSL: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
@@ -81,41 +80,47 @@ Pattern.prototype.mqtt = function (
|
|||||||
}
|
}
|
||||||
cx.connect(props);
|
cx.connect(props);
|
||||||
}
|
}
|
||||||
return this.withHap((hap) => {
|
return this.onSchedule((hap, targetTime, cps) => {
|
||||||
const onTrigger = (hap, currentTime, cps, targetTime) => {
|
let msg_topic = topic;
|
||||||
let msg_topic = topic;
|
if (!cx || !cx.isConnected()) {
|
||||||
if (!cx || !cx.isConnected()) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
let message = '';
|
||||||
let message = '';
|
if (typeof hap.value === 'object') {
|
||||||
if (typeof hap.value === 'object') {
|
let value = hap.value;
|
||||||
let value = hap.value;
|
|
||||||
|
|
||||||
// Try to take topic from pattern if it's not set
|
// Try to take topic from pattern if it's not set
|
||||||
if (typeof msg_topic === 'undefined' && 'topic' in value) {
|
if (typeof msg_topic === 'undefined' && 'topic' in value) {
|
||||||
msg_topic = value.topic;
|
msg_topic = value.topic;
|
||||||
if (Array.isArray(msg_topic)) {
|
if (Array.isArray(msg_topic)) {
|
||||||
msg_topic = msg_topic.join('/');
|
msg_topic = msg_topic.join('/');
|
||||||
}
|
|
||||||
msg_topic = '/' + msg_topic;
|
|
||||||
}
|
}
|
||||||
if (add_meta) {
|
msg_topic = '/' + msg_topic;
|
||||||
const duration = hap.duration.div(cps);
|
|
||||||
value = { ...value, duration: duration.valueOf(), cps: cps };
|
|
||||||
}
|
|
||||||
message = JSON.stringify(value);
|
|
||||||
} else {
|
|
||||||
message = hap.value;
|
|
||||||
}
|
}
|
||||||
message = new Paho.Message(message);
|
if (add_meta) {
|
||||||
message.destinationName = msg_topic;
|
const duration = hap.duration.div(cps);
|
||||||
|
value = { ...value, duration: duration.valueOf(), cps: cps };
|
||||||
|
}
|
||||||
|
message = JSON.stringify(value);
|
||||||
|
} else {
|
||||||
|
message = hap.value;
|
||||||
|
}
|
||||||
|
message = new Paho.Message(message);
|
||||||
|
message.destinationName = msg_topic;
|
||||||
|
|
||||||
const offset = (targetTime - currentTime + latency) * 1000;
|
const offset = (targetTime - getTime() + latency) * 1000;
|
||||||
|
|
||||||
window.setTimeout(function () {
|
window.setTimeout(function () {
|
||||||
cx.send(message);
|
cx.send(message);
|
||||||
}, offset);
|
}, offset);
|
||||||
};
|
|
||||||
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);
|
||||||
|
};
|
||||||
|
|||||||
+16
-8
@@ -4,7 +4,8 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core';
|
import { logger, parseNumeral, register, isNote, noteToMidi } from '@strudel/core';
|
||||||
|
import { ensureMinimalOutput, getClockBridge } from '@strudel/webaudio';
|
||||||
|
|
||||||
let connection; // Promise<OSC>
|
let connection; // Promise<OSC>
|
||||||
function connect() {
|
function connect() {
|
||||||
@@ -42,10 +43,9 @@ export function parseControlsFromHap(hap, cps) {
|
|||||||
if (isNote(controls.note)) {
|
if (isNote(controls.note)) {
|
||||||
controls.midinote = noteToMidi(controls.note, controls.octave || 3);
|
controls.midinote = noteToMidi(controls.note, controls.octave || 3);
|
||||||
} else {
|
} else {
|
||||||
controls.note = parseNumeral(controls.note);
|
controls.midinote = parseNumeral(controls.note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
controls.bank && (controls.s = controls.bank + controls.s);
|
|
||||||
controls.roomsize && (controls.size = parseNumeral(controls.roomsize));
|
controls.roomsize && (controls.size = parseNumeral(controls.roomsize));
|
||||||
// speed adjustment for CPS is handled on the DSP side in superdirt and pattern side in Strudel,
|
// speed adjustment for CPS is handled on the DSP side in superdirt and pattern side in Strudel,
|
||||||
// so we need to undo the adjustment before sending the message to superdirt.
|
// so we need to undo the adjustment before sending the message to superdirt.
|
||||||
@@ -55,13 +55,18 @@ export function parseControlsFromHap(hap, cps) {
|
|||||||
return controls;
|
return controls;
|
||||||
}
|
}
|
||||||
|
|
||||||
const collator = new ClockCollator({});
|
export async function oscScheduler(hap, targetTime, cps = 1) {
|
||||||
|
|
||||||
export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
|
|
||||||
const ws = await connect();
|
const ws = await connect();
|
||||||
|
|
||||||
|
const timeMs = getClockBridge().getPerformanceTime(targetTime);
|
||||||
|
if (!timeMs) {
|
||||||
|
logger('[osc] clockbridge not ready');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ts = performance.timeOrigin + timeMs;
|
||||||
|
|
||||||
const controls = parseControlsFromHap(hap, cps);
|
const controls = parseControlsFromHap(hap, cps);
|
||||||
const keyvals = Object.entries(controls).flat();
|
const keyvals = Object.entries(controls).flat();
|
||||||
const ts = collator.calculateTimestamp(currentTime, targetTime) * 1000;
|
|
||||||
const msg = { address: '/dirt/play', args: keyvals, timestamp: ts };
|
const msg = { address: '/dirt/play', args: keyvals, timestamp: ts };
|
||||||
|
|
||||||
if ('oschost' in hap.value) {
|
if ('oschost' in hap.value) {
|
||||||
@@ -83,4 +88,7 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
*/
|
*/
|
||||||
export const osc = register('osc', (pat) => pat.onTrigger(oscTrigger));
|
export const osc = register('osc', (pat) => {
|
||||||
|
ensureMinimalOutput();
|
||||||
|
return pat.onSchedule(oscScheduler);
|
||||||
|
});
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/webaudio": "workspace:*",
|
||||||
"osc": "^2.4.5",
|
"osc": "^2.4.5",
|
||||||
"ws": "^8.18.3"
|
"ws": "^8.18.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
|
/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
|
||||||
import { isTauri } from '../desktopbridge/utils.mjs'; */
|
import { isTauri } from '../desktopbridge/utils.mjs'; */
|
||||||
import { oscTrigger } from './osc.mjs';
|
import { oscScheduler } from './osc.mjs';
|
||||||
|
|
||||||
const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger;
|
const scheduler = /* isTauri() ? oscTriggerTauri : */ oscScheduler;
|
||||||
|
|
||||||
export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => {
|
export const superdirtOutput = (hap, targetTime, cps) => scheduler(hap, targetTime, cps);
|
||||||
const currentTime = performance.now() / 1000;
|
|
||||||
return trigger(hap, currentTime, cps, targetTime);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"@strudel/codemirror": "workspace:*",
|
"@strudel/codemirror": "workspace:*",
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
|
"@strudel/edo": "workspace:*",
|
||||||
"@strudel/hydra": "workspace:*",
|
"@strudel/hydra": "workspace:*",
|
||||||
"@strudel/midi": "workspace:*",
|
"@strudel/midi": "workspace:*",
|
||||||
"@strudel/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export async function prebake() {
|
|||||||
core,
|
core,
|
||||||
import('@strudel/draw'),
|
import('@strudel/draw'),
|
||||||
import('@strudel/mini'),
|
import('@strudel/mini'),
|
||||||
|
import('@strudel/edo'),
|
||||||
import('@strudel/tonal'),
|
import('@strudel/tonal'),
|
||||||
import('@strudel/webaudio'),
|
import('@strudel/webaudio'),
|
||||||
import('@strudel/codemirror'),
|
import('@strudel/codemirror'),
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ if (args.includes('--json')) {
|
|||||||
}
|
}
|
||||||
writeFileSync(outFile, json, 'utf8');
|
writeFileSync(outFile, json, 'utf8');
|
||||||
console.log(`Wrote json to ${outFile}`);
|
console.log(`Wrote json to ${outFile}`);
|
||||||
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
+40
-43
@@ -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/>.
|
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, getTime } from '@strudel/core';
|
||||||
|
|
||||||
var writeMessagers = {};
|
var writeMessagers = {};
|
||||||
var choosing = false;
|
var choosing = false;
|
||||||
@@ -64,53 +64,50 @@ function crc16(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids = false, name = 'default') {
|
Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids = false, name = 'default') {
|
||||||
return this.withHap((hap) => {
|
if (!(name in writeMessagers)) {
|
||||||
if (!(name in writeMessagers)) {
|
getWriter(name, br);
|
||||||
getWriter(name, br);
|
}
|
||||||
}
|
return this.onSchedule((hap, targetTime) => {
|
||||||
const onTrigger = (hap, currentTime, _cps, targetTime) => {
|
var message = '';
|
||||||
var message = '';
|
var chk = 0;
|
||||||
var chk = 0;
|
if (typeof hap.value === 'object') {
|
||||||
if (typeof hap.value === 'object') {
|
if ('action' in hap.value) {
|
||||||
if ('action' in hap.value) {
|
var action = hap.value['action'];
|
||||||
var action = hap.value['action'];
|
if (singlecharids) {
|
||||||
|
action = action.charAt(0);
|
||||||
|
}
|
||||||
|
message += action + '(';
|
||||||
|
var first = true;
|
||||||
|
for (var [key, val] of Object.entries(hap.value)) {
|
||||||
|
if (key === 'action') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
message += ',';
|
||||||
|
}
|
||||||
if (singlecharids) {
|
if (singlecharids) {
|
||||||
action = action.charAt(0);
|
key = key.charAt(0);
|
||||||
}
|
|
||||||
message += action + '(';
|
|
||||||
var first = true;
|
|
||||||
for (var [key, val] of Object.entries(hap.value)) {
|
|
||||||
if (key === 'action') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (first) {
|
|
||||||
first = false;
|
|
||||||
} else {
|
|
||||||
message += ',';
|
|
||||||
}
|
|
||||||
if (singlecharids) {
|
|
||||||
key = key.charAt(0);
|
|
||||||
}
|
|
||||||
message += key + ':' + val;
|
|
||||||
}
|
|
||||||
message += ')';
|
|
||||||
if (sendcrc) {
|
|
||||||
chk = crc16(message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const [key, val] of Object.entries(hap.value)) {
|
|
||||||
message += `${key}:${val}`;
|
|
||||||
}
|
}
|
||||||
|
message += key + ':' + val;
|
||||||
|
}
|
||||||
|
message += ')';
|
||||||
|
if (sendcrc) {
|
||||||
|
chk = crc16(message);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
message = hap.value;
|
for (const [key, val] of Object.entries(hap.value)) {
|
||||||
|
message += `${key}:${val}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const offset = (targetTime - currentTime + latency) * 1000;
|
} else {
|
||||||
|
message = hap.value;
|
||||||
|
}
|
||||||
|
const offset = (targetTime - getTime() + latency) * 1000;
|
||||||
|
|
||||||
window.setTimeout(function () {
|
window.setTimeout(function () {
|
||||||
writeMessagers[name](message, chk);
|
writeMessagers[name](message, chk);
|
||||||
}, offset);
|
}, offset);
|
||||||
};
|
|
||||||
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getAudioContext, registerSound } from '@strudel/webaudio';
|
|||||||
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
|
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
|
||||||
|
|
||||||
Pattern.prototype.soundfont = function (sf, n = 0) {
|
Pattern.prototype.soundfont = function (sf, n = 0) {
|
||||||
return this.onTrigger((h, ct, cps, targetTime) => {
|
return this.onSchedule((h, targetTime) => {
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
const note = getPlayableNoteValue(h);
|
const note = getPlayableNoteValue(h);
|
||||||
const preset = sf.presets[n % sf.presets.length];
|
const preset = sf.presets[n % sf.presets.length];
|
||||||
|
|||||||
@@ -7,15 +7,24 @@ Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let audioContext;
|
import { clearNodePool } from './nodePools.mjs';
|
||||||
|
import { ClockBridge } from './clockbridge.mjs';
|
||||||
|
|
||||||
|
let audioContext, clockBridge;
|
||||||
|
|
||||||
export const setDefaultAudioContext = () => {
|
export const setDefaultAudioContext = () => {
|
||||||
audioContext = new AudioContext();
|
return setAudioContext(new AudioContext());
|
||||||
return audioContext;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setAudioContext = (context) => {
|
export const setAudioContext = (context) => {
|
||||||
|
// Existing nodes in the node pool contain references to the previous AudioContext,
|
||||||
|
// so all the nodes in the pool must be cleared when we set a new AudioContext.
|
||||||
|
clearNodePool();
|
||||||
|
if (audioContext && audioContext.state !== 'closed') {
|
||||||
|
audioContext.close();
|
||||||
|
}
|
||||||
audioContext = context;
|
audioContext = context;
|
||||||
|
clockBridge = new ClockBridge(audioContext);
|
||||||
return audioContext;
|
return audioContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,6 +36,13 @@ export const getAudioContext = () => {
|
|||||||
return audioContext;
|
return audioContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getClockBridge() {
|
||||||
|
if (!clockBridge) {
|
||||||
|
getAudioContext(); // creates clockBridge
|
||||||
|
}
|
||||||
|
return clockBridge;
|
||||||
|
}
|
||||||
|
|
||||||
export function getAudioContextCurrentTime() {
|
export function getAudioContextCurrentTime() {
|
||||||
return getAudioContext().currentTime;
|
return getAudioContext().currentTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
clockbridge.mjs
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/index.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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// thanks freya https://youtu.be/LSNQuFEDOyQ?si=ukZI2IGgWV_NDZzP&t=2979
|
||||||
|
export function expDecay(a, b, decay, dt) {
|
||||||
|
return b + (a - b) * Math.exp(-decay * dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// translates between audio and performance clock
|
||||||
|
export class ClockBridge {
|
||||||
|
p; // smoothed clock offset
|
||||||
|
lastTime;
|
||||||
|
audioContext;
|
||||||
|
constructor(audioContext) {
|
||||||
|
this.audioContext = audioContext;
|
||||||
|
}
|
||||||
|
// delta between audio and performance time in ms
|
||||||
|
getOffset() {
|
||||||
|
const { contextTime, performanceTime } = this.audioContext.getOutputTimestamp();
|
||||||
|
if (!contextTime || !performanceTime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const offset = performanceTime - contextTime * 1000; // clock offset in ms
|
||||||
|
const dt = performanceTime - (this.lastTime ?? performanceTime); // delta time since last hap
|
||||||
|
const decay = 1 / 10000; // how fast offset changes have an effect
|
||||||
|
this.p = expDecay(this.p ?? offset, offset, decay, dt); // smooth clock offset
|
||||||
|
this.lastTime = performanceTime;
|
||||||
|
return this.p;
|
||||||
|
}
|
||||||
|
getPerformanceTime(audioContextTime) {
|
||||||
|
const offset = this.getOffset();
|
||||||
|
return audioContextTime * 1000 + offset; // this is now correct in performance time (ms)
|
||||||
|
}
|
||||||
|
getAudioContextTime(performanceTime) {
|
||||||
|
const offset = this.getOffset();
|
||||||
|
return (performanceTime - offset) / 1000; // this is now correct in audio context time (seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,13 +67,14 @@ if (typeof window !== 'undefined') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dough = async (code) => {
|
export const rawdsp = async (code) => {
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
stop();
|
stop();
|
||||||
worklet = await dspWorklet(ac, code);
|
worklet = await dspWorklet(ac, code);
|
||||||
worklet.node.connect(ac.destination);
|
worklet.node.connect(ac.destination);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function doughTrigger(hap, currentTime, cps, targetTime) {
|
export function rawdspScheduler(hap, time, cps) {
|
||||||
window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps });
|
const currentTime = getAudioContext().currentTime;
|
||||||
|
window.postMessage({ time, dough: hap.value, currentTime, duration: hap.duration, cps });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,27 @@ import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
|||||||
|
|
||||||
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||||
|
|
||||||
export function gainNode(value) {
|
export function gainNode(value, audioContext = getAudioContext()) {
|
||||||
const node = getAudioContext().createGain();
|
const node = audioContext.createGain();
|
||||||
node.gain.value = value;
|
node.gain.value = value;
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this helper makes sure the audio context is "used", meaning it outputs something
|
||||||
|
// this prevents the browser from throttling timing accuracy
|
||||||
|
// it happened when only midi was running, the clock got more drifty without this
|
||||||
|
let constantNode, constantNodeAudioContext;
|
||||||
|
export function ensureMinimalOutput() {
|
||||||
|
if (constantNode && constantNodeAudioContext === getAudioContext()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
constantNodeAudioContext = getAudioContext();
|
||||||
|
constantNode = new ConstantSourceNode(constantNodeAudioContext);
|
||||||
|
constantNode.offset.value = 1e-7;
|
||||||
|
constantNode.connect(constantNodeAudioContext.destination);
|
||||||
|
constantNode.start();
|
||||||
|
}
|
||||||
|
|
||||||
export function effectSend(input, effect, wet) {
|
export function effectSend(input, effect, wet) {
|
||||||
const send = gainNode(wet);
|
const send = gainNode(wet);
|
||||||
input.connect(send);
|
input.connect(send);
|
||||||
@@ -374,7 +389,7 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
|||||||
|
|
||||||
// Certain browsers requires audio nodes to be connected in order for their onended events
|
// Certain browsers requires audio nodes to be connected in order for their onended events
|
||||||
// to fire, so we _mute it_ and then connect it to the destination
|
// to fire, so we _mute it_ and then connect it to the destination
|
||||||
const zeroGain = gainNode(0);
|
const zeroGain = gainNode(0, audioContext);
|
||||||
zeroGain.connect(audioContext.destination);
|
zeroGain.connect(audioContext.destination);
|
||||||
constantNode.connect(zeroGain);
|
constantNode.connect(zeroGain);
|
||||||
|
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ export * from './modulators.mjs';
|
|||||||
export * from './dspworklet.mjs';
|
export * from './dspworklet.mjs';
|
||||||
export * from './audioContext.mjs';
|
export * from './audioContext.mjs';
|
||||||
export * from './wavetable.mjs';
|
export * from './wavetable.mjs';
|
||||||
|
export * from './clockbridge.mjs';
|
||||||
|
|||||||
@@ -5,11 +5,24 @@ Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { releaseAudioNode } from './helpers.mjs';
|
||||||
|
|
||||||
const nodePools = new Map();
|
const nodePools = new Map();
|
||||||
const POOL_KEY = Symbol('nodePoolKey');
|
const POOL_KEY = Symbol('nodePoolKey');
|
||||||
|
|
||||||
export const isPoolable = (node) => !!node[POOL_KEY];
|
export const isPoolable = (node) => !!node[POOL_KEY];
|
||||||
|
|
||||||
|
export const clearNodePool = () => {
|
||||||
|
for (const pool of nodePools) {
|
||||||
|
for (const node of pool) {
|
||||||
|
if (node instanceof AudioNode) {
|
||||||
|
releaseAudioNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodePools.clear();
|
||||||
|
};
|
||||||
|
|
||||||
const getNodeTime = (node) => {
|
const getNodeTime = (node) => {
|
||||||
return node.context?.currentTime ?? 0;
|
return node.context?.currentTime ?? 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BASE_MIDI_NOTE, getBaseURL, getCommonSampleInfo, noteToFreq, noteToMidi } from './util.mjs';
|
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
||||||
import { registerSound, registerWaveTable } from './index.mjs';
|
import { registerSound, registerWaveTable, soundMap } from './index.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import {
|
import {
|
||||||
getADSRValues,
|
getADSRValues,
|
||||||
@@ -14,13 +14,6 @@ import { logger } from './logger.mjs';
|
|||||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||||
const loadCache = {}; // string: Promise<ArrayBuffer>
|
const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @typedef {Object} SampleMetaData
|
|
||||||
* @property {string} url
|
|
||||||
* @property {midi} number
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const getCachedBuffer = (url) => bufferCache[url];
|
export const getCachedBuffer = (url) => bufferCache[url];
|
||||||
|
|
||||||
function humanFileSize(bytes, si) {
|
function humanFileSize(bytes, si) {
|
||||||
@@ -37,11 +30,33 @@ function humanFileSize(bytes, si) {
|
|||||||
return bytes.toFixed(1) + ' ' + units[u];
|
return bytes.toFixed(1) + ' ' + units[u];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSampleInfo(hapValue, bank) {
|
/**
|
||||||
const { speed = 1.0 } = hapValue;
|
* Returns the duration, in seconds, of the given sample.
|
||||||
const { transpose, url, index, midi, label, baseFrequency } = getCommonSampleInfo(hapValue, bank);
|
* 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);
|
||||||
|
};
|
||||||
|
|
||||||
const playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
export const getDur = getDuration;
|
||||||
|
|
||||||
|
export function getSampleInfo(hapValue, bank) {
|
||||||
|
const { speed = 1.0 } = hapValue;
|
||||||
|
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
|
||||||
|
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
||||||
return { transpose, url, index, midi, label, playbackRate };
|
return { transpose, url, index, midi, label, playbackRate };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,12 +179,7 @@ export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '')
|
|||||||
if (baseUrl.startsWith('github:')) {
|
if (baseUrl.startsWith('github:')) {
|
||||||
baseUrl = githubPath(baseUrl, '');
|
baseUrl = githubPath(baseUrl, '');
|
||||||
}
|
}
|
||||||
/**
|
const fullUrl = (v) => baseUrl + v;
|
||||||
*
|
|
||||||
* @param {string} v
|
|
||||||
* @returns {SampleMetaData}
|
|
||||||
*/
|
|
||||||
const fullUrl = (v) => ({ url: baseUrl + v, midi: extractMidiNoteFromString(v) });
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
//return [key, value.map(replaceUrl)];
|
//return [key, value.map(replaceUrl)];
|
||||||
value = value.map(fullUrl);
|
value = value.map(fullUrl);
|
||||||
@@ -380,16 +390,3 @@ export function registerSampleSource(key, bank, params) {
|
|||||||
registerSample(key, bank, params);
|
registerSample(key, bank, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractMidiNoteFromString(str) {
|
|
||||||
const regex = /_([a-gA-G])([#b])?([1-9])?\b/;
|
|
||||||
const match = str.match(regex);
|
|
||||||
if (match == null) {
|
|
||||||
return BASE_MIDI_NOTE;
|
|
||||||
}
|
|
||||||
const base = match[1].toUpperCase();
|
|
||||||
const accidental = match[2] ?? '';
|
|
||||||
const octave_str = match[3] ?? '';
|
|
||||||
const parsedVal = base + accidental + octave_str;
|
|
||||||
return noteToMidi(parsedVal);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
import { map } from 'nanostores';
|
import { map } from 'nanostores';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { connectLFO, connectEnvelope, connectBusModulator } from './modulators.mjs';
|
import { connectLFO, connectEnvelope, connectBusModulator } from './modulators.mjs';
|
||||||
import { loadBuffer } from './sampler.mjs';
|
import { getSampleBufferSource, loadBuffer } from './sampler.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
||||||
import { resetSeenKeys } from './wavetable.mjs';
|
import { resetSeenKeys } from './wavetable.mjs';
|
||||||
@@ -1048,7 +1048,3 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const superdoughTrigger = (t, hap, ct, cps) => {
|
|
||||||
superdough(hap, t - ct, hap.duration / cps, cps);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -485,8 +485,10 @@ export function waveformN(partials, phases, type) {
|
|||||||
if (phase !== 0) {
|
if (phase !== 0) {
|
||||||
const c = Math.cos(PI2 * phase);
|
const c = Math.cos(PI2 * phase);
|
||||||
const s = Math.sin(PI2 * phase);
|
const s = Math.sin(PI2 * phase);
|
||||||
R = c * R - s * I;
|
const R0 = R;
|
||||||
I = s * R + c * I;
|
const I0 = I;
|
||||||
|
R = c * R0 - s * I0;
|
||||||
|
I = s * R0 + c * I0;
|
||||||
}
|
}
|
||||||
real[n + 1] = R;
|
real[n + 1] = R;
|
||||||
imag[n + 1] = I;
|
imag[n + 1] = I;
|
||||||
|
|||||||
@@ -19,12 +19,7 @@ const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
|||||||
export const getAccidentalsOffset = (accidentals) => {
|
export const getAccidentalsOffset = (accidentals) => {
|
||||||
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} note
|
|
||||||
* @param {number} defaultOctave
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
export const noteToMidi = (note, defaultOctave = 3) => {
|
export const noteToMidi = (note, defaultOctave = 3) => {
|
||||||
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
@@ -34,11 +29,6 @@ export const noteToMidi = (note, defaultOctave = 3) => {
|
|||||||
const offset = getAccidentalsOffset(acc);
|
const offset = getAccidentalsOffset(acc);
|
||||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {number} n
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
export const midiToFreq = (n) => {
|
export const midiToFreq = (n) => {
|
||||||
return Math.pow(2, (n - 69) / 12) * 440;
|
return Math.pow(2, (n - 69) / 12) * 440;
|
||||||
};
|
};
|
||||||
@@ -48,17 +38,7 @@ export const freqToMidi = (freq) => {
|
|||||||
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
|
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export const valueToMidi = (value, fallbackValue) => {
|
||||||
*
|
|
||||||
* @param {string} note
|
|
||||||
* @param {number} defaultOctave
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const noteToFreq = (note, defaultOctave = 3) => {
|
|
||||||
return midiToFreq(noteToMidi(note, defaultOctave));
|
|
||||||
};
|
|
||||||
function __valueToMidi(value) {
|
|
||||||
if (typeof value !== 'object') {
|
if (typeof value !== 'object') {
|
||||||
throw new Error('valueToMidi: expected object value');
|
throw new Error('valueToMidi: expected object value');
|
||||||
}
|
}
|
||||||
@@ -72,15 +52,10 @@ function __valueToMidi(value) {
|
|||||||
if (typeof note === 'number') {
|
if (typeof note === 'number') {
|
||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
return;
|
if (!fallbackValue) {
|
||||||
}
|
|
||||||
|
|
||||||
export const valueToMidi = (value, fallbackValue) => {
|
|
||||||
const parsedValue = __valueToMidi(value) ?? fallbackValue;
|
|
||||||
if (parsedValue == null) {
|
|
||||||
throw new Error('valueToMidi: expected freq or note to be set');
|
throw new Error('valueToMidi: expected freq or note to be set');
|
||||||
}
|
}
|
||||||
return parsedValue;
|
return fallbackValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function nanFallback(value, fallback = 0, silent) {
|
export function nanFallback(value, fallback = 0, silent) {
|
||||||
@@ -109,18 +84,15 @@ export function secondsToCycle(t, cps) {
|
|||||||
// deduces relevant info for sample loading from hap.value and sample definition
|
// deduces relevant info for sample loading from hap.value and sample definition
|
||||||
// it encapsulates the core sampler logic into a pure and synchronous function
|
// it encapsulates the core sampler logic into a pure and synchronous function
|
||||||
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
||||||
export const BASE_MIDI_NOTE = 36;
|
|
||||||
|
|
||||||
export function getCommonSampleInfo(hapValue, bank) {
|
export function getCommonSampleInfo(hapValue, bank) {
|
||||||
const { s, n = 0 } = hapValue;
|
const { s, n = 0 } = hapValue;
|
||||||
const maybeMidiNote = __valueToMidi(hapValue);
|
let midi = valueToMidi(hapValue, 36);
|
||||||
const midi = maybeMidiNote ?? BASE_MIDI_NOTE;
|
let transpose = midi - 36; // C3 is middle C;
|
||||||
let transpose = midi - BASE_MIDI_NOTE; // C3 is middle C;
|
let url;
|
||||||
let index = 0;
|
let index = 0;
|
||||||
let samplemeta;
|
|
||||||
if (Array.isArray(bank)) {
|
if (Array.isArray(bank)) {
|
||||||
index = getSoundIndex(n, bank.length);
|
index = getSoundIndex(n, bank.length);
|
||||||
samplemeta = bank[index];
|
url = bank[index];
|
||||||
} else {
|
} else {
|
||||||
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
||||||
// object format will expect keys as notes
|
// object format will expect keys as notes
|
||||||
@@ -132,14 +104,10 @@ export function getCommonSampleInfo(hapValue, bank) {
|
|||||||
);
|
);
|
||||||
transpose = -midiDiff(closest); // semitones to repitch
|
transpose = -midiDiff(closest); // semitones to repitch
|
||||||
index = getSoundIndex(n, bank[closest].length);
|
index = getSoundIndex(n, bank[closest].length);
|
||||||
samplemeta = bank[closest][index];
|
url = bank[closest][index];
|
||||||
}
|
}
|
||||||
const label = `${s}:${index}`;
|
const label = `${s}:${index}`;
|
||||||
if (maybeMidiNote != null) {
|
return { transpose, url, index, midi, label };
|
||||||
transpose = transpose + (BASE_MIDI_NOTE - samplemeta.midi);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { transpose, index, midi, label, url: samplemeta.url };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Selects entries from `source` and renames them via `map`
|
/** Selects entries from `source` and renames them via `map`
|
||||||
|
|||||||
@@ -137,7 +137,14 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
|
|||||||
const interval = !isNaN(Number(intervalOrSemitones))
|
const interval = !isNaN(Number(intervalOrSemitones))
|
||||||
? Interval.fromSemitones(intervalOrSemitones)
|
? Interval.fromSemitones(intervalOrSemitones)
|
||||||
: String(intervalOrSemitones);
|
: String(intervalOrSemitones);
|
||||||
const targetNote = Note.transpose(note, interval);
|
|
||||||
|
let n = Note.get(note);
|
||||||
|
if (n.oct == undefined) {
|
||||||
|
n.oct = 3;
|
||||||
|
let n1 = Note.get(Note.transpose(n, interval));
|
||||||
|
n.oct = n1.oct == 3 ? undefined : 3;
|
||||||
|
}
|
||||||
|
const targetNote = Note.transpose(n, interval);
|
||||||
if (typeof hap.value === 'object') {
|
if (typeof hap.value === 'object') {
|
||||||
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
|
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,6 +134,16 @@ let modeTarget = {
|
|||||||
duck: (v) => v.slice(-1)[0],
|
duck: (v) => v.slice(-1)[0],
|
||||||
above: (v) => v[0],
|
above: (v) => v[0],
|
||||||
root: (v) => v[0],
|
root: (v) => v[0],
|
||||||
|
oldabove: (v) => v[0],
|
||||||
|
oldroot: (v) => v[0],
|
||||||
|
};
|
||||||
|
let modeMult = {
|
||||||
|
below: 1,
|
||||||
|
duck: 1,
|
||||||
|
above: -1,
|
||||||
|
root: -1,
|
||||||
|
oldabove: 1,
|
||||||
|
oldroot: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below', anchor = 'c5', octaves = 1 }) {
|
export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below', anchor = 'c5', octaves = 1 }) {
|
||||||
@@ -144,19 +154,20 @@ export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below'
|
|||||||
const voicings = dictionary[symbol].map((voicing) =>
|
const voicings = dictionary[symbol].map((voicing) =>
|
||||||
(typeof voicing === 'string' ? voicing.split(' ') : voicing).map(step2semitones),
|
(typeof voicing === 'string' ? voicing.split(' ') : voicing).map(step2semitones),
|
||||||
);
|
);
|
||||||
|
const mult = modeMult[mode];
|
||||||
|
|
||||||
let minDistance, bestIndex;
|
let minDistance, bestIndex;
|
||||||
// calculate distances up from voicing top notes
|
// calculate distances up from voicing top notes
|
||||||
let chromaDiffs = voicings.map((v, i) => {
|
let chromaDiffs = voicings.map((v, i) => {
|
||||||
const targetStep = modeTarget[mode](v);
|
const targetStep = modeTarget[mode](v);
|
||||||
const diff = _mod(anchorChroma - targetStep - rootChroma, 12);
|
const diff = _mod((anchorChroma - targetStep - rootChroma) * mult, 12);
|
||||||
if (minDistance === undefined || diff < minDistance) {
|
if (minDistance === undefined || diff < minDistance) {
|
||||||
minDistance = diff;
|
minDistance = diff;
|
||||||
bestIndex = i;
|
bestIndex = i;
|
||||||
}
|
}
|
||||||
return diff;
|
return diff * mult;
|
||||||
});
|
});
|
||||||
if (mode === 'root') {
|
if (mode === 'root' || mode === 'oldroot') {
|
||||||
bestIndex = 0;
|
bestIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -185,6 +185,9 @@ export const rootNotes = register('rootNotes', function (octave, pat) {
|
|||||||
* - `below`: top note <= anchor
|
* - `below`: top note <= anchor
|
||||||
* - `duck`: top note <= anchor, anchor excluded
|
* - `duck`: top note <= anchor, anchor excluded
|
||||||
* - `above`: bottom note >= anchor
|
* - `above`: bottom note >= anchor
|
||||||
|
* - `root`: bottom note is the lowest root of the chord >= anchor
|
||||||
|
* - `oldabove` : old (buggy) behavior of above, kept for legacy reason
|
||||||
|
* - `oldroot` : old (buggy) behavior of root, kept for legacy reason
|
||||||
* - `offset`: whole number that shifts the voicing up or down to the next voicing
|
* - `offset`: whole number that shifts the voicing up or down to the next voicing
|
||||||
* - `n`: if set, the voicing is played like a scale. Overshooting numbers will be octaved
|
* - `n`: if set, the voicing is played like a scale. Overshooting numbers will be octaved
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,10 +1,31 @@
|
|||||||
# @strudel/transpiler
|
# @strudel/transpiler
|
||||||
|
|
||||||
This package contains a JS code transpiler with the following features:
|
This package contains a JS code transpiler with a plugin system that can be used to transform the code.
|
||||||
|
|
||||||
|
A plugin can be registered via a `registerTranspilerPlugin` call :
|
||||||
|
|
||||||
|
```js
|
||||||
|
registerTranspilerPlugin({
|
||||||
|
walk: (context) => ({
|
||||||
|
enter: function(node, parent, prop, index) {},
|
||||||
|
leave: function(node, parent, prop, index) {}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
where the enter/leave functions (both optional) follow the `estree-walker` walk API.
|
||||||
|
|
||||||
|
4 plugins are currently hosted inside `@strudel/transpiler`
|
||||||
|
|
||||||
|
- mini: add locations of mini notation strings (double quoted or backticked) for highlighting
|
||||||
|
- widgets: add handling of sliders & draw widgets
|
||||||
|
- sample: make it possible to call a sample without await
|
||||||
|
- kabelsalat: transform the code to handle the `K(..)` kabelsalat notation
|
||||||
|
|
||||||
|
it also
|
||||||
|
|
||||||
- add locations of mini notation strings (double quoted or backticked) for highlighting
|
|
||||||
- converts pseudo note variables to note strings
|
|
||||||
- adds return statement to the last expression
|
- adds return statement to the last expression
|
||||||
|
- handles label capturing for block-based eval
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
|
/*
|
||||||
|
index.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
import { evaluate as _evaluate } from '@strudel/core';
|
import { evaluate as _evaluate } from '@strudel/core';
|
||||||
import { transpiler } from './transpiler.mjs';
|
import { transpiler } from './transpiler.mjs';
|
||||||
|
|
||||||
export * from './transpiler.mjs';
|
export * from './transpiler.mjs';
|
||||||
|
|
||||||
export const evaluate = (code) => _evaluate(code, transpiler);
|
import './plugin-kabelsalat.mjs';
|
||||||
|
import './plugin-mini.mjs';
|
||||||
|
import './plugin-sample.mjs';
|
||||||
|
import './plugin-widgets.mjs';
|
||||||
|
|
||||||
|
export { registerWidgetType, getWidgetID } from './plugin-widgets.mjs';
|
||||||
|
export const evaluate = (code, transpilerOptions) => _evaluate(code, transpiler, transpilerOptions);
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
/*
|
||||||
|
plugin-kabelsalat.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { walk } from 'estree-walker';
|
||||||
|
import escodegen from 'escodegen';
|
||||||
|
import { getLanguages, registerTranspilerPlugin } from './transpiler.mjs';
|
||||||
|
|
||||||
|
export function genExprSource(expr) {
|
||||||
|
return escodegen.generate(expr, { format: { semicolons: false } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStrudelPatternWrap(node) {
|
||||||
|
if (node.type !== 'CallExpression') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const callee = node.callee;
|
||||||
|
if (callee.type === 'Identifier') {
|
||||||
|
return callee.name === 'S';
|
||||||
|
}
|
||||||
|
if (callee.type === 'MemberExpression' && !callee.computed) {
|
||||||
|
return callee.property?.name === 'S';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to identify transpiled `m(...)` calls for proper conversion
|
||||||
|
// to, say, kabelsalat placeholders
|
||||||
|
function isMiniCall(node) {
|
||||||
|
if (node.type !== 'CallExpression') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const callee = node.callee;
|
||||||
|
if (callee.type !== 'Identifier') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (callee.name !== getMinilangName()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const firstArg = node.arguments?.[0];
|
||||||
|
return firstArg?.type === 'Literal' && typeof firstArg.value === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMinilangName() {
|
||||||
|
const minilang = getLanguages().get('minilang');
|
||||||
|
return minilang?.name || 'm';
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceNode(node, replacement, parents, currentRoot) {
|
||||||
|
const info = parents.get(node);
|
||||||
|
if (!info || !info.parent) {
|
||||||
|
return replacement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { parent, prop, index } = info;
|
||||||
|
if (Array.isArray(parent[prop])) {
|
||||||
|
parent[prop][index] = replacement;
|
||||||
|
} else {
|
||||||
|
parent[prop] = replacement;
|
||||||
|
}
|
||||||
|
parents.set(replacement, { parent, prop, index });
|
||||||
|
return currentRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If `start` is available, we use it. If it's already been transpiled
|
||||||
|
// to `m(...)`, use the provided offset
|
||||||
|
function getPatternNodeOrder(node) {
|
||||||
|
if (typeof node.start === 'number') {
|
||||||
|
return node.start;
|
||||||
|
}
|
||||||
|
if (isMiniCall(node)) {
|
||||||
|
const offsetArg = node.arguments?.[1];
|
||||||
|
if (offsetArg?.type === 'Literal' && typeof offsetArg.value === 'number') {
|
||||||
|
return offsetArg.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeholderAst(index) {
|
||||||
|
return {
|
||||||
|
type: 'MemberExpression',
|
||||||
|
object: { type: 'Identifier', name: 'pat' },
|
||||||
|
property: { type: 'Literal', value: index },
|
||||||
|
computed: true,
|
||||||
|
optional: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStrudelPatternExpr(node) {
|
||||||
|
if (isStrudelPatternWrap(node)) {
|
||||||
|
const arg = node.arguments?.[0];
|
||||||
|
if (!arg) {
|
||||||
|
throw new Error('S(...) requires an argument');
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
if (isMiniCall(node)) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneNode(node) {
|
||||||
|
return JSON.parse(JSON.stringify(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractPatternPlaceholders(expr) {
|
||||||
|
const templateExpr = cloneNode(expr);
|
||||||
|
const parents = new Map();
|
||||||
|
const targets = [];
|
||||||
|
|
||||||
|
walk(templateExpr, {
|
||||||
|
enter(node, parent, prop, index) {
|
||||||
|
parents.set(node, { parent, prop, index });
|
||||||
|
const patternExpr = getStrudelPatternExpr(node);
|
||||||
|
if (patternExpr) {
|
||||||
|
targets.push({ node, patternExpr });
|
||||||
|
this.skip();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!targets.length) {
|
||||||
|
return { template: genExprSource(templateExpr), patternExprs: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
targets.sort((a, b) => getPatternNodeOrder(a.node) - getPatternNodeOrder(b.node));
|
||||||
|
|
||||||
|
const patternExprs = targets.map(({ patternExpr }) => cloneNode(patternExpr));
|
||||||
|
|
||||||
|
let currentExpr = templateExpr;
|
||||||
|
targets.forEach(({ node }, index) => {
|
||||||
|
currentExpr = replaceNode(node, placeholderAst(index), parents, currentExpr);
|
||||||
|
});
|
||||||
|
|
||||||
|
const template = genExprSource(currentExpr);
|
||||||
|
return { template, patternExprs };
|
||||||
|
}
|
||||||
|
|
||||||
|
const transpilerPlugin = {
|
||||||
|
walk: (context) => ({
|
||||||
|
leave: function (node, parent, prop, index) {
|
||||||
|
if (!isKabelCall(node)) return;
|
||||||
|
let [expr, ...rest] = node.arguments;
|
||||||
|
if (!expr) throw new Error('K(...) requires an expression');
|
||||||
|
if (shouldCallKabelExpression(expr)) {
|
||||||
|
expr = {
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: expr,
|
||||||
|
arguments: [],
|
||||||
|
optional: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const language = 'kabelsalat';
|
||||||
|
const { template, patternExprs } = extractPatternPlaceholders(expr);
|
||||||
|
if (patternExprs.length) {
|
||||||
|
const workletArgs = [
|
||||||
|
/*{ type: 'Literal', value: language },*/
|
||||||
|
{ type: 'Literal', value: template },
|
||||||
|
...patternExprs,
|
||||||
|
...rest,
|
||||||
|
];
|
||||||
|
let callee = node.callee;
|
||||||
|
if (callee.type === 'ChainExpression') callee = callee.expression;
|
||||||
|
if (callee.type === 'MemberExpression') {
|
||||||
|
return this.replace({
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: workletMemberAst(callee.object),
|
||||||
|
arguments: workletArgs,
|
||||||
|
optional: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.replace({
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: { type: 'Identifier', name: 'worklet' },
|
||||||
|
arguments: workletArgs,
|
||||||
|
optional: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const kabelSrc = genExprSource(expr);
|
||||||
|
const workletArgs = [/*{ type: 'Literal', value: language },*/ { type: 'Literal', value: kabelSrc }, ...rest];
|
||||||
|
|
||||||
|
let callee = node.callee;
|
||||||
|
if (callee.type === 'ChainExpression') callee = callee.expression;
|
||||||
|
if (callee.type === 'MemberExpression') {
|
||||||
|
return this.replace({
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: workletMemberAst(callee.object),
|
||||||
|
arguments: workletArgs,
|
||||||
|
optional: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.replace({
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: { type: 'Identifier', name: 'worklet' },
|
||||||
|
arguments: workletArgs,
|
||||||
|
optional: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
registerTranspilerPlugin(transpilerPlugin);
|
||||||
|
|
||||||
|
function isKabelCall(node) {
|
||||||
|
if (node.type !== 'CallExpression') return false;
|
||||||
|
let callee = node.callee;
|
||||||
|
if (callee.type === 'ChainExpression') callee = callee.expression;
|
||||||
|
if (callee.type === 'MemberExpression') return !callee.computed && callee.property?.name === 'K';
|
||||||
|
return callee.type === 'Identifier' && callee.name === 'K';
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldCallKabelExpression(expr) {
|
||||||
|
if (expr.type !== 'ArrowFunctionExpression' && expr.type !== 'FunctionExpression') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (expr.params.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return expr.body?.type === 'BlockStatement';
|
||||||
|
}
|
||||||
|
|
||||||
|
function workletMemberAst(objectExpr) {
|
||||||
|
return {
|
||||||
|
type: 'MemberExpression',
|
||||||
|
object: objectExpr,
|
||||||
|
property: { type: 'Identifier', name: 'worklet' },
|
||||||
|
computed: false,
|
||||||
|
optional: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
/*
|
||||||
|
plugin-mini.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import { getLeafLocations } from '@strudel/mini';
|
||||||
|
import { getLanguages, registerTranspilerPlugin } from './transpiler.mjs';
|
||||||
|
|
||||||
|
const languageLiteral = {
|
||||||
|
walk: (context) => ({
|
||||||
|
enter: function (node, parent, prop, index) {
|
||||||
|
if (!isLanguageLiteral(node)) return;
|
||||||
|
context.miniLocations ??= [];
|
||||||
|
const { options, miniLocations } = context;
|
||||||
|
const { emitMiniLocations } = options;
|
||||||
|
const { name } = node.tag;
|
||||||
|
const language = getLanguages().get(name);
|
||||||
|
const code = node.quasi.quasis[0].value.raw;
|
||||||
|
const offset = node.quasi.start + 1;
|
||||||
|
if (emitMiniLocations) {
|
||||||
|
const locs = language.getLocations(code, offset);
|
||||||
|
miniLocations.push(...locs);
|
||||||
|
}
|
||||||
|
this.skip();
|
||||||
|
return this.replace(languageWithLocation(name, code, offset));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const tidal = {
|
||||||
|
walk: (context) => ({
|
||||||
|
enter: function (node, parent, prop, index) {
|
||||||
|
if (!isTemplateLiteral(node, 'tidal')) return;
|
||||||
|
context.miniLocations ??= [];
|
||||||
|
const { options, miniLocations } = context;
|
||||||
|
const { emitMiniLocations } = options;
|
||||||
|
const raw = node.quasi.quasis[0].value.raw;
|
||||||
|
const offset = node.quasi.start + 1;
|
||||||
|
if (emitMiniLocations) {
|
||||||
|
const stringLocs = collectHaskellMiniLocations(raw, offset);
|
||||||
|
miniLocations.push(...stringLocs);
|
||||||
|
}
|
||||||
|
this.skip();
|
||||||
|
return this.replace(tidalWithLocation(raw, offset));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const backtick = {
|
||||||
|
walk: (context) => ({
|
||||||
|
enter: function (node, parent, prop, index) {
|
||||||
|
if (!isBackTickString(node, parent)) return;
|
||||||
|
context.miniLocations ??= [];
|
||||||
|
const { options, input, miniDisableRanges, miniLocations } = context;
|
||||||
|
const { emitMiniLocations } = options;
|
||||||
|
if (isMiniDisabled(node.start, miniDisableRanges)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { quasis } = node;
|
||||||
|
const { raw } = quasis[0].value;
|
||||||
|
this.skip();
|
||||||
|
emitMiniLocations && collectMiniLocations(raw, node, miniLocations, input);
|
||||||
|
return this.replace(miniWithLocation(raw, node));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const doublequotes = {
|
||||||
|
walk: (context) => ({
|
||||||
|
enter: function (node, parent, prop, index) {
|
||||||
|
if (!isStringWithDoubleQuotes(node)) return;
|
||||||
|
context.miniLocations ??= [];
|
||||||
|
const { options, input, miniDisableRanges, miniLocations } = context;
|
||||||
|
const { emitMiniLocations } = options;
|
||||||
|
if (isMiniDisabled(node.start, miniDisableRanges)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { value } = node;
|
||||||
|
this.skip();
|
||||||
|
emitMiniLocations && collectMiniLocations(value, node, miniLocations, input);
|
||||||
|
return this.replace(miniWithLocation(value, node));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
function isLanguageLiteral(node) {
|
||||||
|
return node.type === 'TaggedTemplateExpression' && getLanguages().has(node.tag.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function languageWithLocation(name, value, offset) {
|
||||||
|
return {
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name: name,
|
||||||
|
},
|
||||||
|
arguments: [
|
||||||
|
{ type: 'Literal', value },
|
||||||
|
{ type: 'Literal', value: offset },
|
||||||
|
],
|
||||||
|
optional: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectHaskellMiniLocations(haskellCode, offset) {
|
||||||
|
return haskellCode
|
||||||
|
.split('')
|
||||||
|
.reduce((acc, char, i) => {
|
||||||
|
if (char !== '"') {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
if (!acc.length || acc[acc.length - 1].length > 1) {
|
||||||
|
acc.push([i + 1]);
|
||||||
|
} else {
|
||||||
|
acc[acc.length - 1].push(i);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [])
|
||||||
|
.map(([start, end]) => {
|
||||||
|
const miniString = haskellCode.slice(start, end);
|
||||||
|
return getLeafLocations(`"${miniString}"`, offset + start - 1);
|
||||||
|
})
|
||||||
|
.flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTemplateLiteral(node, value) {
|
||||||
|
return node.type === 'TaggedTemplateExpression' && node.tag.name === value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tidalWithLocation(value, offset) {
|
||||||
|
return {
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name: 'tidal',
|
||||||
|
},
|
||||||
|
arguments: [
|
||||||
|
{ type: 'Literal', value },
|
||||||
|
{ type: 'Literal', value: offset },
|
||||||
|
],
|
||||||
|
optional: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBackTickString(node, parent) {
|
||||||
|
return node.type === 'TemplateLiteral' && parent.type !== 'TaggedTemplateExpression';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMiniDisabled(offset, miniDisableRanges) {
|
||||||
|
for (const [start, end] of miniDisableRanges) {
|
||||||
|
if (offset >= start && offset < end) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function miniWithLocation(value, node) {
|
||||||
|
const { start: fromOffset } = node;
|
||||||
|
|
||||||
|
const minilang = getLanguages().get('minilang');
|
||||||
|
let name = 'm';
|
||||||
|
if (minilang && minilang.name) {
|
||||||
|
name = minilang.name; // name is expected to be exported from the package of the minilang
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
arguments: [
|
||||||
|
{ type: 'Literal', value },
|
||||||
|
{ type: 'Literal', value: fromOffset },
|
||||||
|
],
|
||||||
|
optional: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectMiniLocations = (value, node, miniLocations, input) => {
|
||||||
|
const minilang = getLanguages().get('minilang');
|
||||||
|
if (minilang) {
|
||||||
|
const code = `[${value}]`;
|
||||||
|
const locs = minilang.getLocations(code, node.start);
|
||||||
|
miniLocations.push(...locs);
|
||||||
|
} else {
|
||||||
|
const leafLocs = getLeafLocations(`"${value}"`, node.start, input);
|
||||||
|
miniLocations.push(...leafLocs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function isStringWithDoubleQuotes(node, locations, code) {
|
||||||
|
if (node.type !== 'Literal') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return node.raw[0] === '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
registerTranspilerPlugin([languageLiteral, tidal, backtick, doublequotes]);
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
plugin-sample.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { registerTranspilerPlugin } from './transpiler.mjs';
|
||||||
|
|
||||||
|
export function withAwait(node) {
|
||||||
|
return {
|
||||||
|
type: 'AwaitExpression',
|
||||||
|
argument: node,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const bareSample = {
|
||||||
|
walk: (context) => ({
|
||||||
|
enter: function (node, parent, prop, index) {
|
||||||
|
if (!isBareSamplesCall(node, parent)) return;
|
||||||
|
this.replace(withAwait(node));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
function isBareSamplesCall(node, parent) {
|
||||||
|
return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression';
|
||||||
|
}
|
||||||
|
|
||||||
|
registerTranspilerPlugin(bareSample);
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
plugin-widgets.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { registerTranspilerPlugin } from './transpiler.mjs';
|
||||||
|
|
||||||
|
let widgetMethods = [];
|
||||||
|
export function registerWidgetType(type) {
|
||||||
|
widgetMethods.push(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
const widgetTranspilerPlugin = {
|
||||||
|
walk: (context) => ({
|
||||||
|
enter: function (node, parent, prop, index) {
|
||||||
|
if (!isWidgetMethod(node)) return;
|
||||||
|
context.widgets ??= [];
|
||||||
|
const { widgets, options } = context;
|
||||||
|
const { emitWidgets } = options;
|
||||||
|
const type = node.callee.property.name;
|
||||||
|
const idx = widgets.filter((w) => w.type === type).length;
|
||||||
|
const widgetConfig = {
|
||||||
|
from: node.start,
|
||||||
|
to: node.end,
|
||||||
|
index: idx,
|
||||||
|
type,
|
||||||
|
id: options.id,
|
||||||
|
};
|
||||||
|
emitWidgets && widgets.push(widgetConfig);
|
||||||
|
return this.replace(widgetWithLocation(node, widgetConfig));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const sliderTranspilerPlugin = {
|
||||||
|
walk: (context) => ({
|
||||||
|
enter: function (node, parent, prop, index) {
|
||||||
|
if (!isSliderFunction(node)) return;
|
||||||
|
context.widgets ??= [];
|
||||||
|
context.sliders ??= [];
|
||||||
|
const { options, widgets, sliders, nodeOffset } = context;
|
||||||
|
const { emitWidgets } = options;
|
||||||
|
const from = node.arguments[0].start + nodeOffset;
|
||||||
|
const to = node.arguments[0].end + nodeOffset;
|
||||||
|
const id = `${from}:${to}`; // Range-based ID for stability
|
||||||
|
const sliderConfig = {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
id,
|
||||||
|
value: node.arguments[0].raw, // don't use value!
|
||||||
|
min: node.arguments[1]?.value ?? 0,
|
||||||
|
max: node.arguments[2]?.value ?? 1,
|
||||||
|
step: node.arguments[3]?.value,
|
||||||
|
type: 'slider',
|
||||||
|
};
|
||||||
|
emitWidgets && widgets.push(sliderConfig);
|
||||||
|
sliders.push(sliderConfig);
|
||||||
|
return this.replace(sliderWithLocation(node, nodeOffset));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const widgetTranspilerPlugins = [sliderTranspilerPlugin, widgetTranspilerPlugin];
|
||||||
|
|
||||||
|
// these functions are connected to @strudel/codemirror -> slider.mjs
|
||||||
|
// maybe someday there will be pluggable transpiler functions, then move this there
|
||||||
|
function isSliderFunction(node) {
|
||||||
|
return node.type === 'CallExpression' && node.callee.name === 'slider';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWidgetMethod(node) {
|
||||||
|
return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sliderWithLocation(node, nodeOffset = 0) {
|
||||||
|
// Apply nodeOffset for block-based evaluation to generate correct range
|
||||||
|
const from = node.arguments[0].start + nodeOffset;
|
||||||
|
const to = node.arguments[0].end + nodeOffset;
|
||||||
|
|
||||||
|
// Use range-based ID for stability during block evaluation
|
||||||
|
const id = `${from}:${to}`;
|
||||||
|
|
||||||
|
// add loc as identifier to first argument
|
||||||
|
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
||||||
|
node.arguments.unshift({
|
||||||
|
type: 'Literal',
|
||||||
|
value: id,
|
||||||
|
raw: id,
|
||||||
|
});
|
||||||
|
node.callee.name = 'sliderWithID';
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWidgetID(widgetConfig) {
|
||||||
|
// the widget id is used as id for the dom element + as key for eventual resources
|
||||||
|
// for example, for each scope widget, a new analyser + buffer (large) is created
|
||||||
|
// Update: use range-based ID generation for better stability during block evaluation
|
||||||
|
// When we have both from and to, use them together for stability
|
||||||
|
// Otherwise fall back to position-based ID for backward compatibility
|
||||||
|
let uniqueIdentifier;
|
||||||
|
if (widgetConfig.from !== undefined && widgetConfig.to !== undefined) {
|
||||||
|
// Use range for more stable identification
|
||||||
|
uniqueIdentifier = `${widgetConfig.from}-${widgetConfig.to}`;
|
||||||
|
} else {
|
||||||
|
// Fallback to single position (for backward compatibility)
|
||||||
|
uniqueIdentifier = widgetConfig.to || widgetConfig.from || 0;
|
||||||
|
}
|
||||||
|
const baseId = `${widgetConfig.id || ''}_widget_${widgetConfig.type}`;
|
||||||
|
return `${baseId}_${widgetConfig.index}_${uniqueIdentifier}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function widgetWithLocation(node, widgetConfig) {
|
||||||
|
const id = getWidgetID(widgetConfig);
|
||||||
|
// Store the unique ID back into the config so it's available for widget management
|
||||||
|
// This is critical for block-based evaluation to match existing widgets with new ones
|
||||||
|
widgetConfig.id = id;
|
||||||
|
// add loc as identifier to first argument
|
||||||
|
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
||||||
|
node.arguments.unshift({
|
||||||
|
type: 'Literal',
|
||||||
|
value: id,
|
||||||
|
raw: id,
|
||||||
|
});
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerTranspilerPlugin(widgetTranspilerPlugins);
|
||||||
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { transpiler } from '../transpiler.mjs';
|
import { transpiler } from '../index.mjs';
|
||||||
|
|
||||||
const simple = { wrapAsync: false, addReturn: false, simpleLocs: true };
|
const simple = { wrapAsync: false, addReturn: false, simpleLocs: true };
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { getLeafLocations } from '@strudel/mini';
|
/*
|
||||||
|
transpiler.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
import { parse } from 'acorn';
|
import { parse } from 'acorn';
|
||||||
import escodegen from 'escodegen';
|
import escodegen from 'escodegen';
|
||||||
import { walk } from 'estree-walker';
|
import { walk } from 'estree-walker';
|
||||||
|
|
||||||
let widgetMethods = [];
|
|
||||||
export function registerWidgetType(type) {
|
|
||||||
widgetMethods.push(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
let languages = new Map();
|
let languages = new Map();
|
||||||
// config = { getLocations: (code: string, offset?: number) => number[][] }
|
// config = { getLocations: (code: string, offset?: number) => number[][] }
|
||||||
// see mondough.mjs for example use
|
// see mondough.mjs for example use
|
||||||
@@ -17,16 +16,31 @@ let languages = new Map();
|
|||||||
export function registerLanguage(type, config) {
|
export function registerLanguage(type, config) {
|
||||||
languages.set(type, config);
|
languages.set(type, config);
|
||||||
}
|
}
|
||||||
|
export function getLanguages() {
|
||||||
|
return languages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugins = [];
|
||||||
|
|
||||||
|
export function registerTranspilerPlugin(plugin) {
|
||||||
|
plugins.push(plugin);
|
||||||
|
}
|
||||||
|
export function getPlugins() {
|
||||||
|
return plugins.flat(Infinity);
|
||||||
|
}
|
||||||
|
|
||||||
export function transpiler(input, options = {}) {
|
export function transpiler(input, options = {}) {
|
||||||
const {
|
options = {
|
||||||
wrapAsync = false,
|
wrapAsync: false,
|
||||||
addReturn = true,
|
addReturn: true,
|
||||||
emitMiniLocations = true,
|
emitMiniLocations: true,
|
||||||
emitWidgets = true,
|
emitWidgets: true,
|
||||||
blockBased = false,
|
blockBased: false,
|
||||||
range = [],
|
range: [],
|
||||||
} = options;
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { wrapAsync, addReturn, emitMiniLocations, emitWidgets, blockBased, range } = options;
|
||||||
|
|
||||||
const comments = [];
|
const comments = [];
|
||||||
let ast = parse(input, {
|
let ast = parse(input, {
|
||||||
@@ -36,8 +50,7 @@ export function transpiler(input, options = {}) {
|
|||||||
onComment: comments,
|
onComment: comments,
|
||||||
});
|
});
|
||||||
|
|
||||||
const miniDisableRanges = findMiniDisableRanges(comments, input.length);
|
let miniDisableRanges = findMiniDisableRanges(comments, input.length);
|
||||||
let miniLocations = [];
|
|
||||||
|
|
||||||
// Position offset for block-based evaluation
|
// Position offset for block-based evaluation
|
||||||
let nodeOffset = range && range.length > 0 ? range[0] : 0;
|
let nodeOffset = range && range.length > 0 ? range[0] : 0;
|
||||||
@@ -45,23 +58,13 @@ export function transpiler(input, options = {}) {
|
|||||||
// Track declarations to add to strudelScope for block-based eval
|
// Track declarations to add to strudelScope for block-based eval
|
||||||
let scopeDeclarations = [];
|
let scopeDeclarations = [];
|
||||||
|
|
||||||
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 sliders = [];
|
|
||||||
let labels = [];
|
let labels = [];
|
||||||
|
|
||||||
|
const context = { options, input, nodeOffset, miniDisableRanges, labels };
|
||||||
|
const plugins = getPlugins().map((plugin) => plugin.walk?.(context));
|
||||||
|
|
||||||
walk(ast, {
|
walk(ast, {
|
||||||
enter(node, parent /* , prop, index */) {
|
enter(node, parent, prop, index) {
|
||||||
// Apply position offset for block-based evaluation
|
// Apply position offset for block-based evaluation
|
||||||
if (blockBased && node.start !== undefined) {
|
if (blockBased && node.start !== undefined) {
|
||||||
node.start = node.start + nodeOffset;
|
node.start = node.start + nodeOffset;
|
||||||
@@ -79,82 +82,12 @@ export function transpiler(input, options = {}) {
|
|||||||
scopeDeclarations.push(node.id.name);
|
scopeDeclarations.push(node.id.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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);
|
|
||||||
}
|
|
||||||
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)) {
|
|
||||||
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 = {
|
for (const plugin of plugins) {
|
||||||
from,
|
if (!plugin?.enter?.call(this, node, parent, prop, index)) continue;
|
||||||
to,
|
return;
|
||||||
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));
|
|
||||||
}
|
|
||||||
if (isWidgetMethod(node)) {
|
|
||||||
const type = node.callee.property.name;
|
|
||||||
const index = widgets.filter((w) => w.type === type).length;
|
|
||||||
const widgetConfig = {
|
|
||||||
from: node.start,
|
|
||||||
to: node.end,
|
|
||||||
index,
|
|
||||||
type,
|
|
||||||
id: options.id,
|
|
||||||
};
|
|
||||||
emitWidgets && widgets.push(widgetConfig);
|
|
||||||
return this.replace(widgetWithLocation(node, widgetConfig));
|
|
||||||
}
|
|
||||||
if (isBareSamplesCall(node, parent)) {
|
|
||||||
return this.replace(withAwait(node));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLabelStatement(node)) {
|
if (isLabelStatement(node)) {
|
||||||
// Collect label info for block-based evaluation
|
// Collect label info for block-based evaluation
|
||||||
// Store positions WITHOUT offset so repl can slice the transpiler output correctly
|
// Store positions WITHOUT offset so repl can slice the transpiler output correctly
|
||||||
@@ -183,61 +116,10 @@ export function transpiler(input, options = {}) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
leave(node, parent, prop, index) {
|
leave(node, parent, prop, index) {
|
||||||
if (!isKabelCall(node)) return;
|
for (const plugin of plugins) {
|
||||||
|
if (!plugin?.leave?.call(this, node, parent, prop, index)) continue;
|
||||||
let [expr, ...rest] = node.arguments;
|
return;
|
||||||
if (!expr) throw new Error('K(...) requires an expression');
|
|
||||||
|
|
||||||
if (shouldCallKabelExpression(expr)) {
|
|
||||||
expr = {
|
|
||||||
type: 'CallExpression',
|
|
||||||
callee: expr,
|
|
||||||
arguments: [],
|
|
||||||
optional: false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { template, patternExprs } = extractPatternPlaceholders(expr);
|
|
||||||
if (patternExprs.length) {
|
|
||||||
const workletArgs = [{ type: 'Literal', value: template }, ...patternExprs, ...rest];
|
|
||||||
|
|
||||||
let callee = node.callee;
|
|
||||||
if (callee.type === 'ChainExpression') callee = callee.expression;
|
|
||||||
if (callee.type === 'MemberExpression') {
|
|
||||||
return this.replace({
|
|
||||||
type: 'CallExpression',
|
|
||||||
callee: workletMemberAst(callee.object),
|
|
||||||
arguments: workletArgs,
|
|
||||||
optional: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.replace({
|
|
||||||
type: 'CallExpression',
|
|
||||||
callee: { type: 'Identifier', name: 'worklet' },
|
|
||||||
arguments: workletArgs,
|
|
||||||
optional: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const kabelSrc = genExprSource(expr);
|
|
||||||
const workletArgs = [{ type: 'Literal', value: kabelSrc }, ...rest];
|
|
||||||
|
|
||||||
let callee = node.callee;
|
|
||||||
if (callee.type === 'ChainExpression') callee = callee.expression;
|
|
||||||
if (callee.type === 'MemberExpression') {
|
|
||||||
return this.replace({
|
|
||||||
type: 'CallExpression',
|
|
||||||
callee: workletMemberAst(callee.object),
|
|
||||||
arguments: workletArgs,
|
|
||||||
optional: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.replace({
|
|
||||||
type: 'CallExpression',
|
|
||||||
callee: { type: 'Identifier', name: 'worklet' },
|
|
||||||
arguments: workletArgs,
|
|
||||||
optional: false,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -256,12 +138,7 @@ export function transpiler(input, options = {}) {
|
|||||||
body.push(silenceExpression);
|
body.push(silenceExpression);
|
||||||
} else if (!body?.[body.length - 1]?.expression) {
|
} else if (!body?.[body.length - 1]?.expression) {
|
||||||
// Last statement is not an expression (e.g., VariableDeclaration, FunctionDeclaration)
|
// Last statement is not an expression (e.g., VariableDeclaration, FunctionDeclaration)
|
||||||
if (blockBased) {
|
body.push(silenceExpression);
|
||||||
// For block-based eval, add silence as the return value when block ends with declaration
|
|
||||||
body.push(silenceExpression);
|
|
||||||
} else {
|
|
||||||
throw new Error('unexpected ast format without body expression');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For block-based eval, add scope assignments before the return statement
|
// For block-based eval, add scope assignments before the return statement
|
||||||
@@ -287,280 +164,17 @@ export function transpiler(input, options = {}) {
|
|||||||
if (!emitMiniLocations) {
|
if (!emitMiniLocations) {
|
||||||
return { output };
|
return { output };
|
||||||
}
|
}
|
||||||
return { output, miniLocations, widgets, sliders, labels };
|
|
||||||
}
|
|
||||||
|
|
||||||
function isKabelCall(node) {
|
let pluginContext;
|
||||||
if (node.type !== 'CallExpression') return false;
|
({ options, input, miniDisableRanges, nodeOffset, ...pluginContext } = context);
|
||||||
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) {
|
return { output, ...pluginContext };
|
||||||
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, 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isBareSamplesCall(node, parent) {
|
|
||||||
return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAllCall(node) {
|
function isAllCall(node) {
|
||||||
return node.type === 'CallExpression' && node.callee.name === 'all';
|
return node.type === 'CallExpression' && node.callee.name === 'all';
|
||||||
}
|
}
|
||||||
|
|
||||||
function withAwait(node) {
|
|
||||||
return {
|
|
||||||
type: 'AwaitExpression',
|
|
||||||
argument: node,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLabelStatement(node) {
|
function isLabelStatement(node) {
|
||||||
return node.type === 'LabeledStatement';
|
return node.type === 'LabeledStatement';
|
||||||
}
|
}
|
||||||
@@ -591,69 +205,6 @@ function labelToP(node) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLanguageLiteral(node) {
|
|
||||||
return node.type === 'TaggedTemplateExpression' && languages.has(node.tag.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 isTemplateLiteral(node, value) {
|
|
||||||
return node.type === 'TaggedTemplateExpression' && node.tag.name === value;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 tidalWithLocation(value, offset) {
|
|
||||||
return {
|
|
||||||
type: 'CallExpression',
|
|
||||||
callee: {
|
|
||||||
type: 'Identifier',
|
|
||||||
name: 'tidal',
|
|
||||||
},
|
|
||||||
arguments: [
|
|
||||||
{ type: 'Literal', value },
|
|
||||||
{ type: 'Literal', value: offset },
|
|
||||||
],
|
|
||||||
optional: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function languageWithLocation(name, value, offset) {
|
|
||||||
return {
|
|
||||||
type: 'CallExpression',
|
|
||||||
callee: {
|
|
||||||
type: 'Identifier',
|
|
||||||
name: name,
|
|
||||||
},
|
|
||||||
arguments: [
|
|
||||||
{ type: 'Literal', value },
|
|
||||||
{ type: 'Literal', value: offset },
|
|
||||||
],
|
|
||||||
optional: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// List of non-inline widgets that need cleanup
|
// List of non-inline widgets that need cleanup
|
||||||
// These are Pattern.prototype methods that create persistent visualizations
|
// These are Pattern.prototype methods that create persistent visualizations
|
||||||
// (should be repalced by a function call producing an actual list of registered widgets)
|
// (should be repalced by a function call producing an actual list of registered widgets)
|
||||||
@@ -795,12 +346,3 @@ function findMiniDisableRanges(comments, codeEnd) {
|
|||||||
}
|
}
|
||||||
return ranges;
|
return ranges;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMiniDisabled(offset, miniDisableRanges) {
|
|
||||||
for (const [start, end] of miniDisableRanges) {
|
|
||||||
if (offset >= start && offset < end) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/edo": "workspace:*",
|
||||||
"@strudel/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
"@strudel/tonal": "workspace:*",
|
"@strudel/tonal": "workspace:*",
|
||||||
"@strudel/transpiler": "workspace:*",
|
"@strudel/transpiler": "workspace:*",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export * from '@strudel/webaudio';
|
|||||||
//export * from '@strudel/soundfonts';
|
//export * from '@strudel/soundfonts';
|
||||||
export * from '@strudel/transpiler';
|
export * from '@strudel/transpiler';
|
||||||
export * from '@strudel/mini';
|
export * from '@strudel/mini';
|
||||||
|
export * from '@strudel/edo';
|
||||||
export * from '@strudel/tonal';
|
export * from '@strudel/tonal';
|
||||||
export * from '@strudel/webaudio';
|
export * from '@strudel/webaudio';
|
||||||
import { Pattern, evalScope, setTime } from '@strudel/core';
|
import { Pattern, evalScope, setTime } from '@strudel/core';
|
||||||
@@ -17,6 +18,7 @@ export async function defaultPrebake() {
|
|||||||
evalScope,
|
evalScope,
|
||||||
import('@strudel/core'),
|
import('@strudel/core'),
|
||||||
import('@strudel/mini'),
|
import('@strudel/mini'),
|
||||||
|
import('@strudel/edo'),
|
||||||
import('@strudel/tonal'),
|
import('@strudel/tonal'),
|
||||||
import('@strudel/webaudio'),
|
import('@strudel/webaudio'),
|
||||||
{ hush, evaluate },
|
{ hush, evaluate },
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const soundMap = new Map();
|
|||||||
const loadedSounds = new Map();
|
const loadedSounds = new Map();
|
||||||
|
|
||||||
Pattern.prototype.supradough = function () {
|
Pattern.prototype.supradough = function () {
|
||||||
return this.onTrigger((hap, __, cps, begin) => {
|
return this.onSchedule((hap, begin, cps) => {
|
||||||
hap.value._begin = begin;
|
hap.value._begin = begin;
|
||||||
hap.value._duration = hap.duration / cps;
|
hap.value._duration = hap.duration / cps;
|
||||||
!doughWorklet && initDoughWorklet();
|
!doughWorklet && initDoughWorklet();
|
||||||
@@ -114,11 +114,11 @@ async function fetchSample(url) {
|
|||||||
return { channels, sampleRate: buffer.sampleRate };
|
return { channels, sampleRate: buffer.sampleRate };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function doughsamples(sampleMap, baseUrl) {
|
export async function supradoughsamples(sampleMap, baseUrl) {
|
||||||
if (typeof sampleMap === 'string') {
|
if (typeof sampleMap === 'string') {
|
||||||
const [json, base] = await fetchSampleMap(sampleMap);
|
const [json, base] = await fetchSampleMap(sampleMap);
|
||||||
// console.log('json', json, 'base', base);
|
// console.log('json', json, 'base', base);
|
||||||
return doughsamples(json, base);
|
return supradoughsamples(json, base);
|
||||||
}
|
}
|
||||||
Object.entries(sampleMap).map(async ([key, urls]) => {
|
Object.entries(sampleMap).map(async ([key, urls]) => {
|
||||||
if (key !== '_base') {
|
if (key !== '_base') {
|
||||||
|
|||||||
+110
-29
@@ -9,7 +9,7 @@ import {
|
|||||||
superdough,
|
superdough,
|
||||||
getAudioContext,
|
getAudioContext,
|
||||||
setLogger,
|
setLogger,
|
||||||
doughTrigger,
|
rawdspScheduler,
|
||||||
registerWorklet,
|
registerWorklet,
|
||||||
setAudioContext,
|
setAudioContext,
|
||||||
initAudio,
|
initAudio,
|
||||||
@@ -33,7 +33,8 @@ const hap2value = (hap) => {
|
|||||||
|
|
||||||
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
|
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
|
||||||
// TODO: refactor output callbacks to eliminate deadline
|
// TODO: refactor output callbacks to eliminate deadline
|
||||||
export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => {
|
export const webaudioOutput = (hap, t, cps) => {
|
||||||
|
const hapDuration = hap.duration / cps;
|
||||||
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
|
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,32 +57,14 @@ export async function renderPatternAudio(
|
|||||||
maxPolyphony,
|
maxPolyphony,
|
||||||
multiChannelOrbits,
|
multiChannelOrbits,
|
||||||
});
|
});
|
||||||
logger('[webaudio] preloading');
|
|
||||||
|
|
||||||
// Calling superdough(...) in ascending onset time order is important
|
// Firefox currently doesn't support suspending an OfflineAudioContext,
|
||||||
// for controls that depend on the audio graph state like `cut`
|
// so no chunked rendering. Bad performance, but at least it works.
|
||||||
let haps = pattern
|
return (
|
||||||
.queryArc(begin, end, { _cps: cps })
|
audioContext.suspend === undefined
|
||||||
.sort((a, b) => a.whole.begin.valueOf() - b.whole.begin.valueOf());
|
? renderPatternAudioWhole(audioContext, pattern, cps, begin, end)
|
||||||
for (const hap of haps) {
|
: renderPatternAudioInChunks(audioContext, pattern, cps, begin, end, 1)
|
||||||
if (hap.hasOnset()) {
|
)
|
||||||
try {
|
|
||||||
await superdough(
|
|
||||||
hap2value(hap),
|
|
||||||
(hap.whole.begin.valueOf() - begin) / cps,
|
|
||||||
hap.duration / cps,
|
|
||||||
cps,
|
|
||||||
(hap.whole?.begin.valueOf() - begin) / cps,
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
errorLogger(err, 'webaudio');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger('[webaudio] start rendering');
|
|
||||||
|
|
||||||
return audioContext
|
|
||||||
.startRendering()
|
|
||||||
.then((renderedBuffer) => {
|
.then((renderedBuffer) => {
|
||||||
const wavBuffer = audioBufferToWav(renderedBuffer);
|
const wavBuffer = audioBufferToWav(renderedBuffer);
|
||||||
const blob = new Blob([wavBuffer], { type: 'audio/wav' });
|
const blob = new Blob([wavBuffer], { type: 'audio/wav' });
|
||||||
@@ -102,6 +85,84 @@ export async function renderPatternAudio(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function renderPatternAudioWhole(audioContext, pattern, cps, begin, end) {
|
||||||
|
logger(`[webaudio] preloading`);
|
||||||
|
|
||||||
|
await scheduleHapsChunk(pattern, cps, begin, begin, end);
|
||||||
|
|
||||||
|
logger('[webaudio] start rendering');
|
||||||
|
|
||||||
|
return audioContext.startRendering();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderPatternAudioInChunks(audioContext, pattern, cps, begin, end, chunkSizeInCycles) {
|
||||||
|
let currentCycle = begin;
|
||||||
|
let renderPromise = null;
|
||||||
|
|
||||||
|
logger('[webaudio] start rendering');
|
||||||
|
|
||||||
|
while (currentCycle <= end) {
|
||||||
|
const chunkStart = currentCycle;
|
||||||
|
const chunkEnd = Math.min(currentCycle + chunkSizeInCycles, end);
|
||||||
|
|
||||||
|
logger(`[webaudio] preloading cycles ${chunkStart} - ${chunkEnd}`);
|
||||||
|
|
||||||
|
await scheduleHapsChunk(pattern, cps, begin, chunkStart, chunkEnd);
|
||||||
|
|
||||||
|
logger(`[webaudio] rendering cycles ${chunkStart} - ${chunkEnd}`);
|
||||||
|
|
||||||
|
currentCycle += chunkSizeInCycles;
|
||||||
|
|
||||||
|
// According to the MDN docs, suspends should be scheduled while
|
||||||
|
// the audioContext is not currently running for better precision.
|
||||||
|
// So we schedule the suspend first, and await after resuming.
|
||||||
|
var suspendPromise;
|
||||||
|
if (currentCycle < end) {
|
||||||
|
// Make sure to suspend one cycle before the next currentCycle
|
||||||
|
// so the next haps can be scheduled on time.
|
||||||
|
suspendPromise = audioContext.suspend((currentCycle - begin - 1) / cps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderPromise === null) {
|
||||||
|
renderPromise = audioContext.startRendering();
|
||||||
|
} else {
|
||||||
|
await audioContext.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentCycle < end) {
|
||||||
|
await suspendPromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger('[webaudio] finish rendering');
|
||||||
|
|
||||||
|
return renderPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scheduleHapsChunk(pattern, cps, begin, chunkStart, chunkEnd) {
|
||||||
|
// Calling superdough(...) in ascending onset time order is important
|
||||||
|
// for controls that depend on the audio graph state like `cut`
|
||||||
|
let haps = pattern
|
||||||
|
.queryArc(chunkStart, chunkEnd, { _cps: cps })
|
||||||
|
.sort((a, b) => a.whole.begin.valueOf() - b.whole.begin.valueOf());
|
||||||
|
|
||||||
|
for (const hap of haps) {
|
||||||
|
if (hap.hasOnset()) {
|
||||||
|
try {
|
||||||
|
await superdough(
|
||||||
|
hap2value(hap),
|
||||||
|
(hap.whole.begin.valueOf() - begin) / cps,
|
||||||
|
hap.duration / cps,
|
||||||
|
cps,
|
||||||
|
(hap.whole?.begin.valueOf() - begin) / cps,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
errorLogger(err, 'webaudio');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function webaudioRepl(options = {}) {
|
export function webaudioRepl(options = {}) {
|
||||||
const audioContext = options.audioContext ?? getAudioContext();
|
const audioContext = options.audioContext ?? getAudioContext();
|
||||||
setAudioContext(audioContext);
|
setAudioContext(audioContext);
|
||||||
@@ -113,8 +174,8 @@ export function webaudioRepl(options = {}) {
|
|||||||
return repl(options);
|
return repl(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype.dough = function () {
|
Pattern.prototype.rawdsp = function () {
|
||||||
return this.onTrigger(doughTrigger, 1);
|
return this.onSchedule(rawdspScheduler, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
function audioBufferToWav(buffer, opt) {
|
function audioBufferToWav(buffer, opt) {
|
||||||
@@ -211,3 +272,23 @@ function writeString(view, offset, string) {
|
|||||||
view.setUint8(offset + i, string.charCodeAt(i));
|
view.setUint8(offset + i, string.charCodeAt(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* make something happen on event time
|
||||||
|
* uses browser timeout which is innacurate for audio tasks
|
||||||
|
* @name onTriggerTime
|
||||||
|
* @tags external_io
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("bd!8").onTriggerTime((hap) => {console.log(hap)})
|
||||||
|
*/
|
||||||
|
Pattern.prototype.onTriggerTime = function (func) {
|
||||||
|
return this.onSchedule((hap, targetTime) => {
|
||||||
|
const diff = targetTime - getAudioContext().currentTime;
|
||||||
|
window.setTimeout(() => {
|
||||||
|
func(hap);
|
||||||
|
}, diff * 1000);
|
||||||
|
}, false);
|
||||||
|
};
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ export const withBase = register('withBase', (b, pat) => {
|
|||||||
let hVal = hap.value;
|
let hVal = hap.value;
|
||||||
const isObject = typeof hVal === 'object';
|
const isObject = typeof hVal === 'object';
|
||||||
let freq = isObject ? hVal.freq : hVal;
|
let freq = isObject ? hVal.freq : hVal;
|
||||||
|
if (!freq) return hap;
|
||||||
freq = (freq * base) / originalBase;
|
freq = (freq * base) / originalBase;
|
||||||
hap.value = isObject ? { ...hap.value, freq } : { freq };
|
hap.value = isObject ? { ...hap.value, freq } : { freq };
|
||||||
return hap;
|
return hap;
|
||||||
|
|||||||
Generated
+123
-8
@@ -4,6 +4,30 @@ settings:
|
|||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
catalogs:
|
||||||
|
default:
|
||||||
|
'@codemirror/autocomplete':
|
||||||
|
specifier: ^6.18.4
|
||||||
|
version: 6.18.4
|
||||||
|
'@codemirror/commands':
|
||||||
|
specifier: ^6.8.0
|
||||||
|
version: 6.8.0
|
||||||
|
'@codemirror/lang-javascript':
|
||||||
|
specifier: ^6.2.2
|
||||||
|
version: 6.2.2
|
||||||
|
'@codemirror/language':
|
||||||
|
specifier: ^6.10.8
|
||||||
|
version: 6.10.8
|
||||||
|
'@codemirror/search':
|
||||||
|
specifier: ^6.5.8
|
||||||
|
version: 6.5.8
|
||||||
|
'@codemirror/state':
|
||||||
|
specifier: ^6.5.1
|
||||||
|
version: 6.5.1
|
||||||
|
'@codemirror/view':
|
||||||
|
specifier: ^6.36.2
|
||||||
|
version: 6.36.2
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
@@ -11,6 +35,9 @@ importers:
|
|||||||
'@strudel/core':
|
'@strudel/core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:packages/core
|
version: link:packages/core
|
||||||
|
'@strudel/edo':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:packages/edo
|
||||||
'@strudel/mini':
|
'@strudel/mini':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:packages/mini
|
version: link:packages/mini
|
||||||
@@ -93,6 +120,9 @@ importers:
|
|||||||
'@strudel/draw':
|
'@strudel/draw':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/draw
|
version: link:../../packages/draw
|
||||||
|
'@strudel/edo':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/edo
|
||||||
'@strudel/mini':
|
'@strudel/mini':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/mini
|
version: link:../../packages/mini
|
||||||
@@ -171,25 +201,25 @@ importers:
|
|||||||
packages/codemirror:
|
packages/codemirror:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/autocomplete':
|
'@codemirror/autocomplete':
|
||||||
specifier: ^6.18.4
|
specifier: 'catalog:'
|
||||||
version: 6.18.4
|
version: 6.18.4
|
||||||
'@codemirror/commands':
|
'@codemirror/commands':
|
||||||
specifier: ^6.8.0
|
specifier: 'catalog:'
|
||||||
version: 6.8.0
|
version: 6.8.0
|
||||||
'@codemirror/lang-javascript':
|
'@codemirror/lang-javascript':
|
||||||
specifier: ^6.2.2
|
specifier: 'catalog:'
|
||||||
version: 6.2.2
|
version: 6.2.2
|
||||||
'@codemirror/language':
|
'@codemirror/language':
|
||||||
specifier: ^6.10.8
|
specifier: 'catalog:'
|
||||||
version: 6.10.8
|
version: 6.10.8
|
||||||
'@codemirror/search':
|
'@codemirror/search':
|
||||||
specifier: ^6.5.8
|
specifier: 'catalog:'
|
||||||
version: 6.5.8
|
version: 6.5.8
|
||||||
'@codemirror/state':
|
'@codemirror/state':
|
||||||
specifier: ^6.5.1
|
specifier: 'catalog:'
|
||||||
version: 6.5.1
|
version: 6.5.1
|
||||||
'@codemirror/view':
|
'@codemirror/view':
|
||||||
specifier: ^6.36.2
|
specifier: 'catalog:'
|
||||||
version: 6.36.2
|
version: 6.36.2
|
||||||
'@lezer/highlight':
|
'@lezer/highlight':
|
||||||
specifier: ^1.2.1
|
specifier: ^1.2.1
|
||||||
@@ -273,6 +303,22 @@ importers:
|
|||||||
specifier: ^2.2.0
|
specifier: ^2.2.0
|
||||||
version: 2.2.0
|
version: 2.2.0
|
||||||
|
|
||||||
|
packages/dough:
|
||||||
|
dependencies:
|
||||||
|
'@strudel/core':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../core
|
||||||
|
'@strudel/webaudio':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../webaudio
|
||||||
|
dough-synth:
|
||||||
|
specifier: 0.2.4
|
||||||
|
version: 0.2.4
|
||||||
|
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)
|
||||||
|
|
||||||
packages/draw:
|
packages/draw:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@strudel/core':
|
'@strudel/core':
|
||||||
@@ -283,6 +329,28 @@ importers:
|
|||||||
specifier: ^6.0.11
|
specifier: ^6.0.11
|
||||||
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||||
|
|
||||||
|
packages/edo:
|
||||||
|
dependencies:
|
||||||
|
'@strudel/core':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../core
|
||||||
|
'@tonaljs/tonal':
|
||||||
|
specifier: ^4.10.0
|
||||||
|
version: 4.10.0
|
||||||
|
chord-voicings:
|
||||||
|
specifier: ^0.0.1
|
||||||
|
version: 0.0.1
|
||||||
|
webmidi:
|
||||||
|
specifier: ^3.1.12
|
||||||
|
version: 3.1.12
|
||||||
|
devDependencies:
|
||||||
|
vite:
|
||||||
|
specifier: ^6.0.11
|
||||||
|
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||||
|
vitest:
|
||||||
|
specifier: ^3.0.4
|
||||||
|
version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||||
|
|
||||||
packages/embed: {}
|
packages/embed: {}
|
||||||
|
|
||||||
packages/gamepad:
|
packages/gamepad:
|
||||||
@@ -418,6 +486,9 @@ importers:
|
|||||||
'@strudel/core':
|
'@strudel/core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core
|
||||||
|
'@strudel/webaudio':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../webaudio
|
||||||
osc:
|
osc:
|
||||||
specifier: ^2.4.5
|
specifier: ^2.4.5
|
||||||
version: 2.4.5
|
version: 2.4.5
|
||||||
@@ -449,6 +520,9 @@ importers:
|
|||||||
'@strudel/draw':
|
'@strudel/draw':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../draw
|
version: link:../draw
|
||||||
|
'@strudel/edo':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../edo
|
||||||
'@strudel/hydra':
|
'@strudel/hydra':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../hydra
|
version: link:../hydra
|
||||||
@@ -624,6 +698,9 @@ importers:
|
|||||||
'@strudel/core':
|
'@strudel/core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core
|
||||||
|
'@strudel/edo':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../edo
|
||||||
'@strudel/mini':
|
'@strudel/mini':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../mini
|
version: link:../mini
|
||||||
@@ -705,6 +782,21 @@ importers:
|
|||||||
'@astrojs/tailwind':
|
'@astrojs/tailwind':
|
||||||
specifier: ^5.1.5
|
specifier: ^5.1.5
|
||||||
version: 5.1.5(astro@5.1.9(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@2.79.2)(terser@5.37.0)(typescript@5.7.3)(yaml@2.7.0))(tailwindcss@3.4.17)
|
version: 5.1.5(astro@5.1.9(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@2.79.2)(terser@5.37.0)(typescript@5.7.3)(yaml@2.7.0))(tailwindcss@3.4.17)
|
||||||
|
'@codemirror/commands':
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 6.8.0
|
||||||
|
'@codemirror/lang-javascript':
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 6.2.2
|
||||||
|
'@codemirror/language':
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 6.10.8
|
||||||
|
'@codemirror/state':
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 6.5.1
|
||||||
|
'@codemirror/view':
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 6.36.2
|
||||||
'@docsearch/css':
|
'@docsearch/css':
|
||||||
specifier: ^3.8.3
|
specifier: ^3.8.3
|
||||||
version: 3.8.3
|
version: 3.8.3
|
||||||
@@ -735,9 +827,15 @@ importers:
|
|||||||
'@strudel/desktopbridge':
|
'@strudel/desktopbridge':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/desktopbridge
|
version: link:../packages/desktopbridge
|
||||||
|
'@strudel/dough':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../packages/dough
|
||||||
'@strudel/draw':
|
'@strudel/draw':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/draw
|
version: link:../packages/draw
|
||||||
|
'@strudel/edo':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../packages/edo
|
||||||
'@strudel/gamepad':
|
'@strudel/gamepad':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/gamepad
|
version: link:../packages/gamepad
|
||||||
@@ -1990,6 +2088,7 @@ packages:
|
|||||||
'@lerna/create@8.1.9':
|
'@lerna/create@8.1.9':
|
||||||
resolution: {integrity: sha512-DPnl5lPX4v49eVxEbJnAizrpMdMTBz1qykZrAbBul9rfgk531v8oAt+Pm6O/rpAleRombNM7FJb5rYGzBJatOQ==}
|
resolution: {integrity: sha512-DPnl5lPX4v49eVxEbJnAizrpMdMTBz1qykZrAbBul9rfgk531v8oAt+Pm6O/rpAleRombNM7FJb5rYGzBJatOQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
deprecated: This package is an implementation detail of Lerna and is no longer published separately.
|
||||||
|
|
||||||
'@lezer/common@1.2.3':
|
'@lezer/common@1.2.3':
|
||||||
resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
|
resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
|
||||||
@@ -2989,6 +3088,7 @@ packages:
|
|||||||
|
|
||||||
'@ungap/structured-clone@1.3.0':
|
'@ungap/structured-clone@1.3.0':
|
||||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||||
|
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
|
||||||
|
|
||||||
'@vite-pwa/astro@0.5.0':
|
'@vite-pwa/astro@0.5.0':
|
||||||
resolution: {integrity: sha512-Yd3Pug/c1EUQJXWvzYh6eTtoqzmSKcdCqWCcNquZeaD13tLWpBb2FIPJ4HMULVY6+GfxMvrT+OBuMrbHQCvftw==}
|
resolution: {integrity: sha512-Yd3Pug/c1EUQJXWvzYh6eTtoqzmSKcdCqWCcNquZeaD13tLWpBb2FIPJ4HMULVY6+GfxMvrT+OBuMrbHQCvftw==}
|
||||||
@@ -3640,6 +3740,7 @@ packages:
|
|||||||
conventional-changelog-core@5.0.1:
|
conventional-changelog-core@5.0.1:
|
||||||
resolution: {integrity: sha512-Rvi5pH+LvgsqGwZPZ3Cq/tz4ty7mjijhr3qR4m9IBXNbxGGYgTVVO+duXzz9aArmHxFtwZ+LRkrNIMDQzgoY4A==}
|
resolution: {integrity: sha512-Rvi5pH+LvgsqGwZPZ3Cq/tz4ty7mjijhr3qR4m9IBXNbxGGYgTVVO+duXzz9aArmHxFtwZ+LRkrNIMDQzgoY4A==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
deprecated: Deprecated and no longer maintained. Please use conventional-changelog instead.
|
||||||
|
|
||||||
conventional-changelog-preset-loader@3.0.0:
|
conventional-changelog-preset-loader@3.0.0:
|
||||||
resolution: {integrity: sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA==}
|
resolution: {integrity: sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA==}
|
||||||
@@ -3971,6 +4072,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
|
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
dough-synth@0.2.4:
|
||||||
|
resolution: {integrity: sha512-qMnMKVj1B2ivUgobNCwqJqNzZHOMN+5339OmCOk07zIDEK7iCYdufa+ZDE2BEgmKrWDyhGS7oTNFjHtTwzEv9w==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
dset@3.1.4:
|
dset@3.1.4:
|
||||||
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
|
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -4503,6 +4608,7 @@ packages:
|
|||||||
git-raw-commits@3.0.0:
|
git-raw-commits@3.0.0:
|
||||||
resolution: {integrity: sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw==}
|
resolution: {integrity: sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
git-remote-origin-url@2.0.0:
|
git-remote-origin-url@2.0.0:
|
||||||
@@ -4512,6 +4618,7 @@ packages:
|
|||||||
git-semver-tags@5.0.1:
|
git-semver-tags@5.0.1:
|
||||||
resolution: {integrity: sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA==}
|
resolution: {integrity: sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
git-up@7.0.0:
|
git-up@7.0.0:
|
||||||
@@ -4539,15 +4646,17 @@ packages:
|
|||||||
|
|
||||||
glob@10.4.5:
|
glob@10.4.5:
|
||||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||||
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
glob@7.2.3:
|
glob@7.2.3:
|
||||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||||
deprecated: Glob versions prior to v9 are no longer supported
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
|
|
||||||
glob@9.3.5:
|
glob@9.3.5:
|
||||||
resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}
|
resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
|
|
||||||
globalize@0.1.1:
|
globalize@0.1.1:
|
||||||
resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==}
|
resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==}
|
||||||
@@ -6330,6 +6439,7 @@ packages:
|
|||||||
prebuild-install@7.1.1:
|
prebuild-install@7.1.1:
|
||||||
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
|
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
precinct@12.1.2:
|
precinct@12.1.2:
|
||||||
@@ -7154,6 +7264,7 @@ packages:
|
|||||||
tar@6.2.1:
|
tar@6.2.1:
|
||||||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
|
|
||||||
temp-dir@1.0.0:
|
temp-dir@1.0.0:
|
||||||
resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==}
|
resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==}
|
||||||
@@ -7281,6 +7392,7 @@ packages:
|
|||||||
tsconfck@3.1.4:
|
tsconfck@3.1.4:
|
||||||
resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==}
|
resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==}
|
||||||
engines: {node: ^18 || >=20}
|
engines: {node: ^18 || >=20}
|
||||||
|
deprecated: unmaintained
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: ^5.0.0
|
typescript: ^5.0.0
|
||||||
@@ -7560,6 +7672,7 @@ packages:
|
|||||||
|
|
||||||
uuid@10.0.0:
|
uuid@10.0.0:
|
||||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||||
|
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
validate-npm-package-license@3.0.4:
|
validate-npm-package-license@3.0.4:
|
||||||
@@ -11681,6 +11794,8 @@ snapshots:
|
|||||||
|
|
||||||
dotenv@16.4.7: {}
|
dotenv@16.4.7: {}
|
||||||
|
|
||||||
|
dough-synth@0.2.4: {}
|
||||||
|
|
||||||
dset@3.1.4: {}
|
dset@3.1.4: {}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
|
|||||||
@@ -10,3 +10,12 @@ onlyBuiltDependencies:
|
|||||||
- sharp
|
- sharp
|
||||||
- tree-sitter
|
- tree-sitter
|
||||||
- tree-sitter-haskell
|
- tree-sitter-haskell
|
||||||
|
|
||||||
|
catalog:
|
||||||
|
"@codemirror/commands": "^6.8.0"
|
||||||
|
"@codemirror/language": "^6.10.8"
|
||||||
|
"@codemirror/lang-javascript": "^6.2.2"
|
||||||
|
"@codemirror/state": "^6.5.1"
|
||||||
|
"@codemirror/view": "^6.36.2"
|
||||||
|
"@codemirror/autocomplete": "^6.18.4"
|
||||||
|
"@codemirror/search": "^6.5.8"
|
||||||
|
|||||||
@@ -639,30 +639,30 @@ exports[`runs examples > example "K" example index 0 1`] = `
|
|||||||
"[ 7/4 → 11/6 | note:C5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
"[ 7/4 → 11/6 | note:C5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||||
"[ 11/6 → 23/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
"[ 11/6 → 23/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||||
"[ 23/12 → 2/1 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
"[ 23/12 → 2/1 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||||
"[ 2/1 → 25/12 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
"[ 2/1 → 25/12 | note:Db5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||||
"[ 25/12 → 13/6 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
"[ 25/12 → 13/6 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||||
"[ 13/6 → 9/4 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
"[ 13/6 → 9/4 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||||
"[ 9/4 → 7/3 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
"[ 9/4 → 7/3 | note:Db5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||||
"[ (7/3 → 19/8) ⇝ 29/12 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
"[ (7/3 → 19/8) ⇝ 29/12 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||||
"[ 7/3 ⇜ (19/8 → 29/12) | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
"[ 7/3 ⇜ (19/8 → 29/12) | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||||
"[ 29/12 → 5/2 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
"[ 29/12 → 5/2 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||||
"[ 5/2 → 31/12 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
"[ 5/2 → 31/12 | note:Db5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||||
"[ 31/12 → 8/3 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
"[ 31/12 → 8/3 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||||
"[ 8/3 → 11/4 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
"[ 8/3 → 11/4 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||||
"[ 11/4 → 17/6 | note:Db4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
"[ 11/4 → 17/6 | note:Db5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||||
"[ 17/6 → 35/12 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
"[ 17/6 → 35/12 | note:F4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||||
"[ 35/12 → 3/1 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
"[ 35/12 → 3/1 | note:Ab4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||||
"[ 3/1 → 37/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
"[ 3/1 → 37/12 | note:Eb5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||||
"[ 37/12 → 19/6 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
"[ 37/12 → 19/6 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||||
"[ 19/6 → 13/4 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
"[ 19/6 → 13/4 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.03] ]",
|
||||||
"[ 13/4 → 10/3 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
"[ 13/4 → 10/3 | note:Eb5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||||
"[ (10/3 → 27/8) ⇝ 41/12 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
"[ (10/3 → 27/8) ⇝ 41/12 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.08] ]",
|
||||||
"[ 10/3 ⇜ (27/8 → 41/12) | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
"[ 10/3 ⇜ (27/8 → 41/12) | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||||
"[ 41/12 → 7/2 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
"[ 41/12 → 7/2 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.2 0.01] ]",
|
||||||
"[ 7/2 → 43/12 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
"[ 7/2 → 43/12 | note:Eb5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||||
"[ 43/12 → 11/3 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
"[ 43/12 → 11/3 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||||
"[ 11/3 → 15/4 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
"[ 11/3 → 15/4 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[2 0 0.1 0.3 0.01] ]",
|
||||||
"[ 15/4 → 23/6 | note:Eb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
"[ 15/4 → 23/6 | note:Eb5 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||||
"[ 23/6 → 47/12 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
"[ 23/6 → 47/12 | note:G4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||||
"[ 47/12 → 4/1 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
"[ 47/12 → 4/1 | note:Bb4 s:saw workletSrc:saw(saw(sFreq / pat[0]).mul(8).add(sFreq).lag(pat[1])).mul(0.3).mul(sGate.adsr(0 0.15 0.5 pat[2])).lpf(sGate.adsr(0 0.2 0.3 0.2).mul(1).add(0)).add(x => x.delay(pat[3]).mul(0.7)).add(x => x.delay(pat[4]).mul(0.77)).mul(0.7).add(x => x.delay(0.13).mul(0.7)).out() workletInputs:[16 0.1 1 0.2 0.013] ]",
|
||||||
]
|
]
|
||||||
@@ -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`] = `
|
exports[`runs examples > example "beat" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/16 | s:bd ]",
|
"[ 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`] = `
|
exports[`runs examples > example "delayspeed" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
"[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||||
@@ -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`] = `
|
exports[`runs examples > example "density" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | s:crackle density:0.01 ]",
|
"[ 0/1 → 1/4 | s:crackle density:0.01 ]",
|
||||||
@@ -3365,6 +3422,30 @@ exports[`runs examples > example "djf" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "dough" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | note:F3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 0/1 → 1/1 | note:C4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 0/1 → 1/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 0/1 → 1/1 | note:E4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 0/1 → 1/1 | note:A4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:A3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:G4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:A4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:D5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:F5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:C6 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:A3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:E4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:F4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "drawLine" example index 0 1`] = `[]`;
|
exports[`runs examples > example "drawLine" example index 0 1`] = `[]`;
|
||||||
|
|
||||||
exports[`runs examples > example "drive" example index 0 1`] = `
|
exports[`runs examples > example "drive" example index 0 1`] = `
|
||||||
@@ -3959,6 +4040,93 @@ exports[`runs examples > example "echoWith" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "edoScale" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/6 | degree:1 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:130.813 edo:12 ]",
|
||||||
|
"[ 1/6 → 1/3 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
||||||
|
"[ 1/3 → 1/2 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
||||||
|
"[ 1/2 → 2/3 | degree:7 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:246.942 edo:12 ]",
|
||||||
|
"[ 2/3 → 5/6 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
||||||
|
"[ 5/6 → 1/1 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
||||||
|
"[ 1/1 → 7/6 | degree:1 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:130.813 edo:12 ]",
|
||||||
|
"[ 7/6 → 4/3 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
||||||
|
"[ 4/3 → 3/2 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
||||||
|
"[ 3/2 → 5/3 | degree:7 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:246.942 edo:12 ]",
|
||||||
|
"[ 5/3 → 11/6 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
||||||
|
"[ 11/6 → 2/1 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
||||||
|
"[ 2/1 → 13/6 | degree:1 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:130.813 edo:12 ]",
|
||||||
|
"[ 13/6 → 7/3 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
||||||
|
"[ 7/3 → 5/2 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
||||||
|
"[ 5/2 → 8/3 | degree:7 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:246.942 edo:12 ]",
|
||||||
|
"[ 8/3 → 17/6 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
||||||
|
"[ 17/6 → 3/1 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
||||||
|
"[ 3/1 → 19/6 | degree:1 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:130.813 edo:12 ]",
|
||||||
|
"[ 19/6 → 10/3 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
||||||
|
"[ 10/3 → 7/2 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
||||||
|
"[ 7/2 → 11/3 | degree:7 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:246.942 edo:12 ]",
|
||||||
|
"[ 11/3 → 23/6 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
||||||
|
"[ 23/6 → 4/1 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "edoScale" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | degree:1 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:97.999 edo:16 s:piano ]",
|
||||||
|
"[ 0/1 → 1/4 | degree:2 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
||||||
|
"[ 1/4 → 1/2 | degree:5 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:151.135 edo:16 s:piano ]",
|
||||||
|
"[ 1/2 → 3/4 | degree:3 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:127.089 edo:16 s:piano ]",
|
||||||
|
"[ 1/2 → 3/4 | degree:2 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
||||||
|
"[ 3/4 → 1/1 | degree:5 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:151.135 edo:16 s:piano ]",
|
||||||
|
"[ 1/1 → 5/4 | degree:1 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:97.999 edo:16 s:piano ]",
|
||||||
|
"[ 1/1 → 5/4 | degree:2 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
||||||
|
"[ 5/4 → 3/2 | degree:5 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:164.814 edo:16 s:piano ]",
|
||||||
|
"[ 3/2 → 7/4 | degree:3 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:127.089 edo:16 s:piano ]",
|
||||||
|
"[ 3/2 → 7/4 | degree:2 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
||||||
|
"[ 7/4 → 2/1 | degree:5 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:164.814 edo:16 s:piano ]",
|
||||||
|
"[ 2/1 → 9/4 | degree:1 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:97.999 edo:16 s:piano ]",
|
||||||
|
"[ 2/1 → 9/4 | degree:2 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
||||||
|
"[ 9/4 → 5/2 | degree:5 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:151.135 edo:16 s:piano ]",
|
||||||
|
"[ 5/2 → 11/4 | degree:3 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:127.089 edo:16 s:piano ]",
|
||||||
|
"[ 5/2 → 11/4 | degree:2 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
||||||
|
"[ 11/4 → 3/1 | degree:5 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:151.135 edo:16 s:piano ]",
|
||||||
|
"[ 3/1 → 13/4 | degree:1 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:97.999 edo:16 s:piano ]",
|
||||||
|
"[ 3/1 → 13/4 | degree:2 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
||||||
|
"[ 13/4 → 7/2 | degree:5 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:164.814 edo:16 s:piano ]",
|
||||||
|
"[ 7/2 → 15/4 | degree:3 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:127.089 edo:16 s:piano ]",
|
||||||
|
"[ 7/2 → 15/4 | degree:2 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
||||||
|
"[ 15/4 → 4/1 | degree:5 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:164.814 edo:16 s:piano ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "edoScale" example index 2 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/6 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:97.999 edo:13 s:piano ]",
|
||||||
|
"[ 1/6 → 1/3 | degree:5 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:167.025 edo:13 s:piano ]",
|
||||||
|
"[ 1/3 → 1/2 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:134.945 edo:13 s:piano ]",
|
||||||
|
"[ 1/2 → 2/3 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:114.998 edo:13 s:piano ]",
|
||||||
|
"[ 2/3 → 5/6 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:134.945 edo:13 s:piano ]",
|
||||||
|
"[ 5/6 → 1/1 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:97.999 edo:13 s:piano ]",
|
||||||
|
"[ 1/1 → 7/6 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:189.995 edo:13 s:piano ]",
|
||||||
|
"[ 7/6 → 4/3 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:153.504 edo:13 s:piano ]",
|
||||||
|
"[ 4/3 → 3/2 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:180.13 edo:13 s:piano ]",
|
||||||
|
"[ 3/2 → 5/3 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:189.995 edo:13 s:piano ]",
|
||||||
|
"[ 5/3 → 11/6 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:130.813 edo:13 s:piano ]",
|
||||||
|
"[ 11/6 → 2/1 | degree:5 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:222.952 edo:13 s:piano ]",
|
||||||
|
"[ 2/1 → 13/6 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:195.998 edo:13 s:piano ]",
|
||||||
|
"[ 13/6 → 7/3 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:142.336 edo:13 s:piano ]",
|
||||||
|
"[ 7/3 → 5/2 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:134.945 edo:13 s:piano ]",
|
||||||
|
"[ 5/2 → 8/3 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:134.945 edo:13 s:piano ]",
|
||||||
|
"[ 8/3 → 17/6 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:142.336 edo:13 s:piano ]",
|
||||||
|
"[ 17/6 → 3/1 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:114.998 edo:13 s:piano ]",
|
||||||
|
"[ 3/1 → 19/6 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:153.504 edo:13 s:piano ]",
|
||||||
|
"[ 19/6 → 10/3 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:153.504 edo:13 s:piano ]",
|
||||||
|
"[ 10/3 → 7/2 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:261.626 edo:13 s:piano ]",
|
||||||
|
"[ 7/2 → 11/3 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:180.13 edo:13 s:piano ]",
|
||||||
|
"[ 11/3 → 23/6 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:180.13 edo:13 s:piano ]",
|
||||||
|
"[ 23/6 → 4/1 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:189.995 edo:13 s:piano ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "end" example index 0 1`] = `
|
exports[`runs examples > example "end" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | s:oh end:0.1 ]",
|
"[ 0/1 → 1/8 | s:oh end:0.1 ]",
|
||||||
@@ -5191,6 +5359,15 @@ exports[`runs examples > example "gain" example index 0 1`] = `
|
|||||||
|
|
||||||
exports[`runs examples > example "gap" example index 0 1`] = `[]`;
|
exports[`runs examples > example "gap" example index 0 1`] = `[]`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "getDur" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | s:sax cps:null ]",
|
||||||
|
"[ 1/1 → 2/1 | s:sax cps:null ]",
|
||||||
|
"[ 2/1 → 3/1 | s:sax cps:null ]",
|
||||||
|
"[ 3/1 → 4/1 | s:sax cps:null ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "grow" example index 0 1`] = `
|
exports[`runs examples > example "grow" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/10 | s:tha bank:mridangam ]",
|
"[ 0/1 → 1/10 | s:tha bank:mridangam ]",
|
||||||
@@ -5777,6 +5954,30 @@ exports[`runs examples > example "inhabit" example index 1 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "initDough" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | note:F3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 0/1 → 1/1 | note:C4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 0/1 → 1/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 0/1 → 1/1 | note:E4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 0/1 → 1/1 | note:A4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:A3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:G4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 1/1 → 2/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:A4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:D5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:F5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 2/1 → 3/1 | note:C6 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:A3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:E4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:F4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
"[ 3/1 → 4/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "inside" example index 0 1`] = `
|
exports[`runs examples > example "inside" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | note:D3 ]",
|
"[ 0/1 → 1/8 | note:D3 ]",
|
||||||
@@ -6403,6 +6604,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 "keyDown" example index 0 1`] = `[]`;
|
||||||
|
|
||||||
exports[`runs examples > example "lastOf" example index 0 1`] = `
|
exports[`runs examples > example "lastOf" example index 0 1`] = `
|
||||||
@@ -7538,6 +8004,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 1 1`] = `[]`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "midikeys" example index 2 1`] = `[]`;
|
||||||
|
|
||||||
exports[`runs examples > example "midin" example index 0 1`] = `
|
exports[`runs examples > example "midin" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
|
"[ 0/1 → 1/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
|
||||||
@@ -8054,6 +8522,19 @@ exports[`runs examples > example "often" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "onSchedule" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/2 | s:bd ]",
|
||||||
|
"[ 1/2 → 1/1 | s:sd ]",
|
||||||
|
"[ 1/1 → 3/2 | s:bd ]",
|
||||||
|
"[ 3/2 → 2/1 | s:sd ]",
|
||||||
|
"[ 2/1 → 5/2 | s:bd ]",
|
||||||
|
"[ 5/2 → 3/1 | s:sd ]",
|
||||||
|
"[ 3/1 → 7/2 | s:bd ]",
|
||||||
|
"[ 7/2 → 4/1 | s:sd ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "onTriggerTime" example index 0 1`] = `
|
exports[`runs examples > example "onTriggerTime" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | s:bd ]",
|
"[ 0/1 → 1/8 | s:bd ]",
|
||||||
@@ -11110,6 +11591,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`] = `
|
exports[`runs examples > example "setDefaultJoin" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | s:saw velocity:1 note:F delay:0 ]",
|
"[ 0/1 → 1/4 | s:saw velocity:1 note:F delay:0 ]",
|
||||||
@@ -12238,18 +12740,10 @@ exports[`runs examples > example "stepcat" example index 1 1`] = `
|
|||||||
|
|
||||||
exports[`runs examples > example "stretch" example index 0 1`] = `
|
exports[`runs examples > example "stretch" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ (0/1 → 1/3) ⇝ 1/1 | s:gm_flute stretch:1 ]",
|
"[ 0/1 → 1/1 | s:gm_flute stretch:2 ]",
|
||||||
"[ 0/1 ⇜ (1/3 → 2/3) ⇝ 1/1 | s:gm_flute stretch:2 ]",
|
"[ 1/1 → 2/1 | s:gm_flute stretch:1 ]",
|
||||||
"[ 0/1 ⇜ (2/3 → 1/1) | s:gm_flute stretch:0.5 ]",
|
"[ 2/1 → 3/1 | s:gm_flute stretch:0 ]",
|
||||||
"[ (1/1 → 4/3) ⇝ 2/1 | s:gm_flute stretch:1 ]",
|
"[ 3/1 → 4/1 | s:gm_flute stretch:-2 ]",
|
||||||
"[ 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 ]",
|
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -7203,8 +7203,12 @@ exports[`renders tunes > tune: holyflute 1`] = `
|
|||||||
|
|
||||||
exports[`renders tunes > tune: juxUndTollerei 1`] = `
|
exports[`renders tunes > tune: juxUndTollerei 1`] = `
|
||||||
[
|
[
|
||||||
|
"[ -99/200 ⇜ (0/1 → 1/200) | note:63 s:triangle pan:0 cutoff:758.852817928549 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
||||||
|
"[ -99/200 ⇜ (0/1 → 1/200) | note:67 s:triangle pan:1 color:green cutoff:758.852817928549 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
||||||
"[ 0/1 → 1/4 | note:c3 s:sawtooth pan:0 cutoff:1100 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
"[ 0/1 → 1/4 | note:c3 s:sawtooth pan:0 cutoff:1100 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
||||||
"[ 0/1 → 1/4 | note:bb3 s:sawtooth pan:1 color:green cutoff:1100 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
"[ 0/1 → 1/4 | note:bb3 s:sawtooth pan:1 color:green cutoff:1100 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
||||||
|
"[ 1/200 → 101/200 | note:55 s:triangle pan:0 cutoff:1103.534282651425 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
||||||
|
"[ 1/200 → 101/200 | note:65 s:triangle pan:1 color:green cutoff:1103.534282651425 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
||||||
"[ 1/4 → 1/2 | note:eb3 s:sawtooth pan:0 cutoff:1275.581289814515 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
"[ 1/4 → 1/2 | note:eb3 s:sawtooth pan:0 cutoff:1275.581289814515 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
||||||
"[ 1/4 → 1/2 | note:g3 s:sawtooth pan:1 color:green cutoff:1275.581289814515 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
"[ 1/4 → 1/2 | note:g3 s:sawtooth pan:1 color:green cutoff:1275.581289814515 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
||||||
"[ 1/2 → 3/4 | note:g3 s:sawtooth pan:0 cutoff:1444.415089128581 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
"[ 1/2 → 3/4 | note:g3 s:sawtooth pan:0 cutoff:1444.415089128581 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { mini, m } from '@strudel/mini/mini.mjs';
|
|||||||
// import euclid from '@strudel/core/euclid.mjs';
|
// import euclid from '@strudel/core/euclid.mjs';
|
||||||
//import '@strudel/midi/midi.mjs';
|
//import '@strudel/midi/midi.mjs';
|
||||||
import * as tonalHelpers from '@strudel/tonal';
|
import * as tonalHelpers from '@strudel/tonal';
|
||||||
|
import * as edoHelpers from '@strudel/edo';
|
||||||
import '@strudel/xen/xen.mjs';
|
import '@strudel/xen/xen.mjs';
|
||||||
// import '@strudel/xen/tune.mjs';
|
// import '@strudel/xen/tune.mjs';
|
||||||
// import '@strudel/core/euclid.mjs';
|
// import '@strudel/core/euclid.mjs';
|
||||||
@@ -98,6 +99,7 @@ const toneHelpersMocked = {
|
|||||||
'_spectrum',
|
'_spectrum',
|
||||||
'markcss',
|
'markcss',
|
||||||
'p',
|
'p',
|
||||||
|
'dough',
|
||||||
].forEach((mock) => {
|
].forEach((mock) => {
|
||||||
strudel.Pattern.prototype[mock] = function () {
|
strudel.Pattern.prototype[mock] = function () {
|
||||||
return this;
|
return this;
|
||||||
@@ -126,6 +128,7 @@ const loadSoundfont = () => {};
|
|||||||
const loadCsound = () => {};
|
const loadCsound = () => {};
|
||||||
const loadCSound = () => {};
|
const loadCSound = () => {};
|
||||||
const loadcsound = () => {};
|
const loadcsound = () => {};
|
||||||
|
const getDuration = () => {};
|
||||||
|
|
||||||
const midin = () => {
|
const midin = () => {
|
||||||
return (ccNum) => strudel.ref(() => 0); // returns ref with default value 0
|
return (ccNum) => strudel.ref(() => 0); // returns ref with default value 0
|
||||||
@@ -144,6 +147,7 @@ evalScope(
|
|||||||
toneHelpersMocked,
|
toneHelpersMocked,
|
||||||
uiHelpersMocked,
|
uiHelpersMocked,
|
||||||
webaudio,
|
webaudio,
|
||||||
|
edoHelpers,
|
||||||
tonalHelpers,
|
tonalHelpers,
|
||||||
gamepadHelpers,
|
gamepadHelpers,
|
||||||
/*
|
/*
|
||||||
@@ -168,8 +172,10 @@ evalScope(
|
|||||||
loadCSound,
|
loadCSound,
|
||||||
loadCsound,
|
loadCsound,
|
||||||
loadcsound,
|
loadcsound,
|
||||||
|
getDuration,
|
||||||
setcps: id,
|
setcps: id,
|
||||||
setcpm: id,
|
setcpm: id,
|
||||||
|
initDough: id,
|
||||||
Clock: {}, // whatever
|
Clock: {}, // whatever
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-2
@@ -597,8 +597,8 @@
|
|||||||
],
|
],
|
||||||
"/packages/superdough/dspworklet.mjs": [
|
"/packages/superdough/dspworklet.mjs": [
|
||||||
"dspWorklet",
|
"dspWorklet",
|
||||||
"dough",
|
"rawdsp",
|
||||||
"doughTrigger"
|
"rawdspTrigger"
|
||||||
],
|
],
|
||||||
"/packages/superdough/index.mjs": [],
|
"/packages/superdough/index.mjs": [],
|
||||||
"/packages/tonal/tonleiter.mjs": [
|
"/packages/tonal/tonleiter.mjs": [
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export default defineConfig({
|
|||||||
urlPattern: ({ url }) =>
|
urlPattern: ({ url }) =>
|
||||||
[
|
[
|
||||||
/^https:\/\/raw\.githubusercontent\.com\/.*/i,
|
/^https:\/\/raw\.githubusercontent\.com\/.*/i,
|
||||||
|
/^https:\/\/strudel\.b-cdn\.net\/.*/i,
|
||||||
/^https:\/\/freesound\.org\/.*/i,
|
/^https:\/\/freesound\.org\/.*/i,
|
||||||
/^https:\/\/cdn\.freesound\.org\/.*/i,
|
/^https:\/\/cdn\.freesound\.org\/.*/i,
|
||||||
/^https:\/\/shabda\.ndre\.gr\/.*/i,
|
/^https:\/\/shabda\.ndre\.gr\/.*/i,
|
||||||
@@ -144,4 +145,13 @@ export default defineConfig({
|
|||||||
// external: ['fraction.js'], // https://github.com/infusion/Fraction.js/issues/51
|
// external: ['fraction.js'], // https://github.com/infusion/Fraction.js/issues/51
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
// these are needed for dough, which uses shared memory
|
||||||
|
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements
|
||||||
|
// this is only for the dev server, so the server strudel runs on, also needs those
|
||||||
|
headers: {
|
||||||
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||||
|
'Cross-Origin-Embedder-Policy': 'credentialless',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,11 @@
|
|||||||
"@astrojs/react": "^4.1.6",
|
"@astrojs/react": "^4.1.6",
|
||||||
"@astrojs/rss": "^4.0.11",
|
"@astrojs/rss": "^4.0.11",
|
||||||
"@astrojs/tailwind": "^5.1.5",
|
"@astrojs/tailwind": "^5.1.5",
|
||||||
|
"@codemirror/commands": "catalog:",
|
||||||
|
"@codemirror/language": "catalog:",
|
||||||
|
"@codemirror/lang-javascript": "catalog:",
|
||||||
|
"@codemirror/state": "catalog:",
|
||||||
|
"@codemirror/view": "catalog:",
|
||||||
"@docsearch/css": "^3.8.3",
|
"@docsearch/css": "^3.8.3",
|
||||||
"@docsearch/react": "^3.8.3",
|
"@docsearch/react": "^3.8.3",
|
||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
@@ -28,6 +33,7 @@
|
|||||||
"@strudel/csound": "workspace:*",
|
"@strudel/csound": "workspace:*",
|
||||||
"@strudel/desktopbridge": "workspace:*",
|
"@strudel/desktopbridge": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
|
"@strudel/edo": "workspace:*",
|
||||||
"@strudel/gamepad": "workspace:*",
|
"@strudel/gamepad": "workspace:*",
|
||||||
"@strudel/hydra": "workspace:*",
|
"@strudel/hydra": "workspace:*",
|
||||||
"@strudel/midi": "workspace:*",
|
"@strudel/midi": "workspace:*",
|
||||||
@@ -35,6 +41,7 @@
|
|||||||
"@strudel/motion": "workspace:*",
|
"@strudel/motion": "workspace:*",
|
||||||
"@strudel/mqtt": "workspace:*",
|
"@strudel/mqtt": "workspace:*",
|
||||||
"@strudel/osc": "workspace:*",
|
"@strudel/osc": "workspace:*",
|
||||||
|
"@strudel/dough": "workspace:*",
|
||||||
"@strudel/serial": "workspace:*",
|
"@strudel/serial": "workspace:*",
|
||||||
"@strudel/soundfonts": "workspace:*",
|
"@strudel/soundfonts": "workspace:*",
|
||||||
"@strudel/tidal": "workspace:*",
|
"@strudel/tidal": "workspace:*",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
|
|||||||
|
|
||||||
<base href={BASE_URL} />
|
<base href={BASE_URL} />
|
||||||
|
|
||||||
<!-- Scrollable a11y code helper -->
|
<!-- Scrollable accessibility code helper -->
|
||||||
<script src={`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
|
<script src={`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
|
||||||
|
|
||||||
<script src="/src/pwa.ts"></script>
|
<script src="/src/pwa.ts"></script>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
|
|||||||
|
|
||||||
<base href={BASE_URL} />
|
<base href={BASE_URL} />
|
||||||
|
|
||||||
<!-- Scrollable a11y code helper -->
|
<!-- Scrollable accessibility code helper -->
|
||||||
<script src={`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
|
<script src={`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
|
||||||
|
|
||||||
<script src="/src/pwa.ts"></script>
|
<script src="/src/pwa.ts"></script>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const imageAlt = frontmatter.image?.alt ?? OPEN_GRAPH.image.alt;
|
|||||||
<!-- Page Metadata -->
|
<!-- Page Metadata -->
|
||||||
<link rel="canonical" href={canonicalUrl} />
|
<link rel="canonical" href={canonicalUrl} />
|
||||||
|
|
||||||
<!-- OpenGraph Tags -->
|
<!-- og Tags -->
|
||||||
<meta property="og:title" content={formattedContentTitle} />
|
<meta property="og:title" content={formattedContentTitle} />
|
||||||
<meta property="og:type" content="article" />
|
<meta property="og:type" content="article" />
|
||||||
<meta property="og:url" content={canonicalUrl} />
|
<meta property="og:url" content={canonicalUrl} />
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export const SIDEBAR: Sidebar = {
|
|||||||
{ text: 'Random Modifiers', link: 'learn/random-modifiers' },
|
{ text: 'Random Modifiers', link: 'learn/random-modifiers' },
|
||||||
{ text: 'Conditional Modifiers', link: 'learn/conditional-modifiers' },
|
{ text: 'Conditional Modifiers', link: 'learn/conditional-modifiers' },
|
||||||
{ text: 'Accumulation', link: 'learn/accumulation' },
|
{ text: 'Accumulation', link: 'learn/accumulation' },
|
||||||
|
{ text: 'LFOs', link: 'learn/lfo' },
|
||||||
{ text: 'Tonal Functions', link: 'learn/tonal' },
|
{ text: 'Tonal Functions', link: 'learn/tonal' },
|
||||||
{ text: 'Stepwise Functions', link: 'learn/stepwise' },
|
{ text: 'Stepwise Functions', link: 'learn/stepwise' },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function MiniRepl({
|
|||||||
tunes,
|
tunes,
|
||||||
hideHeader = false,
|
hideHeader = false,
|
||||||
canvasHeight = 100,
|
canvasHeight = 100,
|
||||||
onTrigger,
|
onSchedule,
|
||||||
punchcard,
|
punchcard,
|
||||||
punchcardLabels = true,
|
punchcardLabels = true,
|
||||||
claviature,
|
claviature,
|
||||||
@@ -58,8 +58,8 @@ export function MiniRepl({
|
|||||||
drawTime,
|
drawTime,
|
||||||
drawContext,
|
drawContext,
|
||||||
editPattern: (pat, id) => {
|
editPattern: (pat, id) => {
|
||||||
if (onTrigger) {
|
if (onSchedule) {
|
||||||
pat = pat.onTrigger(onTrigger, false);
|
pat = pat.onSchedule(onSchedule, false);
|
||||||
}
|
}
|
||||||
if (claviature) {
|
if (claviature) {
|
||||||
pat = pat.onPaint((ctx, time, haps, drawTime) => {
|
pat = pat.onPaint((ctx, time, haps, drawTime) => {
|
||||||
|
|||||||
@@ -150,5 +150,3 @@ In the future, the integration could be improved by passing all patterned contro
|
|||||||
This could work by a unique [channel](https://kunstmusik.github.io/icsc2022-csound-web/tutorial2-interacting-with-csound/#step-4---writing-continuous-data-channels)
|
This could work by a unique [channel](https://kunstmusik.github.io/icsc2022-csound-web/tutorial2-interacting-with-csound/#step-4---writing-continuous-data-channels)
|
||||||
for each value. Channels could be read [like this](https://github.com/csound/csound/blob/master/Android/CsoundForAndroid/CsoundAndroidExamples/src/main/res/raw/multitouch_xy.csd).
|
for each value. Channels could be read [like this](https://github.com/csound/csound/blob/master/Android/CsoundForAndroid/CsoundAndroidExamples/src/main/res/raw/multitouch_xy.csd).
|
||||||
Also, it might make sense to have a standard library of csound instruments for strudel's effects.
|
Also, it might make sense to have a standard library of csound instruments for strudel's effects.
|
||||||
|
|
||||||
Now, let's dive into the [Functional JavaScript API](/functions/intro)
|
|
||||||
|
|||||||
@@ -52,6 +52,15 @@ There are multiple ways to load your sample collection. Some methods are good fo
|
|||||||
- Serve a folder of samples locally using the [strudel 'sampler' commandline tool](https://strudel.cc/learn/samples/#from-disk-via-strudelsampler). This can be most reliable method, but requires [nodejs](https://nodejs.org) to be installed.
|
- Serve a folder of samples locally using the [strudel 'sampler' commandline tool](https://strudel.cc/learn/samples/#from-disk-via-strudelsampler). This can be most reliable method, but requires [nodejs](https://nodejs.org) to be installed.
|
||||||
- Host your sound library online on the web and [load them from an URL](/learn/samples/#loading-custom-samples)
|
- Host your sound library online on the web and [load them from an URL](/learn/samples/#loading-custom-samples)
|
||||||
|
|
||||||
|
## Can I create a new project based on Strudel?
|
||||||
|
|
||||||
|
Strudel is free/open source software, and we are always happy to see people making use of it within the following terms:
|
||||||
|
|
||||||
|
- Please don't use 'strudel' in the name of your project (e.g. strudel2000, foo-strudel), so people don't assume it's official strudel project. (If you'd like it to be an official strudel project, please check in with the community, e.g. on the [discord chat](https://discord.com/invite/HGEdXmRkzT).)
|
||||||
|
- Please respect our AGPL license, which e.g. requires you to share/link to the source code of strudel, any modifications you've made to it, and the source code for the rest of your project if it integrates with strudel. You are also required to maintain Strudel's copyright notices in the source code, and include Strudel's copyright notice in your user interface. This is an ad-hoc summary - please [refer to the license](https://codeberg.org/uzu/strudel/src/branch/main/LICENSE) for full details.
|
||||||
|
|
||||||
|
You are also encouraged to connect with [the community](https://discord.com/invite/HGEdXmRkzT) to understand our aims and values.
|
||||||
|
|
||||||
## Can I use Strudel with AI/LLM tools?
|
## Can I use Strudel with AI/LLM tools?
|
||||||
|
|
||||||
You are free to do what you like with Strudel, within the terms of the free/open source AGPLv3 license.
|
You are free to do what you like with Strudel, within the terms of the free/open source AGPLv3 license.
|
||||||
|
|||||||
@@ -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 to 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,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: Pattern Aligment
|
title: Pattern Alignment
|
||||||
layout: ../../layouts/MainLayout.astro
|
layout: ../../layouts/MainLayout.astro
|
||||||
---
|
---
|
||||||
|
|
||||||
import { MiniRepl } from '../../docs/MiniRepl';
|
import { MiniRepl } from '../../docs/MiniRepl';
|
||||||
|
|
||||||
# Pattern Aligment & Combination
|
# Pattern Alignment & Combination
|
||||||
|
|
||||||
One core aspect of Strudel, inherited from Tidal, is the flexible way that patterns can be combined, irrespective of their structure. Its declarative approach means a live coder does not have to think about the details of _how_ this is done, only _what_ is to be done.
|
One core aspect of Strudel, inherited from Tidal, is the flexible way that patterns can be combined, irrespective of their structure. Its declarative approach means a live coder does not have to think about the details of _how_ this is done, only _what_ is to be done.
|
||||||
|
|
||||||
|
|||||||
@@ -186,9 +186,9 @@ These functions are more low level, probably not needed by the live coder.
|
|||||||
|
|
||||||
# Other
|
# Other
|
||||||
|
|
||||||
## onTrigger
|
## onSchedule
|
||||||
|
|
||||||
<JsDoc client:idle name="Pattern#onTrigger" h={0} />
|
<JsDoc client:idle name="Pattern#onSchedule" h={0} />
|
||||||
|
|
||||||
## log
|
## log
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Besides a UI for playback control and meta information, the main part of the REP
|
|||||||
|
|
||||||
1. The user writes and updates code. Each update transpiles and evaluates it to create a `Pattern` instance
|
1. The user writes and updates code. Each update transpiles and evaluates it to create a `Pattern` instance
|
||||||
2. While the REPL is running, the `Scheduler` queries the active `Pattern` by a regular interval, generating `Events` (also known as `Haps` in Strudel) for the next time span.
|
2. While the REPL is running, the `Scheduler` queries the active `Pattern` by a regular interval, generating `Events` (also known as `Haps` in Strudel) for the next time span.
|
||||||
3. For each scheduling tick, all generated `Events` are triggered by calling their `onTrigger` method, which is set by the output.
|
3. For each scheduling tick, all generated `Events` are triggered by calling their `onSchedule` method, which is set by the output.
|
||||||
|
|
||||||
<img src="https://codeberg.org/uzu/strudel/raw/branch/talk/talk/public/strudelflow.png" width="600" />
|
<img src="https://codeberg.org/uzu/strudel/raw/branch/talk/talk/public/strudelflow.png" width="600" />
|
||||||
|
|
||||||
@@ -134,10 +134,7 @@ let minLatency = 0.1; // min time before a hap should trigger
|
|||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const haps = pattern.queryArc(time, time + interval);
|
const haps = pattern.queryArc(time, time + interval);
|
||||||
time += interval; // increment time
|
time += interval; // increment time
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => onSchedule(hap, hap.whole.begin + minLatency));
|
||||||
const deadline = hap.whole.begin - time + minLatency;
|
|
||||||
onTrigger(hap, deadline, duration);
|
|
||||||
});
|
|
||||||
}, interval * 1000); // query each "interval" seconds
|
}, interval * 1000); // query each "interval" seconds
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -181,20 +178,19 @@ This example creates the custom control parameters `x` and `y` which are then us
|
|||||||
|
|
||||||
### Outputs
|
### Outputs
|
||||||
|
|
||||||
Now that we know how the value of an event is manipulated using control parameters, we can look at how outputs can use that value to generate anything. The scheduler above was calling the `onTrigger` function which is used to implement the output. A very simple version of the web audio output could look like this:
|
Now that we know how the value of an hap is manipulated using control parameters, we can look at how outputs can use that value to generate anything. The scheduler above was calling the `onSchedule` function which is used to implement the output. A very simple version of the web audio output could look like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function onTrigger(hap, deadline, duration) {
|
function onSchedule(hap, time, cps) {
|
||||||
const { note } = hap.value;
|
const { note } = hap.value;
|
||||||
const time = getAudioContext().currentTime + deadline;
|
|
||||||
const o = getAudioContext().createOscillator();
|
const o = getAudioContext().createOscillator();
|
||||||
o.frequency.value = getFreq(note);
|
o.frequency.value = getFreq(note);
|
||||||
o.start(time);
|
o.start(time);
|
||||||
o.stop(time + event.duration);
|
o.stop(time + hap.duration / cps);
|
||||||
o.connect(getAudioContext().destination);
|
o.connect(getAudioContext().destination);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The above example will create an `OscillatorNode` for each event, where the frequency is controlled by the `note` param. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects.
|
The above example will create an `OscillatorNode` for each hap, where the frequency is controlled by the `note` param. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects.
|
||||||
|
|
||||||
I want to help, how do I contribute to the [Docs](/technical-manual/docs)?
|
I want to help, how do I contribute to the [Docs](/technical-manual/docs)?
|
||||||
|
|||||||
@@ -287,7 +287,10 @@ The modes are:
|
|||||||
- `below`: the top note of the voicing is lower than or equal to the anchor (default)
|
- `below`: the top note of the voicing is lower than or equal to the anchor (default)
|
||||||
- `above`: the bottom note of the voicing is higher than or equal to the anchor
|
- `above`: the bottom note of the voicing is higher than or equal to the anchor
|
||||||
- `duck`: the top note of the voicing is lower than the anchor
|
- `duck`: the top note of the voicing is lower than the anchor
|
||||||
- `root`: the bottom note of the voicing is always the root note closest to the anchor
|
- `root`: the bottom note of the voicing is always the lowest root note higher than or equal to the anchor
|
||||||
|
|
||||||
|
- `oldabove` : old (buggy) behavior of above, kept for legacy reasons
|
||||||
|
- `oldroot` : old (buggy) behavior of root, kept for legacy reasons
|
||||||
|
|
||||||
The `anchor` can also be set from within the `mode` function:
|
The `anchor` can also be set from within the `mode` function:
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user