diff --git a/.forgejo/workflows/deploy-warm-beta.yml b/.forgejo/workflows/deploy-warm-beta.yml
new file mode 100644
index 000000000..2de6670c8
--- /dev/null
+++ b/.forgejo/workflows/deploy-warm-beta.yml
@@ -0,0 +1,37 @@
+name: Build and Deploy to beta (warm.strudel.cc)
+
+on: [workflow_dispatch]
+
+# Allow one concurrent deployment
+concurrency:
+ group: "pages"
+ cancel-in-progress: true
+
+jobs:
+ build:
+ runs-on: docker
+ env:
+ SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ with:
+ version: 9.12.2
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ # cache: "pnpm"
+ - name: Install Dependencies
+ run: pnpm install
+
+ - name: Build
+ run: pnpm build
+
+ - name: Deploy
+ run: |
+ eval $(ssh-agent -s)
+ echo "$SSH_PRIVATE_KEY" | ssh-add -
+ apt update && apt install -y rsync
+ mkdir ~/.ssh
+ ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
+ rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/warm.strudel.cc
\ No newline at end of file
diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml
index 561fee658..0643f9be1 100644
--- a/.forgejo/workflows/deploy.yml
+++ b/.forgejo/workflows/deploy.yml
@@ -1,4 +1,4 @@
-name: Build and Deploy
+name: Build and Deploy to live (strudel.cc)
on: [workflow_dispatch]
@@ -34,4 +34,4 @@ jobs:
apt update && apt install -y rsync
mkdir ~/.ssh
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
- rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy
\ No newline at end of file
+ rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc
\ No newline at end of file
diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs
index 4dc23996f..69fce4b50 100644
--- a/packages/codemirror/codemirror.mjs
+++ b/packages/codemirror/codemirror.mjs
@@ -1,30 +1,31 @@
import { closeBrackets } from '@codemirror/autocomplete';
-export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
-// import { search, highlightSelectionMatches } from '@codemirror/search';
-import { indentWithTab } from '@codemirror/commands';
+import { indentWithTab, toggleLineComment } from '@codemirror/commands';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
-import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
+import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { Compartment, EditorState, Prec } from '@codemirror/state';
import {
+ drawSelection,
EditorView,
- highlightActiveLineGutter,
highlightActiveLine,
+ highlightActiveLineGutter,
keymap,
lineNumbers,
- drawSelection,
} from '@codemirror/view';
-import { repl, registerControl } from '@strudel/core';
-import { Drawer, cleanupDraw } from '@strudel/draw';
+import { persistentAtom } from '@nanostores/persistent';
+import { logger, registerControl, repl } from '@strudel/core';
+import { cleanupDraw, Drawer } from '@strudel/draw';
+
import { isAutoCompletionEnabled } from './autocomplete.mjs';
-import { isTooltipEnabled } from './tooltip.mjs';
+import { basicSetup } from './basicSetup.mjs';
import { flash, isFlashEnabled } from './flash.mjs';
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
import { keybindings } from './keybindings.mjs';
-import { initTheme, activateTheme, theme } from './themes.mjs';
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
-import { widgetPlugin, updateWidgets } from './widget.mjs';
-import { persistentAtom } from '@nanostores/persistent';
-import { basicSetup } from './basicSetup.mjs';
+import { activateTheme, initTheme, theme } from './themes.mjs';
+import { isTooltipEnabled } from './tooltip.mjs';
+import { updateWidgets, widgetPlugin } from './widget.mjs';
+
+export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
@@ -94,8 +95,8 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
}),
sliderPlugin,
widgetPlugin,
- // indentOnInput(), // works without. already brought with javascript extension?
- // bracketMatching(), // does not do anything
+ // indentOnInput(), // works without. already brought with javascript
+ // extension? bracketMatching(), // does not do anything
syntaxHighlighting(defaultHighlightStyle),
EditorView.updateListener.of((v) => onChange(v)),
drawSelection({ cursorBlinkRate: 0 }),
@@ -119,13 +120,13 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
run: () => onStop?.(),
},
/* {
- key: 'Ctrl-Shift-.',
- run: () => (onPanic ? onPanic() : onStop?.()),
- },
- {
- key: 'Ctrl-Shift-Enter',
- run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
- }, */
+ key: 'Ctrl-Shift-.',
+ run: () => (onPanic ? onPanic() : onStop?.()),
+ },
+ {
+ key: 'Ctrl-Shift-Enter',
+ run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
+ }, */
]),
),
],
@@ -206,7 +207,8 @@ export class StrudelMirror {
updateWidgets(this.editor, widgets);
updateMiniLocations(this.editor, this.miniLocations);
replOptions?.afterEval?.(options);
- // if no painters are set (.onPaint was not called), then we only need the present moment (for highlighting)
+ // if no painters are set (.onPaint was not called), then we only need
+ // the present moment (for highlighting)
const drawTime = options.pattern.getPainters().length ? this.drawTime : [0, 0];
this.drawer.setDrawTime(drawTime);
// invalidate drawer after we've set the appropriate drawTime
@@ -245,6 +247,33 @@ export class StrudelMirror {
}
};
document.addEventListener('start-repl', this.onStartRepl);
+
+ // Handle global evaluation requests (e.g., from Vim :w)
+ this.onEvaluateRequest = (e) => {
+ try {
+ // Evaluate current editor on repl-evaluate
+ logger('[repl] evaluate via event');
+ this.evaluate();
+ e?.cancelable && e.preventDefault?.();
+ } catch (err) {
+ console.error('Error handling repl-evaluate event', err);
+ }
+ };
+ document.addEventListener('repl-evaluate', this.onEvaluateRequest);
+ document.addEventListener('repl-stop', this.onStopRequest);
+
+ // Toggle comments requested from Vim (gc)
+ this.onToggleComment = (e) => {
+ try {
+ // Honor selections; toggleLineComment handles both selections and
+ // single line
+ toggleLineComment(this.editor);
+ e?.cancelable && e.preventDefault?.();
+ } catch (err) {
+ console.error('Error handling repl-toggle-comment event', err);
+ }
+ };
+ document.addEventListener('repl-toggle-comment', this.onToggleComment);
}
draw(haps, time, painters) {
painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
@@ -271,6 +300,16 @@ export class StrudelMirror {
async stop() {
this.repl.scheduler.stop();
}
+
+ // Listen for global stop requests (e.g., from Vim :q)
+ onStopRequest = (e) => {
+ try {
+ this.stop();
+ e?.cancelable && e.preventDefault?.();
+ } catch (err) {
+ console.error('Error handling repl-stop event', err);
+ }
+ };
async toggle() {
if (this.repl.scheduler.started) {
this.repl.stop();
@@ -346,11 +385,18 @@ export class StrudelMirror {
}
}
setCode(code) {
- const changes = { from: 0, to: this.editor.state.doc.length, insert: code };
+ const changes = {
+ from: 0,
+ to: this.editor.state.doc.length,
+ insert: code,
+ };
this.editor.dispatch({ changes });
}
clear() {
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
+ this.onEvaluateRequest && document.removeEventListener('repl-evaluate', this.onEvaluateRequest);
+ this.onStopRequest && document.removeEventListener('repl-stop', this.onStopRequest);
+ this.onToggleComment && document.removeEventListener('repl-toggle-comment', this.onToggleComment);
}
getCursorLocation() {
return this.editor.state.selection.main.head;
diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs
index ca5f34f4c..81dc87a52 100644
--- a/packages/codemirror/keybindings.mjs
+++ b/packages/codemirror/keybindings.mjs
@@ -1,11 +1,12 @@
+import { defaultKeymap } from '@codemirror/commands';
import { Prec } from '@codemirror/state';
import { keymap, ViewPlugin } from '@codemirror/view';
// import { searchKeymap } from '@codemirror/search';
import { emacs } from '@replit/codemirror-emacs';
-import { vim } from '@replit/codemirror-vim';
+import { vim, Vim } from '@replit/codemirror-vim';
// import { vim } from './vim_test.mjs';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
-import { defaultKeymap } from '@codemirror/commands';
+import { logger } from '@strudel/core';
const vscodePlugin = ViewPlugin.fromClass(
class {
@@ -19,6 +20,110 @@ const vscodePlugin = ViewPlugin.fromClass(
);
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
+// Map Vim :w to trigger the same action as evaluation. We dispatch a custom
+// event 'repl-evaluate' that the editor listens for, and also simulate
+// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it
+// appears in the Console panel.
+try {
+ if (Vim && typeof Vim.defineEx === 'function') {
+ // Map gc to toggle line comments by dispatching a custom event that our
+ // CodeMirror integration listens to. This avoids depending on Vim's
+ // internal actions and works with current selections/visual mode.
+ try {
+ Vim.defineAction('strudelToggleComment', (cm) => {
+ const view = cm?.view || cm;
+ try {
+ const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
+ document.dispatchEvent(ev);
+ } catch (e) {
+ console.error('strudelToggleComment dispatch failed', e);
+ }
+ });
+ Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'normal' });
+ Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'visual' });
+ } catch (e) {
+ console.error('Vim gc mapping failed', e);
+ }
+
+ // :q to pause/stop
+ Vim.defineEx('quit', 'q', (cm) => {
+ try {
+ const view = cm?.view || cm;
+ // First try dispatching our custom stop event, then fallback to Alt+.
+ let handled = false;
+ try {
+ const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true });
+ handled = document.dispatchEvent(ev) === false;
+ } catch (e) {
+ console.error('Error dispatching repl-stop event', e);
+ }
+ if (!handled) {
+ const altDot = new KeyboardEvent('keydown', {
+ key: '.',
+ code: 'Period',
+ altKey: true,
+ bubbles: true,
+ cancelable: true,
+ });
+ view?.dom?.dispatchEvent?.(altDot);
+ }
+ } catch (e) {
+ console.error('Error dispatching :q stop event', e);
+ }
+ });
+
+ // :w to evaluate
+ Vim.defineEx('write', 'w', (cm) => {
+ const view = cm?.view || cm; // CM6 Vim passes either an object with view or the view itself
+ try {
+ view?.focus?.();
+ // Let the app know this came from Vim :w
+ try {
+ logger('[vim] :w — evaluating code');
+ } catch (e) {
+ console.error('Error logging Vim :w evaluation', e);
+ }
+ // Dispatch a dedicated evaluate event first
+ let handled = false;
+ try {
+ const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true });
+ handled = document.dispatchEvent(ev) === false; // false means preventDefault was called
+ } catch (e) {
+ console.error('Error dispatching repl-evaluate event', e);
+ }
+ if (handled) {
+ return;
+ }
+ // Try Ctrl+Enter first if not handled by custom event
+ const ctrlEnter = new KeyboardEvent('keydown', {
+ key: 'Enter',
+ code: 'Enter',
+ ctrlKey: true,
+ bubbles: true,
+ cancelable: true,
+ });
+ view?.dom?.dispatchEvent?.(ctrlEnter);
+ // If not handled (no handler called preventDefault), try Alt+Enter as
+ // fallback
+ if (!ctrlEnter.defaultPrevented) {
+ const altEnter = new KeyboardEvent('keydown', {
+ key: 'Enter',
+ code: 'Enter',
+ altKey: true,
+ bubbles: true,
+ cancelable: true,
+ });
+ view?.dom?.dispatchEvent?.(altEnter);
+ }
+ } catch (e) {
+ console.error('Error dispatching :w evaluation event', e);
+ }
+ });
+ }
+} catch (e) {
+ console.error('Vim ex command setup failed (defineEx missing or Vim unavailable)', e);
+}
+
const keymaps = {
vim,
emacs,
diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json
index b4e7d27a8..e54c4a428 100644
--- a/packages/codemirror/package.json
+++ b/packages/codemirror/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
@@ -49,8 +49,8 @@
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@tonaljs/tonal": "^4.10.0",
- "superdough": "workspace:*",
- "nanostores": "^0.11.3"
+ "nanostores": "^0.11.3",
+ "superdough": "workspace:*"
},
"devDependencies": {
"vite": "^6.0.11"
diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs
index 3475fc2f4..a3ca638aa 100644
--- a/packages/core/controls.mjs
+++ b/packages/core/controls.mjs
@@ -2320,6 +2320,24 @@ export const { miditouch } = registerControl('miditouch');
// TODO: what is this?
export const { polyTouch } = registerControl('polyTouch');
+/**
+ * The host to send open sound control messages to. Requires running the OSC bridge.
+ * @name oschost
+ * @param {string | Pattern} oschost e.g. 'localhost'
+ * @example
+ * note("c4").oschost('127.0.0.1').oscport(57120).osc();
+ */
+export const { oschost } = registerControl('oschost');
+
+/**
+ * The port to send open sound control messages to. Requires running the OSC bridge.
+ * @name oscport
+ * @param {number | Pattern} oscport e.g. 57120
+ * @example
+ * note("c4").oschost('127.0.0.1').oscport(57120).osc();
+ */
+export const { oscport } = registerControl('oscport');
+
export const getControlName = (alias) => {
if (controlAlias.has(alias)) {
return controlAlias.get(alias);
diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs
index 44ab07f11..98c21a116 100644
--- a/packages/core/euclid.mjs
+++ b/packages/core/euclid.mjs
@@ -35,18 +35,18 @@ const right = function (n, x) {
return result;
};
-const _bjork = function (n, x) {
+const _bjorklund = function (n, x) {
const [ons, offs] = n;
- return Math.min(ons, offs) <= 1 ? [n, x] : _bjork(...(ons > offs ? left(n, x) : right(n, x)));
+ return Math.min(ons, offs) <= 1 ? [n, x] : _bjorklund(...(ons > offs ? left(n, x) : right(n, x)));
};
-export const bjork = function (ons, steps) {
+export const bjorklund = function (ons, steps) {
const inverted = ons < 0;
const absOns = Math.abs(ons);
const offs = steps - absOns;
const ones = Array(absOns).fill([1]);
const zeros = Array(offs).fill([0]);
- const result = _bjork([absOns, offs], [ones, zeros]);
+ const result = _bjorklund([absOns, offs], [ones, zeros]);
const pattern = flatten(result[1][0]).concat(flatten(result[1][1]));
return inverted ? pattern.map((x) => 1 - x) : pattern;
};
@@ -128,7 +128,7 @@ export const bjork = function (ons, steps) {
*/
const _euclidRot = function (pulses, steps, rotation) {
- const b = bjork(pulses, steps);
+ const b = bjorklund(pulses, steps);
if (rotation) {
return rotate(b, -rotation);
}
@@ -139,7 +139,7 @@ export const euclid = register('euclid', function (pulses, steps, pat) {
return pat.struct(_euclidRot(pulses, steps, 0));
});
-export const e = register('e', function (euc, pat) {
+export const bjork = register('bjork', function (euc, pat) {
if (!Array.isArray(euc)) {
euc = [euc];
}
@@ -216,6 +216,6 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s
* .pan(sine.slow(8))
*/
export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) {
- const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc);
+ const morphed = _morph(bjorklund(pulses, steps), new Array(pulses).fill(1), perc);
return pat.struct(morphed).setSteps(steps);
});
diff --git a/packages/core/package.json b/packages/core/package.json
index 7cf20cea7..c33432f14 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/core",
- "version": "1.2.4",
+ "version": "1.2.5",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs
index f5b0c676e..e29769900 100644
--- a/packages/core/pattern.mjs
+++ b/packages/core/pattern.mjs
@@ -2574,8 +2574,8 @@ export const { chunkBack, chunkback } = register(
* @returns Pattern
* @example
* "<0 8> 1 2 3 4 5 6 7"
- * .fastChunk(4, x => x.color('red')).slow(2)
* .scale("C2:major").note()
+ * .fastChunk(4, x => x.color('red')).slow(2)
*/
export const { fastchunk, fastChunk } = register(
['fastchunk', 'fastChunk'],
diff --git a/packages/core/test/euclid.test.js b/packages/core/test/euclid.test.js
index a33ec9514..6169abd71 100644
--- a/packages/core/test/euclid.test.js
+++ b/packages/core/test/euclid.test.js
@@ -1,14 +1,14 @@
-import { bjork } from '../euclid.mjs';
+import { bjorklund } from '../euclid.mjs';
import { describe, expect, it } from 'vitest';
import { fastcat } from '../pattern.mjs';
-describe('bjork', () => {
- it('should apply bjorklund to ons and steps', () => {
- expect(bjork(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]);
- expect(bjork(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]);
- expect(bjork(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]);
- expect(bjork(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]);
- expect(bjork(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]);
+describe('bjorklund', () => {
+ it('should apply bjorklundlund to ons and steps', () => {
+ expect(bjorklund(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]);
+ expect(bjorklund(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]);
+ expect(bjorklund(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]);
+ expect(bjorklund(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]);
+ expect(bjorklund(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]);
});
});
diff --git a/packages/core/util.mjs b/packages/core/util.mjs
index ef3f1e961..915a2cb77 100644
--- a/packages/core/util.mjs
+++ b/packages/core/util.mjs
@@ -7,8 +7,8 @@ This program is free software: you can redistribute it and/or modify it under th
import { logger } from './logger.mjs';
// returns true if the given string is a note
-export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
-export const isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]?$/.test(name);
+export const isNoteWithOctave = (name) => /^[a-gA-G][#bsf]*[0-9]*$/.test(name);
+export const isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]*$/.test(name);
export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
@@ -23,6 +23,10 @@ export const tokenizeNote = (note) => {
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
const accs = { '#': 1, b: -1, s: 1, f: -1 };
+export const getAccidentalsOffset = (accidentals) => {
+ return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
+};
+
// turns the given note into its midi number representation
export const noteToMidi = (note, defaultOctave = 3) => {
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
@@ -30,7 +34,7 @@ export const noteToMidi = (note, defaultOctave = 3) => {
throw new Error('not a note: "' + note + '"');
}
const chroma = chromas[pc.toLowerCase()];
- const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
+ const offset = getAccidentalsOffset(acc);
return (Number(oct) + 1) * 12 + chroma + offset;
};
export const midiToFreq = (n) => {
diff --git a/packages/csound/package.json b/packages/csound/package.json
index 90130a101..058b8d774 100644
--- a/packages/csound/package.json
+++ b/packages/csound/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs
index a429395ff..95f3aed50 100644
--- a/packages/draw/draw.mjs
+++ b/packages/draw/draw.mjs
@@ -84,9 +84,18 @@ Pattern.prototype.onPaint = function (painter) {
state.controls.painters = [];
}
state.controls.painters.push(painter);
+ return state;
});
};
+// TODO - Why isn't this pure deep copy not working?
+// Pattern.prototype.onPaint = function (painter) {
+// return this.withState((state) => {
+// const painters = state.controls.painters ? [...state.controls.painters, painter] : [painter];
+// return new State(state.span, { ...state.controls, painters });
+// });
+// };
+
Pattern.prototype.getPainters = function () {
let painters = [];
this.queryArc(0, 0, { painters });
diff --git a/packages/draw/package.json b/packages/draw/package.json
index 6a4c57540..ac2123985 100644
--- a/packages/draw/package.json
+++ b/packages/draw/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/draw",
- "version": "1.2.4",
+ "version": "1.2.5",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
diff --git a/packages/gamepad/package.json b/packages/gamepad/package.json
index 555eac03f..8605e35cd 100644
--- a/packages/gamepad/package.json
+++ b/packages/gamepad/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/gamepad",
- "version": "1.2.4",
+ "version": "1.2.5",
"description": "Gamepad Inputs for strudel",
"main": "index.mjs",
"type": "module",
diff --git a/packages/hydra/package.json b/packages/hydra/package.json
index 7ba79d5d0..eccdde52f 100644
--- a/packages/hydra/package.json
+++ b/packages/hydra/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/hydra",
- "version": "1.2.4",
+ "version": "1.2.5",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"type": "module",
diff --git a/packages/midi/package.json b/packages/midi/package.json
index 2342cf7e9..ebb07fc3a 100644
--- a/packages/midi/package.json
+++ b/packages/midi/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
diff --git a/packages/mini/package.json b/packages/mini/package.json
index 6eeaab0da..05bbd019b 100644
--- a/packages/mini/package.json
+++ b/packages/mini/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/mini",
- "version": "1.2.4",
+ "version": "1.2.5",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs
index b879f24bf..b87a8b588 100644
--- a/packages/mondo/mondo.mjs
+++ b/packages/mondo/mondo.mjs
@@ -21,13 +21,14 @@ export class MondoParser {
close_curly: /^\}/,
number: /^-?[0-9]*\.?[0-9]+/, // before pipe!
// TODO: better error handling when "-" is used as rest, e.g "s [- bd]"
- op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? ..
+ op: /^[*/:!@%?+\-&]|^\.{2}/, // * / : ! @ % ? ..
// dollar: /^\$/,
pipe: /^#/,
stack: /^[,$]/,
or: /^[|]/,
plain: /^[a-zA-Z0-9-~_^#]+/,
};
+ op_precedence = [['*', '/', ':', '!', '@', '%', '?', '+', '-', '..'], ['&']];
// matches next token
next_token(code, offset = 0) {
for (let type in this.token_types) {
@@ -150,9 +151,9 @@ export class MondoParser {
}
return children;
}
- desugar_ops(children) {
+ desugar_ops(children, types) {
while (true) {
- let opIndex = children.findIndex((child) => child.type === 'op');
+ let opIndex = children.findIndex((child) => child.type === 'op' && types.includes(child.value));
if (opIndex === -1) break;
const op = { type: 'plain', value: children[opIndex].value };
if (opIndex === children.length - 1) {
@@ -263,8 +264,10 @@ export class MondoParser {
// the type we've removed before splitting needs to be added back
children = [{ type: 'plain', value: type }, ...children];
}
- children = this.desugar_ops(children);
- // children = this.desugar_pipes(children, (children) => this.desugar_dollars(children));
+ // for each precendence group, call desugar_ops once
+ this.op_precedence.forEach((ops) => {
+ children = this.desugar_ops(children, ops);
+ });
children = this.desugar_pipes(children);
return children;
}),
diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs
index 6393b10f9..a02c6fec1 100644
--- a/packages/mondo/test/mondo.test.mjs
+++ b/packages/mondo/test/mondo.test.mjs
@@ -117,6 +117,7 @@ describe('mondo sugar', () => {
it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)'));
it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))'));
it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))'));
+ it('should desugar x&y:z', () => expect(desguar('bd&3:8')).toEqual('(& (: 8 3) bd)'));
it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)'));
/* it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)'));
it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))'));
diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs
index e2c6dc9ad..a230c3cb9 100644
--- a/packages/mondough/mondough.mjs
+++ b/packages/mondough/mondough.mjs
@@ -11,6 +11,7 @@ import {
chooseIn,
degradeBy,
silence,
+ bjork,
} from '@strudel/core';
import { registerLanguage } from '@strudel/transpiler';
import { MondoRunner } from 'mondolang';
@@ -40,6 +41,7 @@ lib['!'] = replicate;
lib['@'] = expand;
lib['%'] = pace;
lib['?'] = degradeBy; // todo: default 0.5 not working..
+lib['&'] = bjork;
lib[':'] = tail;
lib['..'] = range;
lib['def'] = () => silence;
diff --git a/packages/mondough/package.json b/packages/mondough/package.json
index c81d76cb6..593d2264a 100644
--- a/packages/mondough/package.json
+++ b/packages/mondough/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/mondo",
- "version": "1.1.4",
+ "version": "1.1.5",
"description": "mondo notation for strudel",
"main": "mondough.mjs",
"type": "module",
diff --git a/packages/motion/package.json b/packages/motion/package.json
index 57cac9cc8..b0a4d5565 100644
--- a/packages/motion/package.json
+++ b/packages/motion/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/motion",
- "version": "1.2.4",
+ "version": "1.2.5",
"description": "DeviceMotion API for strudel",
"main": "index.mjs",
"type": "module",
diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json
index 2e32825fe..1b34a4eae 100644
--- a/packages/mqtt/package.json
+++ b/packages/mqtt/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/mqtt",
- "version": "1.2.4",
+ "version": "1.2.5",
"description": "MQTT API for strudel",
"main": "mqtt.mjs",
"type": "module",
diff --git a/packages/osc/osc.mjs b/packages/osc/osc.mjs
index fe0691522..68caab3a7 100644
--- a/packages/osc/osc.mjs
+++ b/packages/osc/osc.mjs
@@ -4,8 +4,6 @@ Copyright (C) 2022 Strudel contributors - see .
*/
-import OSC from 'osc-js';
-
import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core';
let connection; // Promise
@@ -13,19 +11,18 @@ function connect() {
if (!connection) {
// make sure this runs only once
connection = new Promise((resolve, reject) => {
- const osc = new OSC();
- osc.open();
- osc.on('open', () => {
- const url = osc.options?.plugin?.socket?.url;
- logger(`[osc] connected${url ? ` to ${url}` : ''}`);
- resolve(osc);
+ const ws = new WebSocket('ws://localhost:8080');
+ ws.addEventListener('open', (event) => {
+ logger(`[osc] websocket connected`);
+ resolve(ws);
});
- osc.on('close', () => {
+ ws.addEventListener('close', (event) => {
+ logger(`[osc] websocket closed`);
connection = undefined; // allows new connection afterwards
console.log('[osc] disconnected');
reject('OSC connection closed');
});
- osc.on('error', (err) => reject(err));
+ ws.addEventListener('error', (err) => reject(err));
}).catch((err) => {
connection = undefined;
throw new Error('Could not connect to OSC server. Is it running?');
@@ -61,15 +58,19 @@ export function parseControlsFromHap(hap, cps) {
const collator = new ClockCollator({});
export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
- const osc = await connect();
+ const ws = await connect();
const controls = parseControlsFromHap(hap, cps);
const keyvals = Object.entries(controls).flat();
+ const ts = collator.calculateTimestamp(currentTime, targetTime) * 1000;
+ const msg = { address: '/dirt/play', args: keyvals, timestamp: ts };
- const ts = Math.round(collator.calculateTimestamp(currentTime, targetTime) * 1000);
- const message = new OSC.Message('/dirt/play', ...keyvals);
- const bundle = new OSC.Bundle([message], ts);
- bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
- osc.send(bundle);
+ if ('oschost' in hap.value) {
+ msg['host'] = hap.value['oschost'];
+ }
+ if ('oscport' in hap.value) {
+ msg['port'] = hap.value['oscport'];
+ }
+ ws.send(JSON.stringify(msg));
}
/**
diff --git a/packages/osc/package.json b/packages/osc/package.json
index bc828d798..173b76744 100644
--- a/packages/osc/package.json
+++ b/packages/osc/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/osc",
- "version": "1.2.10",
+ "version": "1.3.0",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"bin": "./server.js",
@@ -38,7 +38,8 @@
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
- "osc-js": "^2.4.1"
+ "osc": "^2.4.5",
+ "ws": "^8.18.3"
},
"devDependencies": {
"pkg": "^5.8.1",
diff --git a/packages/osc/server.js b/packages/osc/server.js
old mode 100644
new mode 100755
index d7ec21c4b..ede35e975
--- a/packages/osc/server.js
+++ b/packages/osc/server.js
@@ -6,70 +6,59 @@ Copyright (C) 2022 Strudel contributors - see .
*/
-import OSC from 'osc-js';
+// import OSC from 'osc-js';
-const args = process.argv.slice(2);
-function getArgValue(flag) {
- const i = args.indexOf(flag);
- if (i !== -1) {
- const nextIsFlag = args[i + 1]?.startsWith('--') ?? true;
- if (nextIsFlag) return true;
- return args[i + 1];
- }
-}
+import { WebSocketServer } from 'ws';
+import osc from 'osc';
-let udpClientPort = Number(getArgValue('--port')) || 57120;
-let debug = Number(getArgValue('--debug')) || 0;
+const WS_PORT = 8080; // WebSocket server port
+const OSC_REMOTE_IP = '127.0.0.1';
+const OSC_REMOTE_PORT = 57120;
-const config = {
- receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
- udpServer: {
- host: 'localhost', // @param {string} Hostname of udp server to bind to
- port: 57121, // @param {number} Port of udp client for messaging
- // enabling the following line will receive tidal messages:
- // port: 57120, // @param {number} Port of udp client for messaging
- exclusive: false, // @param {boolean} Exclusive flag
- },
- udpClient: {
- host: 'localhost', // @param {string} Hostname of udp client for messaging
- port: udpClientPort, // @param {number} Port of udp client for messaging
- },
- wsServer: {
- host: 'localhost', // @param {string} Hostname of WebSocket server
- port: 8080, // @param {number} Port of WebSocket server
- },
-};
-
-const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
-
-if (debug) {
- osc.on('*', (message) => {
- const { address, args } = message;
- let str = '';
- for (let i = 0; i < args.length; i += 2) {
- str += `${args[i]}: ${args[i + 1]} `;
- }
- console.log(`${address} ${str}`);
- });
-}
-
-osc.on('error', (message) => {
- if (message.toString().includes('EADDRINUSE')) {
- console.log(`------ ERROR -------
-a server is already running on port 57121! to stop it:
-1. run "lsof -ti :57121 | xargs kill -9" (macos / linux)
-2. re-run the osc server
-`);
- } else {
- console.log(message);
- }
+const udpPort = new osc.UDPPort({
+ localAddress: '0.0.0.0',
+ localPort: 0,
+ remoteAddress: OSC_REMOTE_IP,
+ remotePort: OSC_REMOTE_PORT,
});
-osc.open();
+udpPort.open();
+console.log(`[Sending OSC] ${OSC_REMOTE_IP}:${OSC_REMOTE_PORT}`);
-console.log('osc client running on port', config.udpClient.port);
-console.log('osc server running on port', config.udpServer.port);
-console.log('websocket server running on port', config.wsServer.port);
-if (debug) {
- console.log('debug logs enabled. incoming messages will appear below');
-}
+udpPort.on('error', (e) => {
+ console.log('Error: ', e);
+});
+
+const wss = new WebSocketServer({ port: WS_PORT });
+console.log(`[Listening WS] ws://localhost:${WS_PORT}`);
+
+wss.on('connection', (ws) => {
+ console.log('New WebSocket connection');
+
+ ws.on('message', (message) => {
+ let osc_host = '127.0.0.1';
+ let osc_port = 57120;
+
+ try {
+ const data = JSON.parse(message);
+ if ('host' in data) {
+ osc_host = data['host'];
+ }
+ if ('port' in data) {
+ osc_port = data['port'];
+ }
+ let msg = { address: data['address'], args: data['args'] };
+ if ('timestamp' in data) {
+ msg = { timeTag: osc.timeTag(0, data['timestamp']), packets: [msg] };
+ }
+
+ udpPort.send(msg, osc_host, osc_port);
+ } catch (err) {
+ console.error('Error parsing message:', err);
+ }
+ });
+
+ ws.on('close', () => {
+ console.log('WebSocket connection closed');
+ });
+});
diff --git a/packages/repl/package.json b/packages/repl/package.json
index 41a165c34..1ad173c61 100644
--- a/packages/repl/package.json
+++ b/packages/repl/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/repl",
- "version": "1.2.6",
+ "version": "1.2.7",
"description": "Strudel REPL as a Web Component",
"module": "index.mjs",
"publishConfig": {
diff --git a/packages/serial/package.json b/packages/serial/package.json
index 9ed89cf2a..d73b05a1c 100644
--- a/packages/serial/package.json
+++ b/packages/serial/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/serial",
- "version": "1.2.4",
+ "version": "1.2.5",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"type": "module",
diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json
index 07e35674b..8c41a5d50 100644
--- a/packages/soundfonts/package.json
+++ b/packages/soundfonts/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/soundfonts",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
diff --git a/packages/superdough/package.json b/packages/superdough/package.json
index a82117774..a95fc5ed0 100644
--- a/packages/superdough/package.json
+++ b/packages/superdough/package.json
@@ -1,6 +1,6 @@
{
"name": "superdough",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs
index 3232fa476..84bac3582 100644
--- a/packages/superdough/sampler.mjs
+++ b/packages/superdough/sampler.mjs
@@ -129,9 +129,7 @@ function githubPath(base, subpath = '') {
let repo = components.length >= 2 ? components[1] : 'samples';
let branch = components.length >= 3 ? components[2] : 'main';
let other = components.slice(3);
- if (subpath) {
- other.push(subpath);
- }
+ other.push(subpath ? subpath : '');
other = other.join('/');
return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`;
diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs
index a37504b1d..36bd68969 100644
--- a/packages/superdough/superdough.mjs
+++ b/packages/superdough/superdough.mjs
@@ -630,6 +630,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// effects
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
+ shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
if (tremolosync != null) {
diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs
index 475c05f63..418b7af0a 100644
--- a/packages/superdough/util.mjs
+++ b/packages/superdough/util.mjs
@@ -16,13 +16,17 @@ export const tokenizeNote = (note) => {
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
const accs = { '#': 1, b: -1, s: 1, f: -1 };
+export const getAccidentalsOffset = (accidentals) => {
+ return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
+};
+
export const noteToMidi = (note, defaultOctave = 3) => {
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
if (!pc) {
throw new Error('not a note: "' + note + '"');
}
const chroma = chromas[pc.toLowerCase()];
- const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
+ const offset = getAccidentalsOffset(acc);
return (Number(oct) + 1) * 12 + chroma + offset;
};
export const midiToFreq = (n) => {
diff --git a/packages/supradough/package.json b/packages/supradough/package.json
index 7e465c0a9..503043718 100644
--- a/packages/supradough/package.json
+++ b/packages/supradough/package.json
@@ -1,6 +1,6 @@
{
"name": "supradough",
- "version": "1.2.3",
+ "version": "1.2.4",
"description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.",
"main": "index.mjs",
"type": "module",
@@ -32,6 +32,5 @@
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*",
"wav-encoder": "^1.3.0"
- },
- "dependencies": {}
+ }
}
diff --git a/packages/tonal/package.json b/packages/tonal/package.json
index 1461bdc8e..611df6766 100644
--- a/packages/tonal/package.json
+++ b/packages/tonal/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/tonal",
- "version": "1.2.4",
+ "version": "1.2.5",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs
index 5f61b05a9..06aa91188 100644
--- a/packages/tonal/test/tonal.test.mjs
+++ b/packages/tonal/test/tonal.test.mjs
@@ -61,13 +61,6 @@ describe('tonal', () => {
.firstCycleValues.map((h) => h.note),
).toEqual(['B2', 'Eb3', 'A2', 'G3', 'F3']);
});
- it('produces silence for mixed sharps and flats', () => {
- expect(
- n(seq('0b#', '1#b', '2#b#'))
- .scale('C major')
- .firstCycleValues.map((h) => h.note),
- ).toEqual([]);
- });
it('snaps notes (upwards) to scale', () => {
const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb'];
const expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3'];
diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs
index ae75ab690..0dc468b20 100644
--- a/packages/tonal/tonal.mjs
+++ b/packages/tonal/tonal.mjs
@@ -5,9 +5,8 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { Note, Interval, Scale } from '@tonaljs/tonal';
-import { register, _mod, silence, logger, pure, isNote } from '@strudel/core';
+import { register, _mod, logger, isNote, noteToMidi, removeUndefineds, getAccidentalsOffset } from '@strudel/core';
import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs';
-import { noteToMidi } from '../core/util.mjs';
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
@@ -185,17 +184,15 @@ function _convertStepToNumberAndOffset(step) {
step = String(step);
// Check to see if the step matches the expected format:
// - A number (possibly negative)
- // - Some number of sharps or flats (but not both)
- const match = /^(-?\d+)(#+|b+)?$/.exec(step);
+ // - Some number of sharps or flats
+ const match = /^(-?\d+)([#bsf]*)$/.exec(step);
if (!match) {
throw new Error(`invalid scale step "${step}", expected number or integer with optional # b suffixes`);
}
asNumber = Number(match[1]);
- // These decorations will determine the semitone offset based on the number of
- // sharps or flats
- const decorations = match[2] || '';
- offset = decorations[0] === '#' ? decorations.length : -decorations.length;
+ const accidentals = match[2] || '';
+ offset = getAccidentalsOffset(accidentals);
}
return [asNumber, offset];
}
@@ -226,7 +223,7 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) {
* Turns numbers into notes in the scale (zero indexed) or quantizes notes to a scale.
*
* When describing notes via numbers, note that negative numbers can be used to wrap backwards
- * in the scale as well as sharps or flats (but not both) to produce notes outside of the scale.
+ * in the scale as well as sharps or flats to produce notes outside of the scale.
*
* Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
*
@@ -254,7 +251,6 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) {
* @example
* note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3)
*/
-
export const scale = register(
'scale',
function (scale, pat) {
@@ -262,44 +258,47 @@ export const scale = register(
if (Array.isArray(scale)) {
scale = scale.flat().join(' ');
}
- return (
- pat
- .fmap((value) => {
- const isObject = typeof value === 'object';
- // The case where the note has been defined via `n` or `pure`
- if (!isObject || (isObject && ('n' in value || 'value' in value))) {
- const step = isObject ? (value.n ?? value.value) : value;
- delete value.n; // remove n so it won't cause trouble
- if (isNote(step)) {
- // legacy..
- return pure(step);
+ return pat.withHaps((haps) => {
+ haps = haps.map((hap) => {
+ let hVal = hap.value;
+ const isObject = typeof hVal === 'object';
+ // If hVal is a pure value, place it on `n` so that we interpret it as a scale degree
+ hVal = isObject ? hVal : { n: hVal };
+ const { note, n, value, ...otherValues } = hVal;
+ const noteOrStep = note ?? n ?? value;
+ if (noteOrStep === undefined) {
+ logger(
+ `[tonal] Invalid value format for 'scale'. Value must contain n, note, or value but received keys [${Object.keys(hVal).join(', ')}]`,
+ 'error',
+ );
+ return hap; // pass the value through unchanged
+ }
+ let scaleNote;
+ if (isNote(noteOrStep)) {
+ // Note case (quantize to scale)
+ scaleNote = _getNearestScaleNote(scale, noteOrStep);
+ hap.value = { ...otherValues, note: scaleNote };
+ } else {
+ // Step case (convert to note in scale)
+ try {
+ const [number, offset] = _convertStepToNumberAndOffset(noteOrStep);
+ if (otherValues.anchor) {
+ scaleNote = stepInNamedScale(number, scale, otherValues.anchor);
+ } else {
+ scaleNote = scaleStep(number, scale);
}
- try {
- const [number, offset] = _convertStepToNumberAndOffset(step);
- let note;
- if (isObject && value.anchor) {
- note = stepInNamedScale(number, scale, value.anchor);
- } else {
- note = scaleStep(number, scale);
- }
- if (offset != 0) note = Note.transpose(note, Interval.fromSemitones(offset));
- value = pure(isObject ? { ...value, note } : note);
- } catch (err) {
- logger(`[tonal] ${err.message}`, 'error');
- return silence;
- }
- return value;
+ if (offset != 0) scaleNote = Note.transpose(scaleNote, Interval.fromSemitones(offset));
+ } catch (err) {
+ logger(`[tonal] ${err.message}`, 'error');
+ return; // will be removed
}
- // The case where the note has been defined via `note`
- else {
- const note = _getNearestScaleNote(scale, value.note);
- return pure(isObject ? { ...value, note } : note);
- }
- })
- .outerJoin()
- // legacy:
- .withHap((hap) => hap.setContext({ ...hap.context, scale }))
- );
+ }
+ hap.value = isObject ? { ...otherValues, note: scaleNote } : scaleNote;
+ // Tag with scale for downsteam scale-aware operations
+ return hap.setContext({ ...hap.context, scale });
+ });
+ return removeUndefineds(haps);
+ });
},
true,
true, // preserve step count
diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json
index 18722bdc2..bd28a19f4 100644
--- a/packages/transpiler/package.json
+++ b/packages/transpiler/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/transpiler",
- "version": "1.2.4",
+ "version": "1.2.5",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"type": "module",
diff --git a/packages/web/package.json b/packages/web/package.json
index df21f4055..aef044c79 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/web",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"module": "web.mjs",
"publishConfig": {
diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json
index 49da00f23..2d5cd420c 100644
--- a/packages/webaudio/package.json
+++ b/packages/webaudio/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/webaudio",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
diff --git a/packages/xen/package.json b/packages/xen/package.json
index 0a6736d9c..4015c437d 100644
--- a/packages/xen/package.json
+++ b/packages/xen/package.json
@@ -1,6 +1,6 @@
{
"name": "@strudel/xen",
- "version": "1.2.4",
+ "version": "1.2.5",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"type": "module",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6cabab305..903f46370 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -41,7 +41,7 @@ importers:
version: 2.2.7
'@vitest/coverage-v8':
specifier: 3.0.4
- version: 3.0.4(vitest@3.0.4)
+ version: 3.0.4(vitest@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))
'@vitest/ui':
specifier: ^3.0.4
version: 3.0.4(vitest@3.0.4)
@@ -415,9 +415,12 @@ importers:
'@strudel/core':
specifier: workspace:*
version: link:../core
- osc-js:
- specifier: ^2.4.1
- version: 2.4.1
+ osc:
+ specifier: ^2.4.5
+ version: 2.4.5
+ ws:
+ specifier: ^8.18.3
+ version: 8.18.3
devDependencies:
pkg:
specifier: ^5.8.1
@@ -2432,6 +2435,70 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
+ '@serialport/binding-mock@10.2.2':
+ resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==}
+ engines: {node: '>=12.0.0'}
+
+ '@serialport/bindings-cpp@12.0.1':
+ resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==}
+ engines: {node: '>=16.0.0'}
+
+ '@serialport/bindings-interface@1.2.2':
+ resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==}
+ engines: {node: ^12.22 || ^14.13 || >=16}
+
+ '@serialport/parser-byte-length@12.0.0':
+ resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==}
+ engines: {node: '>=12.0.0'}
+
+ '@serialport/parser-cctalk@12.0.0':
+ resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==}
+ engines: {node: '>=12.0.0'}
+
+ '@serialport/parser-delimiter@11.0.0':
+ resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==}
+ engines: {node: '>=12.0.0'}
+
+ '@serialport/parser-delimiter@12.0.0':
+ resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==}
+ engines: {node: '>=12.0.0'}
+
+ '@serialport/parser-inter-byte-timeout@12.0.0':
+ resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==}
+ engines: {node: '>=12.0.0'}
+
+ '@serialport/parser-packet-length@12.0.0':
+ resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==}
+ engines: {node: '>=8.6.0'}
+
+ '@serialport/parser-readline@11.0.0':
+ resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==}
+ engines: {node: '>=12.0.0'}
+
+ '@serialport/parser-readline@12.0.0':
+ resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==}
+ engines: {node: '>=12.0.0'}
+
+ '@serialport/parser-ready@12.0.0':
+ resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==}
+ engines: {node: '>=12.0.0'}
+
+ '@serialport/parser-regex@12.0.0':
+ resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==}
+ engines: {node: '>=12.0.0'}
+
+ '@serialport/parser-slip-encoder@12.0.0':
+ resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==}
+ engines: {node: '>=12.0.0'}
+
+ '@serialport/parser-spacepacket@12.0.0':
+ resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==}
+ engines: {node: '>=12.0.0'}
+
+ '@serialport/stream@12.0.0':
+ resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==}
+ engines: {node: '>=12.0.0'}
+
'@shikijs/core@1.29.1':
resolution: {integrity: sha512-Mo1gGGkuOYjDu5H8YwzmOuly9vNr8KDVkqj9xiKhhhFS8jisAtDSEWB9hzqRHLVQgFdA310e8XRJcW4tYhRB2A==}
@@ -3692,6 +3759,15 @@ packages:
supports-color:
optional: true
+ debug@4.3.4:
+ resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
debug@4.4.0:
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
engines: {node: '>=6.0'}
@@ -5301,6 +5377,9 @@ packages:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
engines: {node: '>=10'}
+ long@4.0.0:
+ resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==}
+
longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
@@ -5684,6 +5763,9 @@ packages:
ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
+ ms@2.1.2:
+ resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
+
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -5742,6 +5824,9 @@ packages:
resolution: {integrity: sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==}
engines: {node: '>=10'}
+ node-addon-api@7.0.0:
+ resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==}
+
node-addon-api@8.3.0:
resolution: {integrity: sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==}
engines: {node: ^18 || ^20 || >= 21}
@@ -5780,6 +5865,10 @@ packages:
resolution: {integrity: sha512-yqkmYrMbK1wPrfz7mgeYvA4tBperLg9FQ4S3Sau3nSAkpOA0x0zC8nQ1siBwozy1f4SE8vq2n1WKv99r+PCa1Q==}
engines: {node: '>= 0.6.0'}
+ node-gyp-build@4.6.0:
+ resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==}
+ hasBin: true
+
node-gyp-build@4.8.4:
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
hasBin: true
@@ -5938,8 +6027,8 @@ packages:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'}
- osc-js@2.4.1:
- resolution: {integrity: sha512-QlSeRKJclL47FNvO1MUCAAp9frmCF9zcYbnf6R9HpcklAst8ZyX3ISsk1v/Vghr/5GmXn0bhVjFXF9h+hfnl4Q==}
+ osc@2.4.5:
+ resolution: {integrity: sha512-Nc4/qcl+vA/CMxiKS1xrYgzjfnyB3W94gZnrkn3eTzihlndbEml6+wj1YQNKBI4r+qrw3obCcEoU7SVH/OxpxA==}
own-keys@1.0.1:
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
@@ -6700,6 +6789,10 @@ packages:
serialize-javascript@4.0.0:
resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==}
+ serialport@12.0.0:
+ resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==}
+ engines: {node: '>=16.0.0'}
+
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
@@ -6787,6 +6880,9 @@ packages:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
+ slip@1.0.2:
+ resolution: {integrity: sha512-XrcHe3NAcyD3wO+O4I13RcS4/3AF+S9RvGNj9JhJeS02HyImwD2E3QWLrmn9hBfL+fB6yapagwxRkeyYzhk98g==}
+
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
@@ -7662,6 +7758,9 @@ packages:
resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==}
engines: {node: '>=18'}
+ wolfy87-eventemitter@5.2.9:
+ resolution: {integrity: sha512-P+6vtWyuDw+MB01X7UeF8TaHBvbCovf4HPEMF/SV7BdDc1SMTiBy13SRD71lQh4ExFTG1d/WNzDGDCyOKSMblw==}
+
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
@@ -7784,6 +7883,18 @@ packages:
utf-8-validate:
optional: true
+ ws@8.18.3:
+ resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
xmlcreate@2.0.4:
resolution: {integrity: sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==}
@@ -9780,6 +9891,76 @@ snapshots:
'@rtsao/scc@1.1.0': {}
+ '@serialport/binding-mock@10.2.2':
+ dependencies:
+ '@serialport/bindings-interface': 1.2.2
+ debug: 4.4.0
+ transitivePeerDependencies:
+ - supports-color
+ optional: true
+
+ '@serialport/bindings-cpp@12.0.1':
+ dependencies:
+ '@serialport/bindings-interface': 1.2.2
+ '@serialport/parser-readline': 11.0.0
+ debug: 4.3.4
+ node-addon-api: 7.0.0
+ node-gyp-build: 4.6.0
+ transitivePeerDependencies:
+ - supports-color
+ optional: true
+
+ '@serialport/bindings-interface@1.2.2':
+ optional: true
+
+ '@serialport/parser-byte-length@12.0.0':
+ optional: true
+
+ '@serialport/parser-cctalk@12.0.0':
+ optional: true
+
+ '@serialport/parser-delimiter@11.0.0':
+ optional: true
+
+ '@serialport/parser-delimiter@12.0.0':
+ optional: true
+
+ '@serialport/parser-inter-byte-timeout@12.0.0':
+ optional: true
+
+ '@serialport/parser-packet-length@12.0.0':
+ optional: true
+
+ '@serialport/parser-readline@11.0.0':
+ dependencies:
+ '@serialport/parser-delimiter': 11.0.0
+ optional: true
+
+ '@serialport/parser-readline@12.0.0':
+ dependencies:
+ '@serialport/parser-delimiter': 12.0.0
+ optional: true
+
+ '@serialport/parser-ready@12.0.0':
+ optional: true
+
+ '@serialport/parser-regex@12.0.0':
+ optional: true
+
+ '@serialport/parser-slip-encoder@12.0.0':
+ optional: true
+
+ '@serialport/parser-spacepacket@12.0.0':
+ optional: true
+
+ '@serialport/stream@12.0.0':
+ dependencies:
+ '@serialport/bindings-interface': 1.2.2
+ debug: 4.3.4
+ transitivePeerDependencies:
+ - supports-color
+ optional: true
+
'@shikijs/core@1.29.1':
dependencies:
'@shikijs/engine-javascript': 1.29.1
@@ -10384,7 +10565,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@vitest/coverage-v8@3.0.4(vitest@3.0.4)':
+ '@vitest/coverage-v8@3.0.4(vitest@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))':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
@@ -11298,6 +11479,11 @@ snapshots:
dependencies:
ms: 2.1.3
+ debug@4.3.4:
+ dependencies:
+ ms: 2.1.2
+ optional: true
+
debug@4.4.0:
dependencies:
ms: 2.1.3
@@ -13167,6 +13353,8 @@ snapshots:
chalk: 4.1.2
is-unicode-supported: 0.1.0
+ long@4.0.0: {}
+
longest-streak@3.1.0: {}
loupe@3.1.2: {}
@@ -13844,6 +14032,9 @@ snapshots:
ms@2.0.0: {}
+ ms@2.1.2:
+ optional: true
+
ms@2.1.3: {}
multimatch@5.0.0:
@@ -13893,6 +14084,9 @@ snapshots:
dependencies:
semver: 7.6.3
+ node-addon-api@7.0.0:
+ optional: true
+
node-addon-api@8.3.0: {}
node-domexception@1.0.0: {}
@@ -13919,6 +14113,9 @@ snapshots:
node-getopt@0.3.2: {}
+ node-gyp-build@4.6.0:
+ optional: true
+
node-gyp-build@4.8.4: {}
node-gyp@10.3.1:
@@ -14170,11 +14367,17 @@ snapshots:
os-tmpdir@1.0.2: {}
- osc-js@2.4.1:
+ osc@2.4.5:
dependencies:
+ long: 4.0.0
+ slip: 1.0.2
+ wolfy87-eventemitter: 5.2.9
ws: 8.18.0
+ optionalDependencies:
+ serialport: 12.0.0
transitivePeerDependencies:
- bufferutil
+ - supports-color
- utf-8-validate
own-keys@1.0.1:
@@ -15069,6 +15272,26 @@ snapshots:
dependencies:
randombytes: 2.1.0
+ serialport@12.0.0:
+ dependencies:
+ '@serialport/binding-mock': 10.2.2
+ '@serialport/bindings-cpp': 12.0.1
+ '@serialport/parser-byte-length': 12.0.0
+ '@serialport/parser-cctalk': 12.0.0
+ '@serialport/parser-delimiter': 12.0.0
+ '@serialport/parser-inter-byte-timeout': 12.0.0
+ '@serialport/parser-packet-length': 12.0.0
+ '@serialport/parser-readline': 12.0.0
+ '@serialport/parser-ready': 12.0.0
+ '@serialport/parser-regex': 12.0.0
+ '@serialport/parser-slip-encoder': 12.0.0
+ '@serialport/parser-spacepacket': 12.0.0
+ '@serialport/stream': 12.0.0
+ debug: 4.3.4
+ transitivePeerDependencies:
+ - supports-color
+ optional: true
+
set-blocking@2.0.0: {}
set-function-length@1.2.2:
@@ -15211,6 +15434,8 @@ snapshots:
slash@3.0.0: {}
+ slip@1.0.2: {}
+
smart-buffer@4.2.0: {}
socks-proxy-agent@8.0.5:
@@ -16120,6 +16345,8 @@ snapshots:
dependencies:
string-width: 7.2.0
+ wolfy87-eventemitter@5.2.9: {}
+
word-wrap@1.2.5: {}
wordwrap@0.0.3: {}
@@ -16319,6 +16546,8 @@ snapshots:
ws@8.18.0: {}
+ ws@8.18.3: {}
+
xmlcreate@2.0.4: {}
xtend@4.0.2: {}
diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap
index d7e13d4d9..a27c5a6bc 100644
--- a/test/__snapshots__/examples.test.mjs.snap
+++ b/test/__snapshots__/examples.test.mjs.snap
@@ -3838,8 +3838,8 @@ exports[`runs examples > example "fast" example index 0 1`] = `
exports[`runs examples > example "fastChunk" example index 0 1`] = `
[
- "[ 0/1 → 1/4 | color:red note:0 ]",
- "[ 1/4 → 1/2 | color:red note:1 ]",
+ "[ 0/1 → 1/4 | note:C2 color:red ]",
+ "[ 1/4 → 1/2 | note:D2 color:red ]",
"[ 1/2 → 3/4 | note:E2 ]",
"[ 3/4 → 1/1 | note:F2 ]",
"[ 1/1 → 5/4 | note:G2 ]",
@@ -3848,8 +3848,8 @@ exports[`runs examples > example "fastChunk" example index 0 1`] = `
"[ 7/4 → 2/1 | note:C3 ]",
"[ 2/1 → 9/4 | note:D3 ]",
"[ 9/4 → 5/2 | note:D2 ]",
- "[ 5/2 → 11/4 | color:red note:2 ]",
- "[ 11/4 → 3/1 | color:red note:3 ]",
+ "[ 5/2 → 11/4 | note:E2 color:red ]",
+ "[ 11/4 → 3/1 | note:F2 color:red ]",
"[ 3/1 → 13/4 | note:G2 ]",
"[ 13/4 → 7/2 | note:A2 ]",
"[ 7/2 → 15/4 | note:B2 ]",
@@ -6958,6 +6958,24 @@ exports[`runs examples > example "orbit" example index 0 1`] = `
]
`;
+exports[`runs examples > example "oschost" example index 0 1`] = `
+[
+ "[ 0/1 → 1/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
+ "[ 1/1 → 2/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
+ "[ 2/1 → 3/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
+ "[ 3/1 → 4/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
+]
+`;
+
+exports[`runs examples > example "oscport" example index 0 1`] = `
+[
+ "[ 0/1 → 1/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
+ "[ 1/1 → 2/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
+ "[ 2/1 → 3/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
+ "[ 3/1 → 4/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
+]
+`;
+
exports[`runs examples > example "outside" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:A3 ]",
diff --git a/website/src/pages/learn/synths.mdx b/website/src/pages/learn/synths.mdx
index 21a9b917e..0fcc41363 100644
--- a/website/src/pages/learn/synths.mdx
+++ b/website/src/pages/learn/synths.mdx
@@ -157,7 +157,7 @@ It has 20 parameters in total, here is a snippet that uses all:
.pitchJump(0) // +/- pitch change after pitchJumpTime
.pitchJumpTime(0) // >0 time after pitchJump is applied
.lfo(0) // >0 resets slide + pitchJump + sets tremolo speed
- .tremolo(0) // 0-1 lfo volume modulation amount
+ .tremolo(0.5) // 0-1 lfo volume modulation amount
//.duration(.2) // overwrite strudel event duration
//.gain(1) // change volume
._scope() // vizualise waveform (not zzfx related)
diff --git a/website/src/pages/technical-manual/repl.mdx b/website/src/pages/technical-manual/repl.mdx
index 8c4287af5..f74a4fb8b 100644
--- a/website/src/pages/technical-manual/repl.mdx
+++ b/website/src/pages/technical-manual/repl.mdx
@@ -117,6 +117,10 @@ Here's an example AST for `c3 [e3 g3]`
which translates to `seq(c3, seq(e3, g3))`
+## Vim Keybindings
+
+See the separate page on Vim shortcuts for a quick reference: [/technical-manual/vim](/technical-manual/vim)
+
## Scheduling Events
After an instance of `Pattern` is obtained from the user code,
diff --git a/website/src/pages/technical-manual/vim.mdx b/website/src/pages/technical-manual/vim.mdx
new file mode 100644
index 000000000..fa53aa550
--- /dev/null
+++ b/website/src/pages/technical-manual/vim.mdx
@@ -0,0 +1,37 @@
+---
+title: Vim Shortcuts
+layout: ../../layouts/MainLayout.astro
+---
+
+# Vim Shortcuts in the REPL
+
+When the REPL editor (CodeMirror) is configured to use Vim keybindings, the following commands are available:
+
+- :w — Evaluate the current code
+
+ - Triggers the same evaluation as Ctrl+Enter / Alt+Enter
+ - You'll see messages in the Console panel such as:
+ - [vim] :w — evaluating code
+ - [repl] evaluate via event
+ - [eval] code updated
+
+- :q — Stop/pause playback
+
+ - Triggers the same stop action as Alt+.
+ - Useful to quickly stop scheduling without leaving Vim mode
+
+- gc — Toggle line comments for the current selection(s)
+
+ - Works in normal and visual mode
+ - If there's a selection, all selected lines are toggled
+
+Notes
+
+- Behavior respects the current language mode in the editor for comment syntax.
+- If multiple REPL editors are open, commands target the active editor. The implementation dispatches custom events handled by the editor.
+- If you don't see the Console panel, open the right panel in the REPL UI.
+
+Troubleshooting
+
+- If :w logs but evaluation doesn't apply, ensure Vim keybindings are active and try again. You can also use Ctrl+Enter as a fallback.
+- For :q / gc, ensure focus is inside the editor. If an error occurs, reload the page to reset editor state and try again.
diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs
index 1fbc84021..8a6ccc7e0 100644
--- a/website/src/repl/prebake.mjs
+++ b/website/src/repl/prebake.mjs
@@ -6,6 +6,7 @@ import './files.mjs';
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
+const baseCDN = 'https://strudel.b-cdn.net';
export async function prebake() {
// https://archive.org/details/SalamanderGrandPianoV3
@@ -19,23 +20,23 @@ export async function prebake() {
// => getting "window is not defined", as soon as "@strudel/soundfonts" is imported statically
// seems to be a problem with soundfont2
import('@strudel/soundfonts').then(({ registerSoundfonts }) => registerSoundfonts()),
- samples(`${baseNoTrailing}/piano.json`, undefined, { prebake: true }),
+ samples(`${baseCDN}/piano.json`, `${baseCDN}/piano/`, { prebake: true }),
// https://github.com/sgossner/VCSL/
// https://api.github.com/repositories/126427031/contents/
// LICENSE: CC0 general-purpose
- samples(`${baseNoTrailing}/vcsl.json`, 'github:sgossner/VCSL/master/', { prebake: true }),
- samples(`${baseNoTrailing}/tidal-drum-machines.json`, 'github:ritchse/tidal-drum-machines/main/machines/', {
+ samples(`${baseCDN}/vcsl.json`, `${baseCDN}/VCSL/`, { prebake: true }),
+ samples(`${baseCDN}/tidal-drum-machines.json`, `${baseCDN}/tidal-drum-machines/machines/`, {
prebake: true,
tag: 'drum-machines',
}),
- samples(`${baseNoTrailing}/uzu-drumkit.json`, undefined, {
+ samples(`${baseCDN}/uzu-drumkit.json`, `${baseCDN}/uzu-drumkit/`, {
prebake: true,
tag: 'drum-machines',
}),
- samples(`${baseNoTrailing}/uzu-wavetables.json`, undefined, {
+ samples(`${baseCDN}/uzu-wavetables.json`, `${baseCDN}/uzu-wavetables/`, {
prebake: true,
}),
- samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }),
+ samples(`${baseCDN}/mridangam.json`, `${baseCDN}/mrid/`, { prebake: true, tag: 'drum-machines' }),
samples(
{
casio: ['casio/high.wav', 'casio/low.wav', 'casio/noise.wav'],
@@ -145,14 +146,14 @@ export async function prebake() {
'num/20.wav',
],
},
- 'github:tidalcycles/dirt-samples',
+ `${baseCDN}/Dirt-Samples/`,
{
prebake: true,
},
),
]);
- aliasBank(`${baseNoTrailing}/tidal-drum-machines-alias.json`);
+ aliasBank(`${baseCDN}/tidal-drum-machines-alias.json`);
}
const maxPan = noteToMidi('C8');