mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 14:26:58 -04:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
|
||||
@@ -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))'));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/soundfonts",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.6",
|
||||
"description": "Soundsfont support for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/tonal",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"description": "Tonal functions for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -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'];
|
||||
|
||||
+45
-46
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
-3
@@ -6016,9 +6016,6 @@ 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==}
|
||||
|
||||
|
||||
@@ -3905,8 +3905,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 ]",
|
||||
@@ -3915,8 +3915,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 ]",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user