Compare commits

..

16 Commits

Author SHA1 Message Date
Jade (Rose) Rowland 5fbc073f9f fix sample prefetch 2026-05-25 14:05:55 +02:00
Jade (Rose) Rowland 262fe3b516 fix ir 2026-05-23 18:47:24 +02:00
Jade (Rose) Rowland 6c67851909 fix merge conflict 2026-02-16 01:42:51 -05:00
Jade (Rose) Rowland 109ea3093b Merge branch 'soloshortcut' into show5 2026-02-11 19:56:37 -05:00
Jade (Rose) Rowland 8d991ad8ae Merge branch 'sample_note' into show5 2026-02-11 19:53:20 -05:00
Jade (Rose) Rowland 0b69ce21c7 jsdoc 2026-02-08 21:11:10 -05:00
Jade (Rose) Rowland 4f05674cb3 working 2026-02-08 20:32:59 -05:00
Jade (Rose) Rowland feab3f5c86 freq 2026-02-08 19:25:10 -05:00
Jade (Rose) Rowland 90c05ec38a fix edge cases 2026-02-08 14:12:21 -05:00
Jade (Rose) Rowland 7e0eed87cb normalize shortcuts 2026-01-27 17:21:36 -05:00
Jade (Rose) Rowland 1a1197f67a Merge branch 'main' into soloshortcut 2026-01-22 21:11:31 -05:00
Jade (Rose) Rowland 3c5afb32f3 scroll to center 2026-01-22 02:04:27 -05:00
Jade (Rose) Rowland 76cbd23859 Merge branch 'soloshortcut' of ssh://codeberg.org/uzu/strudel into soloshortcut 2026-01-22 00:25:41 -05:00
Jade (Rose) Rowland b436ae789c rm dead code 2026-01-22 00:25:28 -05:00
Switch Angel AKA Jade Rose 275731afc7 Merge branch 'main' into soloshortcut 2026-01-22 06:22:17 +01:00
Jade (Rose) Rowland 5cb2214d88 working 2026-01-22 00:20:44 -05:00
74 changed files with 1000 additions and 3138 deletions
-11
View File
@@ -1,11 +0,0 @@
**/node_modules
**/.angular
**/dist
.astro/
.forgejo/
.idea/
.vscode/
test/
.gitignore
Dockerfile
.dockerignore
+2 -2
View File
@@ -1,4 +1,4 @@
name: Build and Deploy to warm (strudel.cc) name: Build and Deploy to beta (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/strudel.cc rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/warm.strudel.cc
+2 -2
View File
@@ -1,4 +1,4 @@
name: Build and Deploy to cold (cold.strudel.cc) name: Build and Deploy to live (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/cold.strudel.cc rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc
+1 -11
View File
@@ -52,17 +52,7 @@ 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](https://discord.com/invite/HGEdXmRkzT). Please do not discuss or share LLM-related things outside of those channels. There are #llm-chat and #llm-share channels on our discord. 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
-1
View File
@@ -29,7 +29,6 @@ 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'),
); );
-1
View File
@@ -15,7 +15,6 @@
"@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:*",
-1
View File
@@ -4,7 +4,6 @@ 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';
-1
View File
@@ -48,7 +48,6 @@
"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:*",
+85 -24
View File
@@ -20,11 +20,16 @@ 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';
@@ -44,9 +49,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()]));
@@ -75,6 +80,10 @@ 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();
@@ -138,12 +147,75 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
}, },
{ {
key: 'Alt-w', key: 'Alt-w',
run: (view) => jumpToCharacter(view, '$', 1), run: (view) => jumpToNextCharacter(view, ANON_LABEL, 1),
}, },
{ {
key: 'Alt-q', key: 'Alt-q',
run: (view) => jumpToCharacter(view, '$', -1), run: (view) => {
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?.()),
@@ -230,7 +302,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');
@@ -293,9 +365,6 @@ 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();
@@ -310,9 +379,6 @@ 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);
@@ -353,9 +419,6 @@ 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) {
@@ -444,15 +507,13 @@ export class StrudelMirror {
this.setFontSize(value); this.setFontSize(value);
} }
} }
replaceCode(code, from, to) {
const changes = { from, to, insert: code };
this.editor.dispatch({ changes });
}
insertCode(code, position) {
this.replaceCode(code, position, position);
}
setCode(code) { setCode(code) {
this.replaceCode(code, 0, this.editor.state.doc.length); const changes = {
from: 0,
to: this.editor.state.doc.length,
insert: code,
};
this.editor.dispatch({ changes });
} }
// used for debugging but could serve other purposes // used for debugging but could serve other purposes
getActiveWidgets() { getActiveWidgets() {
-1
View File
@@ -5,4 +5,3 @@ 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';
+4 -5
View File
@@ -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.cm6; const view = cm?.view || cm;
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,16 +112,15 @@ try {
// :q to pause/stop // :q to pause/stop
Vim.defineEx('quit', 'q', (cm) => { Vim.defineEx('quit', 'q', (cm) => {
const view = cm.cm6; const view = cm?.view || cm;
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.cm6; const view = cm?.view || cm; // CM6 Vim passes either an object with view or the view itself
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');
+133 -11
View File
@@ -1,18 +1,54 @@
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;
while (!cursor.next().done) { const characterPositions = getCharacterPositions(state, character);
characterPositions.push(cursor.value.to);
}
if (!characterPositions.length) { if (!characterPositions.length) {
return false; return true;
} }
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
@@ -21,11 +57,97 @@ export function jumpToCharacter(view, character, direction = 1) {
} }
if (jumpPos == null) { if (jumpPos == null) {
return false; return true;
} }
const selection = EditorSelection.cursor(jumpPos - 1);
dispatch({ dispatch({
selection: EditorSelection.cursor(jumpPos - 1), selection,
scrollIntoView: true, effects: EditorView.scrollIntoView(
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;
}
+7 -7
View File
@@ -32,13 +32,13 @@
}, },
"homepage": "https://codeberg.org/uzu/strudel#readme", "homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "catalog:", "@codemirror/autocomplete": "^6.18.4",
"@codemirror/commands": "catalog:", "@codemirror/commands": "^6.8.0",
"@codemirror/lang-javascript": "catalog:", "@codemirror/lang-javascript": "^6.2.2",
"@codemirror/language": "catalog:", "@codemirror/language": "^6.10.8",
"@codemirror/search": "catalog:", "@codemirror/search": "^6.5.8",
"@codemirror/state": "catalog:", "@codemirror/state": "^6.5.1",
"@codemirror/view": "catalog:", "@codemirror/view": "^6.36.2",
"@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",
+6 -16
View File
@@ -1906,19 +1906,13 @@ 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} delay in seconds * @param {number | Pattern} delaytime sets the time of the delay effect.
* @synonyms delayt, dt * @synonyms delayt, dt
* @example * @example
* note("d d a# a".fast(2)) * note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -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');
@@ -2582,17 +2576,13 @@ export const { compressorRelease } = registerControl('compressorRelease');
export const { speed } = registerControl('speed'); export const { speed } = registerControl('speed');
/** /**
* Changes the pitch of the sample without changing its speed. * Changes the speed of sample playback, i.e. a cheap way of changing pitch.
* 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 between `-4` and `inf`. Positive increases pitch, 0 does nothing, negative decreases the pitch. * @param {number | Pattern} factor -inf to inf, negative numbers play the sample backwards.
* @example * @example
* s("gm_flute").stretch("<2 1 0 -2>") * s("gm_flute").stretch("1 2 .5")
* *
*/ */
export const { stretch } = registerControl('stretch'); export const { stretch } = registerControl('stretch');
+1 -146
View File
@@ -1056,55 +1056,7 @@ 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)],
@@ -1158,6 +1110,7 @@ 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)],
@@ -1741,12 +1694,6 @@ 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) {
@@ -1869,8 +1816,6 @@ 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`
@@ -2552,20 +2497,6 @@ 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
@@ -2580,21 +2511,6 @@ 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
@@ -3359,11 +3275,6 @@ 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*
* *
@@ -4132,59 +4043,3 @@ 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();
};
+1 -8
View File
@@ -307,12 +307,8 @@ export function repl({
let allTransforms = []; let allTransforms = [];
/** /**
* 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 * Applies a function to all the running patterns. Note that the patterns are groups together into a single `stack` before the function is applied. This is probably what you want, but see `each` for
* a version that applies the function to each pattern separately. * 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")
@@ -332,9 +328,6 @@ 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")
+3 -5
View File
@@ -316,7 +316,7 @@ export const run = (n) => saw.range(0, n).round().segment(n);
* // "hh".s().struct("1 0 1") * // "hh".s().struct("1 0 1")
*/ */
export const binary = (n) => { export const binary = (n) => {
const nBits = reify(n).log2().floor().add(1); const nBits = reify(n).log2(0).floor().add(1);
return binaryN(n, nBits); return binaryN(n, nBits);
}; };
@@ -348,7 +348,7 @@ export const binaryN = (n, nBits = 16) => {
* .partials(binaryL(irand(4096).add(1))) * .partials(binaryL(irand(4096).add(1)))
*/ */
export const binaryL = (n) => { export const binaryL = (n) => {
const nBits = reify(n).log2().floor().add(1); const nBits = reify(n).log2(0).floor().add(1);
return binaryNL(n, nBits); return binaryNL(n, nBits);
}; };
@@ -389,9 +389,7 @@ 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,...
let rands = getRandsAtTime(t.floor().add(0.5), n, controls.randSeed); const 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]))
+1 -7
View File
@@ -9,7 +9,7 @@ import Fraction from 'fraction.js';
import { describe, it, expect, vi } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs'; import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs';
import { fastcat, sequence, State, TimeSpan, Hap, note } from '../index.mjs'; import { fastcat, sequence, State, TimeSpan, Hap } from '../index.mjs';
const st = (begin, end) => new State(ts(begin, end)); const st = (begin, end) => new State(ts(begin, end));
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end)); const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
@@ -59,9 +59,3 @@ 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());
});
});
+1 -32
View File
@@ -54,42 +54,11 @@ 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();
// Draw interval label for degree i when it exists: ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
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();
-41
View File
@@ -1,41 +0,0 @@
# @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
}
]"
]
```
-114
View File
@@ -1,114 +0,0 @@
/*
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
);
-213
View File
@@ -1,213 +0,0 @@
/*
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;
}
}
-5
View File
@@ -1,5 +0,0 @@
import './edo.mjs';
export * from './edo.mjs';
export const packageName = '@strudel/edo';
-103
View File
@@ -1,103 +0,0 @@
/*
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;
}
}
}
-42
View File
@@ -1,42 +0,0 @@
{
"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"
}
}
-98
View File
@@ -1,98 +0,0 @@
/*
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;
}
}
}
-95
View File
@@ -1,95 +0,0 @@
/*
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;
-34
View File
@@ -1,34 +0,0 @@
/*
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));
});
});
-21
View File
@@ -1,21 +0,0 @@
/*
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);
});
});
-18
View File
@@ -1,18 +0,0 @@
/*
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);
});
});
-19
View File
@@ -1,19 +0,0 @@
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',
},
});
+1 -8
View File
@@ -537,9 +537,6 @@ 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
@@ -555,10 +552,6 @@ 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 = {};
@@ -640,7 +633,7 @@ export async function midikeys(input) {
*/ */
return; return;
} else { } else {
value = { ...value, note: Math.round(note), velocity: velocity / 127, midichan: message.channel }; value = { ...value, note: Math.round(note), velocity: velocity / 127 };
} }
kHaps[input].push(new Hap(span, span, value, {})); kHaps[input].push(new Hap(span, span, value, {}));
if (!noteoff && triggerAvailable) { if (!noteoff && triggerAvailable) {
+2 -10
View File
@@ -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, createParams } from '@strudel/core'; import { Pattern, isPattern } from '@strudel/core';
import Paho from 'paho-mqtt'; import Paho from 'paho-mqtt';
const connections = {}; const connections = {};
@@ -61,6 +61,7 @@ Pattern.prototype.mqtt = function (
const props = { const props = {
onSuccess: onConnect, onSuccess: onConnect,
onFailure: onFailure, onFailure: onFailure,
useSSL: true,
}; };
if (username) { if (username) {
@@ -118,12 +119,3 @@ Pattern.prototype.mqtt = function (
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true }); 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);
};
+1 -1
View File
@@ -42,7 +42,7 @@ 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.midinote = parseNumeral(controls.note); controls.note = parseNumeral(controls.note);
} }
} }
controls.bank && (controls.s = controls.bank + controls.s); controls.bank && (controls.s = controls.bank + controls.s);
-1
View File
@@ -36,7 +36,6 @@
"@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:*",
-1
View File
@@ -8,7 +8,6 @@ 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'),
+56 -44
View File
@@ -1,5 +1,5 @@
import { getBaseURL, getCommonSampleInfo } from './util.mjs'; import { BASE_MIDI_NOTE, getBaseURL, getCommonSampleInfo, noteToFreq, noteToMidi } from './util.mjs';
import { registerSound, registerWaveTable, soundMap } from './index.mjs'; import { registerSound, registerWaveTable } from './index.mjs';
import { getAudioContext } from './audioContext.mjs'; import { getAudioContext } from './audioContext.mjs';
import { import {
getADSRValues, getADSRValues,
@@ -9,11 +9,18 @@ import {
onceEnded, onceEnded,
releaseAudioNode, releaseAudioNode,
} from './helpers.mjs'; } from './helpers.mjs';
import { logger } from './logger.mjs'; import { errorLogger, 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) {
@@ -30,33 +37,11 @@ function humanFileSize(bytes, si) {
return bytes.toFixed(1) + ' ' + units[u]; return bytes.toFixed(1) + ' ' + units[u];
} }
/** function getSampleInfo(hapValue, bank) {
* Returns the duration, in seconds, of the given sample.
* Has optional param `n` (for instance, the `2` in `s("casio:2")`)
*
* Note: `must` be called with await, otherwise you'll get a pending Promise object.
*
* @name getDuration,getDur
* @tag samples
* @param {string} sampleName
* @param {number} (optional) n
*
* @example
* // Set a patterns cycle length to exactly the length of the sample
* samples('github:tidalcycles/dirt-samples')
* let k = await getDuration('sax')
* s("sax").cps(1/k)
*/
export const getDuration = (s, n = 0) => {
return getSampleBufferSource({ s, n }, soundMap.get(s)[s].data.samples).then((x) => x.bufferDuration);
};
export const getDur = getDuration;
export function getSampleInfo(hapValue, bank) {
const { speed = 1.0 } = hapValue; const { speed = 1.0 } = hapValue;
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank); const { transpose, url, index, midi, label, baseFrequency } = getCommonSampleInfo(hapValue, bank);
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
const playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
return { transpose, url, index, midi, label, playbackRate }; return { transpose, url, index, midi, label, playbackRate };
} }
@@ -179,7 +164,12 @@ 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);
@@ -238,22 +228,31 @@ export async function fetchSampleMap(url) {
} }
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`; url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
} }
if (typeof fetch !== 'function') {
// not a browser
return;
}
const base = getBaseURL(url); const base = getBaseURL(url);
if (typeof fetch === 'undefined') { if (typeof fetch !== 'function') {
// skip fetch when in node / testing errorLogger(new Error(`fetch is not supported in this environment. Skipping map load for: ${url}`), 'sampler.mjs')
return; return [{}, base || ''];
} }
const json = await fetch(url)
.then((res) => res.json())
.catch((error) => { try {
console.error(error); const res = await fetch(url);
throw new Error(`error loading "${url}"`); if (!res.ok) {
}); throw new Error(`HTTP error! status: ${res.status}`);
return [json, json._base || base]; }
const json = await res.json();
return [json, json._base || base];
} catch (error) {
// Catching the failure here prevents it from bubbling up and crashing upstream
errorLogger(new Error(`Failed to fetch or parse sample map at "${url}":`), "sampler.mjs");
// Return a safe fallback structure so destructuring (e.g., [json, base]) won't break
return [{}, base || ''];
}
} }
/** /**
@@ -390,3 +389,16 @@ 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);
}
+3 -3
View File
@@ -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 { getSampleBufferSource, loadBuffer } from './sampler.mjs'; import { 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';
@@ -963,9 +963,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
let url; let url;
let sample = getSound(ir); let sample = getSound(ir);
if (Array.isArray(sample)) { if (Array.isArray(sample)) {
url = sample.data.samples[i % sample.data.samples.length]; url = sample.data.samples[i % sample.data.samples.length].url;
} else if (typeof sample === 'object') { } else if (typeof sample === 'object') {
url = Object.values(sample.data.samples).flat()[i % Object.values(sample.data.samples).length]; url = Object.values(sample.data.samples).flat()[i % Object.values(sample.data.samples).length].url;
} }
roomIR = await loadBuffer(url, ac, ir, 0); roomIR = await loadBuffer(url, ac, ir, 0);
} }
+2 -4
View File
@@ -485,10 +485,8 @@ 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);
const R0 = R; R = c * R - s * I;
const I0 = I; I = s * R + c * 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;
+42 -10
View File
@@ -19,7 +19,12 @@ 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) {
@@ -29,6 +34,11 @@ 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;
}; };
@@ -38,7 +48,17 @@ 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');
} }
@@ -52,10 +72,15 @@ export const valueToMidi = (value, fallbackValue) => {
if (typeof note === 'number') { if (typeof note === 'number') {
return note; return note;
} }
if (!fallbackValue) { return;
}
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 fallbackValue; return parsedValue;
}; };
export function nanFallback(value, fallback = 0, silent) { export function nanFallback(value, fallback = 0, silent) {
@@ -84,15 +109,18 @@ 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;
let midi = valueToMidi(hapValue, 36); const maybeMidiNote = __valueToMidi(hapValue);
let transpose = midi - 36; // C3 is middle C; const midi = maybeMidiNote ?? BASE_MIDI_NOTE;
let url; let transpose = midi - BASE_MIDI_NOTE; // C3 is middle C;
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);
url = bank[index]; samplemeta = 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
@@ -104,10 +132,14 @@ 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);
url = bank[closest][index]; samplemeta = bank[closest][index];
} }
const label = `${s}:${index}`; const label = `${s}:${index}`;
return { transpose, url, index, midi, label }; if (maybeMidiNote != null) {
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`
+3 -24
View File
@@ -1,31 +1,10 @@
# @strudel/transpiler # @strudel/transpiler
This package contains a JS code transpiler with a plugin system that can be used to transform the code. This package contains a JS code transpiler with the following features:
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 -13
View File
@@ -1,17 +1,5 @@
/*
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';
import './plugin-kabelsalat.mjs'; export const evaluate = (code) => _evaluate(code, transpiler);
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);
-235
View File
@@ -1,235 +0,0 @@
/*
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,
};
}
-200
View File
@@ -1,200 +0,0 @@
/*
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]);
-29
View File
@@ -1,29 +0,0 @@
/*
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);
-128
View File
@@ -1,128 +0,0 @@
/*
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);
+1 -1
View File
@@ -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 '../index.mjs'; import { transpiler } from '../transpiler.mjs';
const simple = { wrapAsync: false, addReturn: false, simpleLocs: true }; const simple = { wrapAsync: false, addReturn: false, simpleLocs: true };
+502 -44
View File
@@ -1,12 +1,13 @@
/* 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
@@ -16,31 +17,16 @@ 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 = {}) {
options = { const {
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, {
@@ -50,7 +36,8 @@ export function transpiler(input, options = {}) {
onComment: comments, onComment: comments,
}); });
let miniDisableRanges = findMiniDisableRanges(comments, input.length); const 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;
@@ -58,13 +45,23 @@ 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;
@@ -82,12 +79,82 @@ export function transpiler(input, options = {}) {
scopeDeclarations.push(node.id.name); scopeDeclarations.push(node.id.name);
} }
} }
if (isLanguageLiteral(node)) {
for (const plugin of plugins) { const { name } = node.tag;
if (!plugin?.enter?.call(this, node, parent, prop, index)) continue; const language = languages.get(name);
return; 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 = {
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));
}
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
@@ -116,10 +183,61 @@ export function transpiler(input, options = {}) {
}, },
leave(node, parent, prop, index) { leave(node, parent, prop, index) {
for (const plugin of plugins) { if (!isKabelCall(node)) return;
if (!plugin?.leave?.call(this, node, parent, prop, index)) continue;
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 { 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,
});
}, },
}); });
@@ -138,7 +256,12 @@ 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)
body.push(silenceExpression); if (blockBased) {
// For block-based eval, add silence as the return value when block ends with declaration
body.push(silenceExpression);
} else {
throw new Error('unexpected ast format without body expression');
}
} }
// For block-based eval, add scope assignments before the return statement // For block-based eval, add scope assignments before the return statement
@@ -164,17 +287,280 @@ export function transpiler(input, options = {}) {
if (!emitMiniLocations) { if (!emitMiniLocations) {
return { output }; return { output };
} }
return { output, miniLocations, widgets, sliders, labels };
}
let pluginContext; function isKabelCall(node) {
({ options, input, miniDisableRanges, nodeOffset, ...pluginContext } = context); 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';
}
return { output, ...pluginContext }; function shouldCallKabelExpression(expr) {
if (expr.type !== 'ArrowFunctionExpression' && expr.type !== 'FunctionExpression') {
return false;
}
if (expr.params.length) {
return false;
}
return expr.body?.type === 'BlockStatement';
}
function genExprSource(expr) {
return escodegen.generate(expr, { format: { semicolons: false } });
}
function extractPatternPlaceholders(expr) {
const templateExpr = cloneNode(expr);
const parents = new Map();
const targets = [];
walk(templateExpr, {
enter(node, parent, prop, index) {
parents.set(node, { parent, prop, index });
const patternExpr = getStrudelPatternExpr(node);
if (patternExpr) {
targets.push({ node, patternExpr });
this.skip();
}
},
});
if (!targets.length) {
return { template: genExprSource(templateExpr), patternExprs: [] };
}
targets.sort((a, b) => getPatternNodeOrder(a.node) - getPatternNodeOrder(b.node));
const patternExprs = targets.map(({ patternExpr }) => cloneNode(patternExpr));
let currentExpr = templateExpr;
targets.forEach(({ node }, index) => {
currentExpr = replaceNode(node, placeholderAst(index), parents, currentExpr);
});
const template = genExprSource(currentExpr);
return { template, patternExprs };
}
function getStrudelPatternExpr(node) {
if (isStrudelPatternWrap(node)) {
const arg = node.arguments?.[0];
if (!arg) {
throw new Error('S(...) requires an argument');
}
return arg;
}
if (isMiniCall(node)) {
return node;
}
return null;
}
function isStrudelPatternWrap(node) {
if (node.type !== 'CallExpression') {
return false;
}
const callee = node.callee;
if (callee.type === 'Identifier') {
return callee.name === 'S';
}
if (callee.type === 'MemberExpression' && !callee.computed) {
return callee.property?.name === 'S';
}
return false;
}
function getMinilangName() {
const minilang = languages.get('minilang');
return minilang?.name || 'm';
}
// Used to identify transpiled `m(...)` calls for proper conversion
// to, say, kabelsalat placeholders
function isMiniCall(node) {
if (node.type !== 'CallExpression') {
return false;
}
const callee = node.callee;
if (callee.type !== 'Identifier') {
return false;
}
if (callee.name !== getMinilangName()) {
return false;
}
const firstArg = node.arguments?.[0];
return firstArg?.type === 'Literal' && typeof firstArg.value === 'string';
}
// If `start` is available, we use it. If it's already been transpiled
// to `m(...)`, use the provided offset
function getPatternNodeOrder(node) {
if (typeof node.start === 'number') {
return node.start;
}
if (isMiniCall(node)) {
const offsetArg = node.arguments?.[1];
if (offsetArg?.type === 'Literal' && typeof offsetArg.value === 'number') {
return offsetArg.value;
}
}
return 0;
}
function placeholderAst(index) {
return {
type: 'MemberExpression',
object: { type: 'Identifier', name: 'pat' },
property: { type: 'Literal', value: index },
computed: true,
optional: false,
};
}
function replaceNode(node, replacement, parents, currentRoot) {
const info = parents.get(node);
if (!info || !info.parent) {
return replacement;
}
const { parent, prop, index } = info;
if (Array.isArray(parent[prop])) {
parent[prop][index] = replacement;
} else {
parent[prop] = replacement;
}
parents.set(replacement, { parent, prop, index });
return currentRoot;
}
function cloneNode(node) {
return JSON.parse(JSON.stringify(node));
}
function workletMemberAst(objectExpr) {
return {
type: 'MemberExpression',
object: objectExpr,
property: { type: 'Identifier', name: 'worklet' },
computed: false,
optional: false,
};
}
function isStringWithDoubleQuotes(node, locations, code) {
if (node.type !== 'Literal') {
return false;
}
return node.raw[0] === '"';
}
function isBackTickString(node, parent) {
return node.type === 'TemplateLiteral' && parent.type !== 'TaggedTemplateExpression';
}
function miniWithLocation(value, node) {
const { start: fromOffset } = node;
const minilang = languages.get('minilang');
let name = 'm';
if (minilang && minilang.name) {
name = minilang.name; // name is expected to be exported from the package of the minilang
}
return {
type: 'CallExpression',
callee: {
type: 'Identifier',
name,
},
arguments: [
{ type: 'Literal', value },
{ type: 'Literal', value: fromOffset },
],
optional: false,
};
}
// these functions are connected to @strudel/codemirror -> slider.mjs
// maybe someday there will be pluggable transpiler functions, then move this there
function isSliderFunction(node) {
return node.type === 'CallExpression' && node.callee.name === 'slider';
}
function isWidgetMethod(node) {
return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name);
}
function sliderWithLocation(node, 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';
} }
@@ -205,6 +591,69 @@ 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)
@@ -346,3 +795,12 @@ 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;
}
-1
View File
@@ -34,7 +34,6 @@
"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:*",
-2
View File
@@ -3,7 +3,6 @@ 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';
@@ -18,7 +17,6 @@ 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 },
-1
View File
@@ -149,7 +149,6 @@ 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;
+7 -83
View File
@@ -4,30 +4,6 @@ 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:
.: .:
@@ -35,9 +11,6 @@ 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
@@ -120,9 +93,6 @@ 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
@@ -201,25 +171,25 @@ importers:
packages/codemirror: packages/codemirror:
dependencies: dependencies:
'@codemirror/autocomplete': '@codemirror/autocomplete':
specifier: 'catalog:' specifier: ^6.18.4
version: 6.18.4 version: 6.18.4
'@codemirror/commands': '@codemirror/commands':
specifier: 'catalog:' specifier: ^6.8.0
version: 6.8.0 version: 6.8.0
'@codemirror/lang-javascript': '@codemirror/lang-javascript':
specifier: 'catalog:' specifier: ^6.2.2
version: 6.2.2 version: 6.2.2
'@codemirror/language': '@codemirror/language':
specifier: 'catalog:' specifier: ^6.10.8
version: 6.10.8 version: 6.10.8
'@codemirror/search': '@codemirror/search':
specifier: 'catalog:' specifier: ^6.5.8
version: 6.5.8 version: 6.5.8
'@codemirror/state': '@codemirror/state':
specifier: 'catalog:' specifier: ^6.5.1
version: 6.5.1 version: 6.5.1
'@codemirror/view': '@codemirror/view':
specifier: 'catalog:' specifier: ^6.36.2
version: 6.36.2 version: 6.36.2
'@lezer/highlight': '@lezer/highlight':
specifier: ^1.2.1 specifier: ^1.2.1
@@ -313,28 +283,6 @@ 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:
@@ -501,9 +449,6 @@ 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
@@ -679,9 +624,6 @@ 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
@@ -763,21 +705,6 @@ 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
@@ -811,9 +738,6 @@ importers:
'@strudel/draw': '@strudel/draw':
specifier: workspace:* specifier: workspace:*
version: link:../packages/draw version: link:../packages/draw
'@strudel/edo':
specifier: workspace:*
version: link:../packages/edo
'@strudel/gamepad': '@strudel/gamepad':
specifier: workspace:* specifier: workspace:*
version: link:../packages/gamepad version: link:../packages/gamepad
-9
View File
@@ -10,12 +10,3 @@ 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"
+21 -454
View File
@@ -1242,35 +1242,6 @@ 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 ]",
@@ -3041,6 +3012,15 @@ 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 ]",
@@ -3091,43 +3071,6 @@ 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 ]",
@@ -4016,93 +3959,6 @@ 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 ]",
@@ -5335,15 +5191,6 @@ 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 ]",
@@ -6556,271 +6403,6 @@ 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`] = `
@@ -7956,8 +7538,6 @@ 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 ]",
@@ -11530,27 +11110,6 @@ 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 ]",
@@ -12679,10 +12238,18 @@ 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/1 | s:gm_flute stretch:2 ]", "[ (0/1 → 1/3) ⇝ 1/1 | s:gm_flute stretch:1 ]",
"[ 1/1 → 2/1 | s:gm_flute stretch:1 ]", "[ 0/1 ⇜ (1/3 → 2/3) ⇝ 1/1 | s:gm_flute stretch:2 ]",
"[ 2/1 → 3/1 | s:gm_flute stretch:0 ]", "[ 0/1 ⇜ (2/3 → 1/1) | s:gm_flute stretch:0.5 ]",
"[ 3/1 → 4/1 | s:gm_flute stretch:-2 ]", "[ (1/1 → 4/3) ⇝ 2/1 | s:gm_flute stretch:1 ]",
"[ 1/1 ⇜ (4/3 → 5/3) ⇝ 2/1 | s:gm_flute stretch:2 ]",
"[ 1/1 ⇜ (5/3 → 2/1) | s:gm_flute stretch:0.5 ]",
"[ (2/1 → 7/3) ⇝ 3/1 | s:gm_flute stretch:1 ]",
"[ 2/1 ⇜ (7/3 → 8/3) ⇝ 3/1 | s:gm_flute stretch:2 ]",
"[ 2/1 ⇜ (8/3 → 3/1) | s:gm_flute stretch:0.5 ]",
"[ (3/1 → 10/3) ⇝ 4/1 | s:gm_flute stretch:1 ]",
"[ 3/1 ⇜ (10/3 → 11/3) ⇝ 4/1 | s:gm_flute stretch:2 ]",
"[ 3/1 ⇜ (11/3 → 4/1) | s:gm_flute stretch:0.5 ]",
] ]
`; `;
-4
View File
@@ -13,7 +13,6 @@ 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';
@@ -127,7 +126,6 @@ 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
@@ -146,7 +144,6 @@ evalScope(
toneHelpersMocked, toneHelpersMocked,
uiHelpersMocked, uiHelpersMocked,
webaudio, webaudio,
edoHelpers,
tonalHelpers, tonalHelpers,
gamepadHelpers, gamepadHelpers,
/* /*
@@ -171,7 +168,6 @@ evalScope(
loadCSound, loadCSound,
loadCsound, loadCsound,
loadcsound, loadcsound,
getDuration,
setcps: id, setcps: id,
setcpm: id, setcpm: id,
Clock: {}, // whatever Clock: {}, // whatever
-1
View File
@@ -78,7 +78,6 @@ 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,
-6
View File
@@ -17,11 +17,6 @@
"@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",
@@ -33,7 +28,6 @@
"@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:*",
-1
View File
@@ -98,7 +98,6 @@ 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' },
], ],
-9
View File
@@ -52,15 +52,6 @@ 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.
-301
View File
@@ -1,301 +0,0 @@
---
title: Low-frequency oscillators (LFO)
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
# Low frequency oscillators (LFO)
A low frequency oscillator (or short LFO) is a common way on synthesizers to continuously modulate various signals.
This documentation is an interactive version of glossing's tutorial https://www.youtube.com/watch?v=11frBA9L638
## Signals vs LFOs
In Strudel, there are two ways to modulate:
1. [signals](/learn/signals/) for pattern-level modulation
2. `lfo` (this page) for audio-level modulation
## Applying an LFO
Here, the LFO will change the frequency of the `saw`. Put a comment like this `//.lfo()` to see and hear how it changes, and remove the comment again.
<MiniRepl
client:idle
tune={`
s("saw").lfo()
.lpf(800)
._spectrum({height: 300, width: 800})`}
/>
By default, the LFO will modulate the control parameter which is right before `.lfo()`:
<MiniRepl
client:idle
tune={`
s("saw")
.lpf(800).lfo()
._spectrum({height: 300, width: 800})`}
/>
Here, the LFO will modulate the low pass filter `.lpf`.
## Moving away from the default
The following sections explain how pass parameters to `.lfo`. Similar to `._spectrum` above, almost all the configuration of `lfo` lives inside a json object, starting with `{` and ending with `}`.
All the parameters (except `id`) are written as `key: value` inside and separated by `,`.
The reference refers to them as `config.key`, i.e. for the following one as `config.control` but you use them like below.
## Control
`control` determines which parameter will be modulated. This allows you to place your `lfo` at different places,
not necessarily immediately after the controlled parameter.
Here, we place `lfo` after the `s`, but it modulates the low pass filter.
<MiniRepl
client:idle
tune={`
s("saw")
.lfo({control:'lpf'})
.lpf(800)
._spectrum({height: 300, width: 800})`}
/>
You can even influence parameters which are always present, even if you haven't explicitly written them, like `gain`.
<MiniRepl
client:idle
tune={`
s("saw")
.lfo({control:'gain'})
.lpf(800)
._spectrum({height: 300, width: 800})`}
/>
`control` has an alias `c`.
## Rate
`rate` determines how often the `lfo` oscillates per second:
<MiniRepl
client:idle
tune={`
s("saw")
.lfo({c:'gain', rate:"<2 4>"})
.lpf(800)
._spectrum({height: 300, width: 800})`}
/>
The alias of `rate` is `r`.{' '}
## Sync
Instead of controlling the frequency with `rate` by setting a frequency in Hz, you can use `sync`
to snychronize your lfo with your other patterns.
`sync` expresses this frequency as "times per cycle"
Try removing the `sync: "<2 4 8 0.5 >"` from the pattern and notice that there is something not in sync.
<MiniRepl
client:idle
tune={`
$: s("bd*4").bank("TR909").postgain(0.5)
$: s("saw")
.lpf(800)
.lfo({sync: "<2 4 8 0.5 >"})
._spectrum({height: 300, width: 800})`}
/>
You can put patterns into the parameters of `lfo` if you want them to change over time, as seen above.
## Relative Depth
`.depth` is relative depth, a value of 1 (the default) means that
the value goes above and below by half of the value which is being modulated
E.g. for depth `1`, it modulates the frequency to oscillate between 32 = (64/2) and 96 (= 64 + 64/2).
<MiniRepl
client:idle
tune={`
s("saw").freq(64)
.lfo({r: 2, depth: "<1 2 3>"})
.lpf(800)
._spectrum({height: 300, width: 800})`}
/>
The `freq` is not needed here, as this is the default frequency, it's just for instructive purposes.
The aliases of `depth` are `dr` and `dep`.
## Absolute Depth
`depthabs` controls the absolute modulation depth. For example you can modulate
the low pass filter by exactly 250 Hz up and below:
<MiniRepl
client:idle
tune={`
s("saw")
.lpf(800)
.lfo({r: 2, depthabs: "250"})`}
/>
The alias of `depthabs` is `da`.
## DC offset
If you don't want to go up or down by the same amount with your modulation, then you can shift the center of the modulation with `dcoffset`.
The default value is -0.5, which is the middle point between:
"dcoffset = 0: All modulations increase the control parameter (or keep it constant)"
"dcoffset = -1: All modulations decrease the control parameter (or keep it constant)"
Other values will work as well.
<MiniRepl
client:idle
tune={`
s("saw")
.lpf(600)
.lfo({r: 2, da: "200", dcoffset: "<-0.5 -1 -0.5 0>"})`}
/>
The marvellous alias of `dcoffset` is `dc`.
## Shape
You can change the shape of the modulation with `shape`. The default is `triangle`, but other shapes are available too:
<MiniRepl
client:idle
tune={`
s("saw")
.lpf(800)
.lfo({r: 2, shape: "<triangle sine ramp saw square>"}) `}
/>
You can add `._spectrum()` to see the shape of the modulations in the spectrum.
You can also get these shapes by using numbers:
| Shape | number |
| -------- | ------ |
| triangle | 0 |
| sine | 1 |
| ramp | 2 |
| saw | 3 |
| square | 4 |
this way, you can use a function like `irand` to generate numbers:
<MiniRepl
client:idle
tune={`
s("saw")
.lpf(800)
.lfo({r: 2, shape: irand(4)}) `}
/>
The alias of `shape` is `sh`.
## Skewing some shapes
You can influence some of the shapes (`triangle` and `square`) in more detail.
The default skew is 0.5, it does different things for these two.
For `triangle`, it skews the top of the triangle to the left or right,
where 0 makes it look like `saw` and 1 makes it look like `ramp`.
For `square`, the skew influences the pulse width (see reference for `pulse` and `.pw`):
<MiniRepl
client:idle
tune={`
s("saw")
.lpf(800)
.lfo({r: 2, sh: "<triangle square>/5",
skew: "<0 0.25 0.5 0.75 1>"}) `}
/>
The alias of `skew` is `sk`.{' '}
## Curve
You can change the curves of your lfo and can make it more intense.
The default value is 1. Writing numbers greater than one can make it more intense,
numbers between 0 and 1 will make it less intense.
This will raise the lfo to the power of curve, so larger numbers (such as 10) can have very unexpected results.
<MiniRepl
client:idle
tune={`
s("saw")
.lpf(800)
.lfo({r: 2, sh: "<triangle>", curve: "<1.3 1 0.75 1>"}) `}
/>
## Referencing your lfos with id
All the lfos are numbered from the first starting with 0, the second having an `id` of 1 and so on.
You can refer to this in a later call if you want to modify a specific `lfo`.
The `id` is outside of the config json (which is different from the other parameters of `lfo()`
Try out how the sound changes when you replace the 0 with a 1.
<MiniRepl
client:idle
tune={`
s("saw").lfo().lpf(800)
.lfo({s: "<4 8 0.5>"})
.sometimes(x => x.lfo({dr: "4"},0))`}
/>
You can also name your lfos and refer to them by name, using the `id` parameter
<MiniRepl
client:idle
tune={`
s("saw").lfo({}, "lfo_freq_saw").lpf(800)
.lfo({s: "<4 8 0.5>"}, "lfo_lpf")
.sometimes(x => x.lfo({dr: "4"},"<lfo_lpf lfo_freq_saw>"))`}
/>
## FX index
If you are using `FX()` to reorder your effects, you dont need to write your lfos inside the `FX`,
but instead can refer to them by their FX index (starting with 0)
<MiniRepl
client:idle
tune={`
s("saw").FX(
distort(3),
gain(0.3), // this has the fx index 1
lpf(400)
).lfo({s: 16, dr:2, c:"gain", fxi: 1})`}
/>
## Modulating other LFOs with Sub-control
LFOs can modulate other lfos and will modulate their frequency (given by `r` or `s`):
<MiniRepl
client:idle
tune={`
s("saw").lpf(400).gain(0.8)
.lfo({s: 16, dr:2, c:"gain"})
.lfo({s: 0.3, dc:-1, dr: 0.8})`}
/>
To modulate other parameters of the first lfo (like `skew`, `depth` and so on), we can specify this with `subControl`or
its alias `sc`
<MiniRepl
client:idle
tune={`
s("saw").lpf(400).gain(0.8)
.lfo({s: 4, dr:2, c:"gain"})
.lfo({s: 0.3, sc: "skew"})`}
/>
@@ -1,11 +1,11 @@
--- ---
title: Pattern Alignment title: Pattern Aligment
layout: ../../layouts/MainLayout.astro layout: ../../layouts/MainLayout.astro
--- ---
import { MiniRepl } from '../../docs/MiniRepl'; import { MiniRepl } from '../../docs/MiniRepl';
# Pattern Alignment & Combination # Pattern Aligment & 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.
-117
View File
@@ -1,117 +0,0 @@
---
title: Movement with Strudel
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '@src/docs/MiniRepl';
import Box from '@components/Box.astro';
import QA from '@components/QA';
# Controlling motors with Strudel
Strudel is mainly made for making music, but it's possible to pattern other things with it, including motors.
We're going to use a microcontroller for this, called an "[Inventor 2040W](https://shop.pimoroni.com/products/inventor-2040-w)", which is
a [Pico W](https://shop.pimoroni.com/products/inventor-2040-w?variant=40053063155795) with extra ports added including some for controlling motors.
![image](https://shop.pimoroni.com/cdn/shop/products/Inventor2040_1of3_1500x1500_crop_center.jpg?v=1656927927)
## Technical details
Feel free to gloss over these!
- The Inventor 2040W connects to the internet wirelessly, and it can power from a battery or USB. Hopefully the batteries last!
- It's running [some code](https://github.com/patternclub/alpacalab/blob/main/course/main.py) that listens for messages using an "Internet of Things" network protocol (called MQTT). When it receives a message, it moves a motor.
- It connects to a small server (running software called 'mosquitto') on Alex's laptop.
- Strudel can send these messages instead of triggering sounds - that's how we use it to pattern movement.
## First movement
Let's get a motor running!
1. Note the letter drawn on a label on the back of the microcontroller.
2. Plug a battery into your microcontroller.
3. Plug a motor into 'servo' (not motor) plug numbered 1, with the yellow (lightest) cable closest to the '1', and the brown (darkest) cable outward
4. Run the below to set up some values, changing the `x` in 'robot('x')` to the letter on your microcontroller.
<MiniRepl
client:visible
tune={`
$: move("-60 80").motor("0").robot('x');
`}
/>
<Box>
If you refresh the page, you'll need to change the letter to match your robot again.
If your motor starts moving unexpectedly, someone else might have put your letter in by mistake!
Note that in the above, we start counting motors from '0', so motor 1 on the board is motor 0 in the code.
</Box>
## Patterning movement
Many strudel features for playing with sound patterns will work when
playing with motor patterns. Try playing with the mininotation in the
`move` command:
<MiniRepl
client:visible
tune={`
$: move("-10 0 10 [20 30]*2").motor("0").slow(2).robot('x')
`}
/>
The move instructions are in the range from -90 to 90.
<box>
If your motors stop working at some point, and your code looks right, try pressing the 'reset' button on the
microcontroller.
</box>
It's possible to make smooth movements based on different 'waveforms', for example a smooth sinewave:
<MiniRepl
client:visible
tune={`
$: move(sine.range(-30, 30).segment(16)).motor("0").slow(2).robot('x');
`}
/>
The movement is still quite jerky, because the 'segment' command is
only taking 16 positions from the sinewave. Try increasing it to 32 or 64. It's best not too much higher than that, as the microcontroller
might get overwhelmed with a backlog of instructions!
<box>
Try replacing `sine` with other waveforms: `saw` (sawtooth wave), `tri` (triangular wave) are good, and there is also
`rand` (random wave) and `perlin` (a kind of smoothed-out randomness).
</box>
## Patterning more than one motor
You can pattern the `motor` command separately from the `move` one:
<MiniRepl
client:visible
tune={`
$: move("-10 0 10 [20 30]*2").motor("0 1").slow(2).robot('x');
`}
/>
Alternatively, you can pattern two motors in separate patterns. The below sends the same pattern for the first two motors, but with the second one running slower:
<MiniRepl
client:visible
tune={`
$: move("-10 0 10 [20 30]\*2").motor("0").slow(2).robot('x');
$: move("-10 0 10 [20 30]\*2").motor("1").slow(3).robot('x');
`}
/>
+11 -11
View File
@@ -10,50 +10,50 @@
opacity: 0.5; opacity: 0.5;
} }
.code-container .cm-scroller { #code .cm-scroller {
padding-top: 10px !important; padding-top: 10px !important;
height: 100%; height: 100%;
font-family: inherit; font-family: inherit;
} }
.code-container .cm-content { #code .cm-content {
padding-bottom: 50vh; padding-bottom: 50vh;
} }
.code-container .cm-line > * { #code .cm-line > * {
background: var(--lineBackground); background: var(--lineBackground);
} }
.code-container .cm-editor { #code .cm-editor {
background-color: transparent !important; background-color: transparent !important;
height: 100%; height: 100%;
z-index: 11; z-index: 11;
} }
.code-container .cm-theme { #code .cm-theme {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.code-container .cm-theme-light { #code .cm-theme-light {
width: 100%; width: 100%;
} }
.code-container .cm-cursorLayer { #code .cm-cursorLayer {
animation-name: inherit !important; animation-name: inherit !important;
} }
.code-container .cm-cursor { #code .cm-cursor {
border-left: 2px solid currentcolor !important; border-left: 2px solid currentcolor !important;
} }
.code-container .cm-foldGutter { #code .cm-foldGutter {
display: none !important; display: none !important;
} }
.code-container .cm-focused { #code .cm-focused {
outline: none; outline: none;
} }
.code-container .cm-matchingBracket { #code .cm-matchingBracket {
text-decoration: underline 0.18rem; text-decoration: underline 0.18rem;
text-underline-offset: 0.22rem; text-underline-offset: 0.22rem;
} }
+2 -1
View File
@@ -8,7 +8,8 @@ export function Code(Props) {
return ( return (
<section <section
className={'code-container text-gray-100 cursor-text pb-0 overflow-auto grow z-10'} className={'text-gray-100 cursor-text pb-0 overflow-auto grow z-10'}
id="code"
ref={(el) => { ref={(el) => {
containerRef.current = el; containerRef.current = el;
if (!editorRef.current) { if (!editorRef.current) {
@@ -8,3 +8,31 @@ export function ActionButton({ children, label, labelIsHidden, className, ...but
</button> </button>
); );
} }
export function SpecialActionButton(props) {
const { className, ...buttonProps } = props;
return (
<ActionButton {...buttonProps} className={cx('bg-background p-2 max-w-[300px] hover:opacity-50', className)} />
);
}
export function ActionInput({ label, className, ...props }) {
return (
<label className={cx('inline-flex items-center cursor-pointer ', className)}>
<input {...props} className="sr-only peer" />
<span className="inline-flex items-center peer-hover:opacity-50">{label}</span>
</label>
);
}
export function SpecialActionInput({ className, ...props }) {
return (
<ActionInput
{...props}
className={className}
label={<span className="bg-background p-2 max-w-[300px]">{props.label}</span>}
/>
);
}
@@ -1,50 +1,42 @@
import { errorLogger } from '@strudel/core'; import { errorLogger } from '@strudel/core';
import { storePrebakeScript } from '../../../settings.mjs'; import { useSettings, storePrebakeScript } from '../../../settings.mjs';
import { confirmDialog } from '@src/repl/util.mjs'; import { SpecialActionInput } from '../button/action-button';
import { confirmDialog, SETTING_CHANGE_RELOAD_MSG } from '@src/repl/util.mjs';
async function importScript(script, updateEditor) { async function importScript(script) {
const reader = new FileReader(); const reader = new FileReader();
reader.readAsText(script); reader.readAsText(script);
reader.onload = () => { reader.onload = () => {
const text = reader.result; const text = reader.result;
storePrebakeScript(text); storePrebakeScript(text);
updateEditor && updateEditor(text);
}; };
reader.onerror = () => { reader.onerror = () => {
errorLogger(new Error('failed to import prebake script'), 'importScript'); errorLogger(new Error('failed to import prebake script'), 'importScript');
}; };
} }
export function ImportPrebakeScriptButton() {
const settings = useSettings();
export async function exportScript(script) {
const blob = new Blob([script], { type: 'application/javascript' });
const downloadLink = document.createElement('a');
downloadLink.href = window.URL.createObjectURL(blob);
const date = new Date().toISOString().split('T')[0];
downloadLink.download = `prebake_${date}.strudel`;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
}
export function ImportPrebakeScriptButton({ updateEditor }) {
const handleChange = async (e) => {
const file = e.target.files[0];
const confirmed = await confirmDialog('Warning: This will overwrite the current prebake.\nContinue?');
if (!confirmed) {
return;
}
try {
await importScript(file, updateEditor);
} catch (e) {
errorLogger(e);
}
};
return ( return (
<label className="space-x-1 inline-flex items-center cursor-pointer"> <SpecialActionInput
<input type="file" accept=".strudel,.js" className="sr-only peer" onChange={handleChange} /> type="file"
<span className="inline-flex items-center peer-hover:opacity-50 text-xs max-w-[300px]">import</span> label="import prebake script"
</label> accept=".strudel"
onChange={async (e) => {
const file = e.target.files[0];
const confirmed = await confirmDialog(SETTING_CHANGE_RELOAD_MSG);
if (!confirmed) {
return;
}
try {
await importScript(file);
window.location.reload();
} catch (e) {
errorLogger(e);
}
}}
/>
); );
} }
@@ -44,25 +44,6 @@ export function LogoButton({ context, isEmbedded }) {
export function MainPanel({ context, isEmbedded = false, className }) { export function MainPanel({ context, isEmbedded = false, className }) {
const { isZen, isButtonRowHidden, fontFamily } = useSettings(); const { isZen, isButtonRowHidden, fontFamily } = useSettings();
let loc = window.location;
let ver = 'unofficial';
let hot = false;
let b = loc.hostname.match(/^(.+)\.(strudel)/);
if (/(strudel.cc$)/.test(loc.hostname)) {
// if there's no text before 'strudel', it's warm, otherwise use the text before strudel
ver = b ? b[1] : 'warm';
} else {
// match both versions of localhost
if (/(localhost)|(127.0.0.1)/.test(loc.hostname)) ver = 'dev';
}
let pr = ver.match(/pr-([0-9]+)/);
if (pr) {
pr = pr[1];
ver = `hot: ${pr}`;
hot = true;
pr = `https://codeberg.org/uzu/strudel/pulls/${pr}`;
}
return ( return (
<nav <nav
id="header" id="header"
@@ -91,13 +72,6 @@ export function MainPanel({ context, isEmbedded = false, className }) {
<div className="space-x-2 flex items-baseline"> <div className="space-x-2 flex items-baseline">
<span className="hidden sm:block">strudel</span> <span className="hidden sm:block">strudel</span>
<span className="text-sm font-medium hidden sm:block">REPL</span> <span className="text-sm font-medium hidden sm:block">REPL</span>
{!hot ? (
<span className="text-sm font-medium hidden sm:block">({ver})</span>
) : (
<a className="hover:opacity-50" href={pr} target="_blank">
<span className="text-sm font-medium hidden sm:block">({ver})</span>
</a>
)}
</div> </div>
)} )}
</h1> </h1>
@@ -1,16 +1,18 @@
import { defaultSettings, settingsMap, useSettings, storePrebakeScript, setSettingsTab } from '../../../settings.mjs'; import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
import { themes } from '@strudel/codemirror'; import { themes } from '@strudel/codemirror';
import { PrebakeCodeMirror } from '../../../repl/prebakeCodeMirror.mjs';
import { confirmAndReloadPage, isUdels } from '../../util.mjs'; import { confirmAndReloadPage, isUdels } from '../../util.mjs';
import { ButtonGroup } from './Forms.jsx'; import { ButtonGroup } from './Forms.jsx';
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx'; import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
import { confirmDialog } from '../../util.mjs'; import { confirmDialog } from '../../util.mjs';
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio'; import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
import { ActionButton } from '../button/action-button.jsx'; import { SpecialActionButton } from '../button/action-button.jsx';
import { exportScript, ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx'; import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
import { useEffect, useRef } from 'react';
import cx from '@src/cx.mjs'; function cx(...classes) {
// : Array<string | undefined>
return classes.filter(Boolean).join(' ');
}
const inputClass = const inputClass =
'bg-background text-xs h-8 max-h-8 border border-box rounded-0 text-foreground border-muted placeholder-muted focus:outline-none focus:ring-0 focus:border-foreground'; 'bg-background text-xs h-8 max-h-8 border border-box rounded-0 text-foreground border-muted placeholder-muted focus:outline-none focus:ring-0 focus:border-foreground';
@@ -121,7 +123,7 @@ const fontFamilyOptions = {
galactico: 'galactico', galactico: 'galactico',
}; };
function MainSettingsContent({ started }) { export function SettingsTab({ started }) {
const { const {
theme, theme,
keybindings, keybindings,
@@ -147,12 +149,13 @@ function MainSettingsContent({ started }) {
isTabIndentationEnabled, isTabIndentationEnabled,
isMultiCursorEnabled, isMultiCursorEnabled,
patternAutoStart, patternAutoStart,
includePrebakeScriptInShare,
isBlockBasedEvalEnabled, isBlockBasedEvalEnabled,
} = useSettings(); } = useSettings();
const shouldAlwaysSync = isUdels(); const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
return ( return (
<div className="p-4 text-foreground space-y-4 w-full overflow-auto" style={{ fontFamily }}> <div className="p-4 text-foreground space-y-4 w-full" style={{ fontFamily }}>
{canChangeAudioDevice && ( {canChangeAudioDevice && (
<FormItem label="Audio Output Device"> <FormItem label="Audio Output Device">
<AudioDeviceSelector <AudioDeviceSelector
@@ -230,6 +233,14 @@ function MainSettingsContent({ started }) {
/> />
</FormItem> </FormItem>
</div> </div>
<FormItem label="Prebake">
<ImportPrebakeScriptButton />
<Checkbox
label="Include prebake script in share"
onChange={(cbEvent) => settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)}
value={includePrebakeScriptInShare}
/>
</FormItem>
<FormItem label="Keybindings"> <FormItem label="Keybindings">
<ButtonGroup <ButtonGroup
@@ -335,7 +346,7 @@ function MainSettingsContent({ started }) {
</FormItem> </FormItem>
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem> <FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
<FormItem label="Reset Settings"> <FormItem label="Reset Settings">
<ActionButton <SpecialActionButton
onClick={() => { onClick={() => {
confirmDialog('Sure?').then((r) => { confirmDialog('Sure?').then((r) => {
if (r) { if (r) {
@@ -344,70 +355,10 @@ function MainSettingsContent({ started }) {
} }
}); });
}} }}
className="bg-background p-2 max-w-[300px] hover:opacity-50"
> >
restore default settings restore default settings
</ActionButton> </SpecialActionButton>
</FormItem> </FormItem>
</div> </div>
); );
} }
function PrebakeSettingsContent() {
const { fontFamily, includePrebakeScriptInShare, prebakeScript } = useSettings();
const editorRef = useRef();
useEffect(() => {
return () => {
editorRef.current?.cleanup();
};
});
return (
<div className="flex flex-col h-full text-foreground w-full overflow-auto" style={{ fontFamily }}>
<div className="flex flex-col grow overflow-hidden h-full bg-background">
<section
className="pb-0 overflow-auto grow z-10 code-container"
ref={(el) => {
if (editorRef.current) {
return;
}
editorRef.current = new PrebakeCodeMirror(prebakeScript, (code) => storePrebakeScript(code), el);
}}
></section>
</div>
<div className="flex justify-between items-center border-t border-muted px-4 whitespace-nowrap">
<Checkbox
label="share with patterns"
className="whitespace-nowrap max-w-[200px]"
onChange={(cbEvent) => settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)}
value={includePrebakeScriptInShare}
/>
<div className="py-2 flex flex-row items-center space-x-3 ">
<ImportPrebakeScriptButton updateEditor={(code) => editorRef?.current.setCode(code)} />
<ActionButton onClick={() => exportScript(prebakeScript)}>export</ActionButton>
<ActionButton onClick={() => editorRef.current?.savePrebake()}>save</ActionButton>
</div>
</div>
</div>
);
}
export function SettingsTab({ started }) {
const { settingsTab } = useSettings();
return (
<div className="w-full h-full text-foreground flex flex-col overflow-hidden">
<div className="px-2 shrink-0 h-8 space-x-4 flex max-w-full overflow-x-auto border-b border-muted">
<ButtonGroup
wrap
value={settingsTab}
onChange={(value) => setSettingsTab(value)}
items={{
settings: 'settings',
prebake: 'prebake',
}}
></ButtonGroup>
</div>
{settingsTab === 'settings' && <MainSettingsContent started={started} />}
{settingsTab === 'prebake' && <PrebakeSettingsContent />}
</div>
);
}
@@ -36,9 +36,8 @@ export function WelcomeTab({ context }) {
<a href="https://tidalcycles.org/" target="_blank"> <a href="https://tidalcycles.org/" target="_blank">
tidalcycles tidalcycles
</a> </a>
, which is a popular live coding language for music, written in Haskell. Strudel is free/open source software, , which is a popular live coding language for music, written in Haskell. Strudel is free/open source software:
with copyright owned by its [contributors](https://codeberg.org/uzu/strudel/activity/contributors). You can you can redistribute and/or modify it under the terms of the{' '}
redistribute and/or modify it under the terms of the{' '}
<a href="https://codeberg.org/uzu/strudel/src/branch/main/LICENSE" target="_blank"> <a href="https://codeberg.org/uzu/strudel/src/branch/main/LICENSE" target="_blank">
GNU Affero General Public License GNU Affero General Public License
</a> </a>
+13 -12
View File
@@ -1,4 +1,4 @@
import { registerSampleSource } from '@strudel/webaudio'; import { extractMidiNoteFromString, registerSampleSource } from '@strudel/webaudio';
import { isAudioFile } from './files.mjs'; import { isAudioFile } from './files.mjs';
import { logger } from '@strudel/core'; import { logger } from '@strudel/core';
@@ -47,7 +47,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
Promise.all( Promise.all(
[...soundFiles] [...soundFiles]
.sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' })) .sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }))
.map((soundFile, i) => { .map((soundFile) => {
const title = soundFile.title; const title = soundFile.title;
if (!isAudioFile(title)) { if (!isAudioFile(title)) {
return; return;
@@ -58,25 +58,26 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user'; splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
const blob = soundFile.blob; const blob = soundFile.blob;
return blobToDataUrl(blob).then((soundPath) => { return blobToDataUrl(blob).then((path) => {
const titlePathMap = sounds.get(parentDirectory) ?? new Map(); const sampleInfoMap = sounds.get(parentDirectory) ?? new Map();
const midi = extractMidiNoteFromString(title);
/** @type {import('@strudel/webaudio').SampleMetaData} */
const samplemetadata = { url: path, midi };
sampleInfoMap.set(title, samplemetadata);
titlePathMap.set(title, soundPath); sounds.set(parentDirectory, sampleInfoMap);
sounds.set(parentDirectory, titlePathMap);
return; return;
}); });
}), }),
) )
.then(() => { .then(() => {
sounds.forEach((titlePathMap, key) => { sounds.forEach((sampleInfoMap, key) => {
const value = Array.from(titlePathMap.keys()) const bank = Array.from(sampleInfoMap.keys())
.sort((a, b) => { .sort((a, b) => {
return a.localeCompare(b); return a.localeCompare(b);
}) })
.map((title) => titlePathMap.get(title)); .map((title) => sampleInfoMap.get(title));
registerSampleSource(key, bank, { prebake: false });
registerSampleSource(key, value, { prebake: false });
}); });
logger('imported sounds registered!', 'success'); logger('imported sounds registered!', 'success');
-115
View File
@@ -1,115 +0,0 @@
import { toggleLineComment } from '@codemirror/commands';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { Compartment, EditorState, Prec } from '@codemirror/state';
import { drawSelection, EditorView, keymap } from '@codemirror/view';
import { logger } from '@strudel/core';
import { basicSetup, flash, initTheme, extensions, parseBooleans, codemirrorSettings } from '@strudel/codemirror';
import { evaluate } from '@strudel/transpiler';
export class PrebakeCodeMirror {
constructor(initialCode, storePrebake, container) {
const settings = codemirrorSettings.get();
this.storePrebake = storePrebake;
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
const initialSettings = Object.keys(compartments).map((key) =>
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
);
initTheme(settings.theme);
let state = EditorState.create({
doc: initialCode,
extensions: [
...initialSettings,
basicSetup,
javascript(),
javascriptLanguage.data.of({
closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] },
bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] },
}),
syntaxHighlighting(defaultHighlightStyle),
EditorView.updateListener.of((v) => {
if (v.docChanged) {
this.code = v.state.doc.toString();
}
}),
drawSelection({ cursorBlinkRate: 0 }),
Prec.highest(
keymap.of([
{
mac: 'Meta-Enter',
run: () => {
this.savePrebake();
},
},
{
key: 'Ctrl-Enter',
run: () => {
this.savePrebake();
},
},
{
key: 'Alt-Enter',
run: () => {
this.savePrebake();
},
},
]),
),
],
});
this.code = initialCode;
this.view = new EditorView({
state,
parent: container,
});
const handleSaveEvent = async (e) => {
if (e.detail.view !== this.view) {
return; // ignore events from other editors
}
await this.savePrebake();
e?.cancelable && e.preventDefault?.();
};
const handleToggleComment = (e) => {
if (e.detail.view !== this.view) {
return; // ignore events from other editors
}
this.toggleComment();
e?.cancelable && e.preventDefault?.();
};
document.addEventListener('repl-evaluate', handleSaveEvent);
document.addEventListener('repl-toggle-comment', handleToggleComment);
this.cleanup = () => {
document.removeEventListener('prebake-evaluate', handleSaveEvent);
document.removeEventListener('prebake-toggle-comment', handleToggleComment);
};
}
async savePrebake() {
flash(this.view);
this.storePrebake(this.code);
evaluate(this.code, { addReturn: false }); // run prebake
logger('[prebake] prebake saved');
}
toggleComment() {
try {
// Honor selections; toggleLineComment handles both selections and
// single line
toggleLineComment(this.view);
} catch (err) {
console.error('Error handling repl-toggle-comment event', err);
}
}
setCode(code) {
const changes = {
from: 0,
to: this.view.state.doc.length,
insert: code,
};
this.view.dispatch({ changes });
}
}
+7 -7
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core'; import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core';
import { getDrawContext } from '@strudel/draw'; import { getDrawContext } from '@strudel/draw';
import { transpiler, evaluate } from '@strudel/transpiler'; import { evaluate, transpiler } from '@strudel/transpiler';
import { import {
getAudioContextCurrentTime, getAudioContextCurrentTime,
renderPatternAudio, renderPatternAudio,
@@ -87,12 +87,12 @@ export function useReplContext() {
pattern: silence, pattern: silence,
drawTime, drawTime,
drawContext, drawContext,
prebake: async () => { prebake: async () =>
await Promise.all([modulesLoading, presets]); Promise.all([modulesLoading, presets]).then(() => {
if (prebakeScript) { if (prebakeScript?.length) {
return evaluate(prebakeScript, { addReturn: false }); return evaluate(prebakeScript ?? '');
} }
}, }),
onUpdateState: (state) => { onUpdateState: (state) => {
setReplState({ ...state }); setReplState({ ...state });
}, },
-2
View File
@@ -70,7 +70,6 @@ export function loadModules() {
let modules = [ let modules = [
import('@strudel/core'), import('@strudel/core'),
import('@strudel/draw'), import('@strudel/draw'),
import('@strudel/edo'),
import('@strudel/tonal'), import('@strudel/tonal'),
import('@strudel/mini'), import('@strudel/mini'),
import('@strudel/xen'), import('@strudel/xen'),
@@ -109,7 +108,6 @@ export function confirmDialog(msg) {
}); });
} }
export const SETTING_CHANGE_RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?'; export const SETTING_CHANGE_RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?';
export function confirmAndReloadPage(onSuccess) { export function confirmAndReloadPage(onSuccess) {
confirmDialog(SETTING_CHANGE_RELOAD_MSG).then((r) => { confirmDialog(SETTING_CHANGE_RELOAD_MSG).then((r) => {
if (r == true) { if (r == true) {
+1 -17
View File
@@ -18,20 +18,6 @@ export const soundFilterType = {
ALL: 'all', ALL: 'all',
}; };
const initialPrebakeScript = `// Prebake script
//
// This is code that is loaded before your pattern is run.
// You can use it to define custom functions to use in any pattern.
//
// This is an initial example script. You can edit it to add
// your own funtions.
//
// To use a script shared by some other user you can use
// the import-button or paste the script in this editor.
const ratchet = register('ratchet', (pat) => pat.sometimes(ply(2)))
`;
export const defaultSettings = { export const defaultSettings = {
activeFooter: 'intro', activeFooter: 'intro',
keybindings: 'codemirror', keybindings: 'codemirror',
@@ -61,14 +47,13 @@ export const defaultSettings = {
isPanelPinned: false, isPanelPinned: false,
isPanelOpen: true, isPanelOpen: true,
userPatterns: '{}', userPatterns: '{}',
prebakeScript: initialPrebakeScript, prebakeScript: '',
audioEngineTarget: audioEngineTargets.webaudio, audioEngineTarget: audioEngineTargets.webaudio,
isButtonRowHidden: false, isButtonRowHidden: false,
isCSSAnimationDisabled: false, isCSSAnimationDisabled: false,
maxPolyphony: 128, maxPolyphony: 128,
multiChannelOrbits: false, multiChannelOrbits: false,
includePrebakeScriptInShare: true, includePrebakeScriptInShare: true,
settingsTab: 'settings',
}; };
let search = null; let search = null;
@@ -130,7 +115,6 @@ export function useSettings() {
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab); export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool); export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool);
export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool); export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool);
export const setSettingsTab = (tab) => settingsMap.setKey('settingsTab', tab);
export const storePrebakeScript = (script) => settingsMap.setKey('prebakeScript', script); export const storePrebakeScript = (script) => settingsMap.setKey('prebakeScript', script);