Compare commits

..

8 Commits

Author SHA1 Message Date
Alex McLean e9188f07e0 Merge branch 'main' into initial-random-value 2025-10-27 17:24:07 +01:00
froos 0fef7b4b1f Merge branch 'main' into initial-random-value 2025-10-20 20:40:54 +02:00
Felix Roos 987fb5903c Merge branch 'main' into initial-random-value 2025-07-06 23:48:00 +02:00
alex 998b37038e snapshot 2025-02-23 11:02:56 +00:00
alex 92c4cff606 format 2025-02-23 10:57:45 +00:00
alex 252416b056 return 0.5 for random value at cycle 0, rather than 0 2025-02-23 10:55:37 +00:00
alex 51d9c8c214 add missing import 2025-02-22 17:22:23 +00:00
alex b62ff5e115 allow wchooseCycles probabilities to be patterned 2025-02-22 17:19:21 +00:00
30 changed files with 784 additions and 1200 deletions
+10 -14
View File
@@ -1,10 +1,8 @@
import jsdoc from '../../doc.json'; import jsdoc from '../../doc.json';
import { autocompletion } from '@codemirror/autocomplete'; import { autocompletion } from '@codemirror/autocomplete';
import { h } from './html'; import { h } from './html';
//TODO: fix tonal scale import import { Scale } from '@tonaljs/tonal';
// import { Scale } from '@tonaljs/tonal'; import { soundMap } from 'superdough';
// import { soundMap } from '@strudel/webaudio';
let soundMap = undefined;
import { complex } from '@strudel/tonal'; import { complex } from '@strudel/tonal';
const escapeHtml = (str) => { const escapeHtml = (str) => {
@@ -81,9 +79,7 @@ const hasExcludedTags = (doc) =>
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)); ['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
export function bankCompletions() { export function bankCompletions() {
// TODO: FIX IMPORT const soundDict = soundMap.get();
const soundDict = soundMap?.get() ?? {};
const banks = new Set(); const banks = new Set();
for (const key of Object.keys(soundDict)) { for (const key of Object.keys(soundDict)) {
const [bank, suffix] = key.split('_'); const [bank, suffix] = key.split('_');
@@ -94,13 +90,13 @@ export function bankCompletions() {
.map((name) => ({ label: name, type: 'bank' })); .map((name) => ({ label: name, type: 'bank' }));
} }
// Attempt to get all scale names from Tonal TODO: FIX IMPORT // Attempt to get all scale names from Tonal
let scaleCompletions = []; let scaleCompletions = [];
// try { try {
// scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' })); scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' }));
// } catch (e) { } catch (e) {
// console.warn('[autocomplete] Could not load scale names from Tonal:', e); console.warn('[autocomplete] Could not load scale names from Tonal:', e);
// } }
// Valid mode values for voicing // Valid mode values for voicing
const modeCompletions = [ const modeCompletions = [
@@ -272,7 +268,7 @@ function soundHandler(context) {
const inside = text.slice(quoteIdx + 1); const inside = text.slice(quoteIdx + 1);
const fragMatch = inside.match(SOUND_FRAGMENT_MATCH_REGEX); const fragMatch = inside.match(SOUND_FRAGMENT_MATCH_REGEX);
const fragment = fragMatch ? fragMatch[1] : inside; const fragment = fragMatch ? fragMatch[1] : inside;
const soundNames = Object.keys(soundMap?.get() ?? {}).sort(); const soundNames = Object.keys(soundMap.get()).sort();
const filteredSounds = soundNames.filter((name) => name.includes(fragment)); const filteredSounds = soundNames.filter((name) => name.includes(fragment));
let options = filteredSounds.map((name) => ({ label: name, type: 'sound' })); let options = filteredSounds.map((name) => ({ label: name, type: 'sound' }));
const from = soundContext.to - fragment.length; const from = soundContext.to - fragment.length;
+24 -70
View File
@@ -1,31 +1,30 @@
import { closeBrackets } from '@codemirror/autocomplete'; import { closeBrackets } from '@codemirror/autocomplete';
import { indentWithTab, toggleLineComment } from '@codemirror/commands'; export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
// import { search, highlightSelectionMatches } from '@codemirror/search';
import { indentWithTab } from '@codemirror/commands';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript'; import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language'; import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
import { Compartment, EditorState, Prec } from '@codemirror/state'; import { Compartment, EditorState, Prec } from '@codemirror/state';
import { import {
drawSelection,
EditorView, EditorView,
highlightActiveLine,
highlightActiveLineGutter, highlightActiveLineGutter,
highlightActiveLine,
keymap, keymap,
lineNumbers, lineNumbers,
drawSelection,
} from '@codemirror/view'; } from '@codemirror/view';
import { persistentAtom } from '@nanostores/persistent'; import { repl, registerControl } from '@strudel/core';
import { logger, registerControl, repl } from '@strudel/core'; import { Drawer, cleanupDraw } from '@strudel/draw';
import { cleanupDraw, Drawer } from '@strudel/draw';
import { isAutoCompletionEnabled } from './autocomplete.mjs'; import { isAutoCompletionEnabled } from './autocomplete.mjs';
import { basicSetup } from './basicSetup.mjs'; import { isTooltipEnabled } from './tooltip.mjs';
import { flash, isFlashEnabled } from './flash.mjs'; import { flash, isFlashEnabled } from './flash.mjs';
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs'; import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
import { keybindings } from './keybindings.mjs'; import { keybindings } from './keybindings.mjs';
import { initTheme, activateTheme, theme } from './themes.mjs';
import { sliderPlugin, updateSliderWidgets } from './slider.mjs'; import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
import { activateTheme, initTheme, theme } from './themes.mjs'; import { widgetPlugin, updateWidgets } from './widget.mjs';
import { isTooltipEnabled } from './tooltip.mjs'; import { persistentAtom } from '@nanostores/persistent';
import { updateWidgets, widgetPlugin } from './widget.mjs'; import { basicSetup } from './basicSetup.mjs';
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
const extensions = { const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []), isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
@@ -95,8 +94,8 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
}), }),
sliderPlugin, sliderPlugin,
widgetPlugin, widgetPlugin,
// indentOnInput(), // works without. already brought with javascript // indentOnInput(), // works without. already brought with javascript extension?
// extension? bracketMatching(), // does not do anything // bracketMatching(), // does not do anything
syntaxHighlighting(defaultHighlightStyle), syntaxHighlighting(defaultHighlightStyle),
EditorView.updateListener.of((v) => onChange(v)), EditorView.updateListener.of((v) => onChange(v)),
drawSelection({ cursorBlinkRate: 0 }), drawSelection({ cursorBlinkRate: 0 }),
@@ -120,13 +119,13 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
run: () => onStop?.(), run: () => onStop?.(),
}, },
/* { /* {
key: 'Ctrl-Shift-.', key: 'Ctrl-Shift-.',
run: () => (onPanic ? onPanic() : onStop?.()), run: () => (onPanic ? onPanic() : onStop?.()),
}, },
{ {
key: 'Ctrl-Shift-Enter', key: 'Ctrl-Shift-Enter',
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()), run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
}, */ }, */
]), ]),
), ),
], ],
@@ -207,8 +206,7 @@ export class StrudelMirror {
updateWidgets(this.editor, widgets); updateWidgets(this.editor, widgets);
updateMiniLocations(this.editor, this.miniLocations); updateMiniLocations(this.editor, this.miniLocations);
replOptions?.afterEval?.(options); replOptions?.afterEval?.(options);
// if no painters are set (.onPaint was not called), then we only need // if no painters are set (.onPaint was not called), then we only need the present moment (for highlighting)
// the present moment (for highlighting)
const drawTime = options.pattern.getPainters().length ? this.drawTime : [0, 0]; const drawTime = options.pattern.getPainters().length ? this.drawTime : [0, 0];
this.drawer.setDrawTime(drawTime); this.drawer.setDrawTime(drawTime);
// invalidate drawer after we've set the appropriate drawTime // invalidate drawer after we've set the appropriate drawTime
@@ -247,33 +245,6 @@ export class StrudelMirror {
} }
}; };
document.addEventListener('start-repl', this.onStartRepl); 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) { draw(haps, time, painters) {
painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime)); painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
@@ -300,16 +271,6 @@ export class StrudelMirror {
async stop() { async stop() {
this.repl.scheduler.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() { async toggle() {
if (this.repl.scheduler.started) { if (this.repl.scheduler.started) {
this.repl.stop(); this.repl.stop();
@@ -385,18 +346,11 @@ export class StrudelMirror {
} }
} }
setCode(code) { setCode(code) {
const changes = { const changes = { from: 0, to: this.editor.state.doc.length, insert: code };
from: 0,
to: this.editor.state.doc.length,
insert: code,
};
this.editor.dispatch({ changes }); this.editor.dispatch({ changes });
} }
clear() { clear() {
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl); 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() { getCursorLocation() {
return this.editor.state.selection.main.head; return this.editor.state.selection.main.head;
+2 -107
View File
@@ -1,12 +1,11 @@
import { defaultKeymap } from '@codemirror/commands';
import { Prec } from '@codemirror/state'; import { Prec } from '@codemirror/state';
import { keymap, ViewPlugin } from '@codemirror/view'; import { keymap, ViewPlugin } from '@codemirror/view';
// import { searchKeymap } from '@codemirror/search'; // import { searchKeymap } from '@codemirror/search';
import { emacs } from '@replit/codemirror-emacs'; import { emacs } from '@replit/codemirror-emacs';
import { vim, Vim } from '@replit/codemirror-vim'; import { vim } from '@replit/codemirror-vim';
// import { vim } from './vim_test.mjs'; // import { vim } from './vim_test.mjs';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap'; import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
import { logger } from '@strudel/core'; import { defaultKeymap } from '@codemirror/commands';
const vscodePlugin = ViewPlugin.fromClass( const vscodePlugin = ViewPlugin.fromClass(
class { class {
@@ -20,110 +19,6 @@ const vscodePlugin = ViewPlugin.fromClass(
); );
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []); 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 = { const keymaps = {
vim, vim,
emacs, emacs,
+6 -53
View File
@@ -1750,64 +1750,17 @@ export const { semitone } = registerControl('semitone');
// TODO: synth param // TODO: synth param
export const { voice } = registerControl('voice'); export const { voice } = registerControl('voice');
// voicings // https://codeberg.org/uzu/strudel/issues/506 // voicings // https://codeberg.org/uzu/strudel/issues/506
/** // chord to voice, like C Eb Fm7 G7. the symbols can be defined via addVoicings
* The chord to voice
* @name chord
* @param {string | Pattern} symbols chord symbols to voice e.g., C, Eb, Fm7, G7. The symbols can be defined via addVoicings
* @example
* chord("<Am C D F Am E Am E>").voicing()
**/
export const { chord } = registerControl('chord'); export const { chord } = registerControl('chord');
/** // which dictionary to use for the voicings
* Which dictionary to use for the voicings. This falls back to the default dictionary if not provided
*
* @name dictionary
* @param {string} dictionaryName which dictionary (having been defined with `addVoicings`) to use
* @example
* addVoicings('house', {
'': ['7 12 16', '0 7 16', '4 7 12'],
'm': ['0 3 7']
})
chord("<Am C D F Am E Am E>")
.dict('house').anchor(66)
.voicing().room(.5)
**/
export const { dictionary, dict } = registerControl('dictionary', 'dict'); export const { dictionary, dict } = registerControl('dictionary', 'dict');
/** The top note to align the voicing to. Defaults to c5 // the top note to align the voicing to, defaults to c5
*
* @name anchor
* @param {string | Pattern} anchorNote the note to align the voicings to
* @example
* anchor("<c4 g4 c5 g5>").chord("C").voicing()
**/
export const { anchor } = registerControl('anchor'); export const { anchor } = registerControl('anchor');
/** // how the voicing is offset from the anchored position
* Sets how the voicing is offset from the anchored position
*
* @name offset
* @param {number | Pattern} shift the amount to shift the voicing up or down
* @example
* chord("<Am C D F Am E Am E>").offset("<0 1 2 3 4 5>") // alter the voicing each time
**/
export const { offset } = registerControl('offset'); export const { offset } = registerControl('offset');
/** // how many octaves are voicing steps spread apart, defaults to 1
* How many octaves are voicing steps spread apart, defaults to 1
*
* @name octaves
* @param {number | Pattern} count the number of octaves
* @example
* chord("<Am C D F Am E Am E>").octaves("<2 4>").voicing()
**/
export const { octaves } = registerControl('octaves'); export const { octaves } = registerControl('octaves');
/** // below = anchor note will be removed from the voicing, useful for melody harmonization
* Remove anchor note from the voicing. Useful for melody harmonization
*
* @name mode
* @param {string | Pattern} modeName one of {below | above | duck | root}
* @example
* mode("<below above duck root>").chord("C").voicing()
*
**/
export const { mode } = registerControl(['mode', 'anchor']); export const { mode } = registerControl(['mode', 'anchor']);
/** /**
+7 -7
View File
@@ -35,18 +35,18 @@ const right = function (n, x) {
return result; return result;
}; };
const _bjorklund = function (n, x) { const _bjork = function (n, x) {
const [ons, offs] = n; const [ons, offs] = n;
return Math.min(ons, offs) <= 1 ? [n, x] : _bjorklund(...(ons > offs ? left(n, x) : right(n, x))); return Math.min(ons, offs) <= 1 ? [n, x] : _bjork(...(ons > offs ? left(n, x) : right(n, x)));
}; };
export const bjorklund = function (ons, steps) { export const bjork = function (ons, steps) {
const inverted = ons < 0; const inverted = ons < 0;
const absOns = Math.abs(ons); const absOns = Math.abs(ons);
const offs = steps - absOns; const offs = steps - absOns;
const ones = Array(absOns).fill([1]); const ones = Array(absOns).fill([1]);
const zeros = Array(offs).fill([0]); const zeros = Array(offs).fill([0]);
const result = _bjorklund([absOns, offs], [ones, zeros]); const result = _bjork([absOns, offs], [ones, zeros]);
const pattern = flatten(result[1][0]).concat(flatten(result[1][1])); const pattern = flatten(result[1][0]).concat(flatten(result[1][1]));
return inverted ? pattern.map((x) => 1 - x) : pattern; return inverted ? pattern.map((x) => 1 - x) : pattern;
}; };
@@ -128,7 +128,7 @@ export const bjorklund = function (ons, steps) {
*/ */
const _euclidRot = function (pulses, steps, rotation) { const _euclidRot = function (pulses, steps, rotation) {
const b = bjorklund(pulses, steps); const b = bjork(pulses, steps);
if (rotation) { if (rotation) {
return rotate(b, -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)); return pat.struct(_euclidRot(pulses, steps, 0));
}); });
export const bjork = register('bjork', function (euc, pat) { export const e = register('e', function (euc, pat) {
if (!Array.isArray(euc)) { if (!Array.isArray(euc)) {
euc = [euc]; euc = [euc];
} }
@@ -216,6 +216,6 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s
* .pan(sine.slow(8)) * .pan(sine.slow(8))
*/ */
export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) { export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) {
const morphed = _morph(bjorklund(pulses, steps), new Array(pulses).fill(1), perc); const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc);
return pat.struct(morphed).setSteps(steps); return pat.struct(morphed).setSteps(steps);
}); });
+1 -1
View File
@@ -2574,8 +2574,8 @@ export const { chunkBack, chunkback } = register(
* @returns Pattern * @returns Pattern
* @example * @example
* "<0 8> 1 2 3 4 5 6 7" * "<0 8> 1 2 3 4 5 6 7"
* .scale("C2:major").note()
* .fastChunk(4, x => x.color('red')).slow(2) * .fastChunk(4, x => x.color('red')).slow(2)
* .scale("C2:major").note()
*/ */
export const { fastchunk, fastChunk } = register( export const { fastchunk, fastChunk } = register(
['fastchunk', 'fastChunk'], ['fastchunk', 'fastChunk'],
+2 -1
View File
@@ -201,7 +201,8 @@ const timeToIntSeed = (x) => xorwise(Math.trunc(_frac(x / 300) * 536870912));
const intSeedToRand = (x) => (x % 536870912) / 536870912; const intSeedToRand = (x) => (x % 536870912) / 536870912;
const timeToRand = (x) => Math.abs(intSeedToRand(timeToIntSeed(x))); // Set first random value to be 0.5, otherwise it would be 0. https://github.com/tidalcycles/strudel/issues/1293
const timeToRand = (x) => (x === 0 ? 0.5 : Math.abs(intSeedToRand(timeToIntSeed(x))));
const timeToRandsPrime = (seed, n) => { const timeToRandsPrime = (seed, n) => {
const result = []; const result = [];
+8 -8
View File
@@ -1,14 +1,14 @@
import { bjorklund } from '../euclid.mjs'; import { bjork } from '../euclid.mjs';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { fastcat } from '../pattern.mjs'; import { fastcat } from '../pattern.mjs';
describe('bjorklund', () => { describe('bjork', () => {
it('should apply bjorklundlund to ons and steps', () => { it('should apply bjorklund to ons and steps', () => {
expect(bjorklund(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]); expect(bjork(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(bjork(-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(bjork(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(bjork(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]);
expect(bjorklund(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]); expect(bjork(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]);
}); });
}); });
+3 -7
View File
@@ -7,8 +7,8 @@ This program is free software: you can redistribute it and/or modify it under th
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
// returns true if the given string is a note // returns true if the given string is a note
export const isNoteWithOctave = (name) => /^[a-gA-G][#bsf]*[0-9]*$/.test(name); 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 isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]?$/.test(name);
export const tokenizeNote = (note) => { export const tokenizeNote = (note) => {
if (typeof note !== 'string') { if (typeof note !== 'string') {
return []; return [];
@@ -23,10 +23,6 @@ export const tokenizeNote = (note) => {
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }; 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 }; 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 // turns the given note into its midi number representation
export const noteToMidi = (note, defaultOctave = 3) => { export const noteToMidi = (note, defaultOctave = 3) => {
const [pc, acc, oct = defaultOctave] = tokenizeNote(note); const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
@@ -34,7 +30,7 @@ export const noteToMidi = (note, defaultOctave = 3) => {
throw new Error('not a note: "' + note + '"'); throw new Error('not a note: "' + note + '"');
} }
const chroma = chromas[pc.toLowerCase()]; const chroma = chromas[pc.toLowerCase()];
const offset = getAccidentalsOffset(acc); const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset; return (Number(oct) + 1) * 12 + chroma + offset;
}; };
export const midiToFreq = (n) => { export const midiToFreq = (n) => {
-6
View File
@@ -40,12 +40,6 @@ const pattern = sequence([
- D-Pad - D-Pad
- `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase) - `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase)
- Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight`(or `tglU`, `tglD`, `tglL`, `tglR`) - Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight`(or `tglU`, `tglD`, `tglL`, `tglR`)
- Stick Buttons
- `l3`, `r3` (or `ls`, `rs`)
- Toggle versions: `tglL3`, `tglR3` (or `tglLS`, `tglRS`)
- System Buttons
- `start`, `back` (or uppercase `START`, `BACK`)
- Toggle versions: `tglStart`, `tglBack` (or `tglSTART`, `tglBACK`)
### Analog Sticks ### Analog Sticks
- Left Stick - Left Stick
-4
View File
@@ -29,10 +29,6 @@ The gamepad module provides access to buttons and analog sticks as normalized si
| | Toggle versions: `tglLB`, `tglRB`, `tglLT`, `tglRT` | | | Toggle versions: `tglLB`, `tglRB`, `tglLT`, `tglRT` |
| D-Pad | `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase) | | D-Pad | `up`, `down`, `left`, `right` (or `u`, `d`, `l`, `r` or uppercase) |
| | Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight` (or `tglU`, `tglD`, `tglL`, `tglR`) | | | Toggle versions: `tglUp`, `tglDown`, `tglLeft`, `tglRight` (or `tglU`, `tglD`, `tglL`, `tglR`) |
| Stick Buttons | `l3`, 'r3' (or `ls`, `rs`) |
| | Toggle versions: `tglL3`, 'tglR3' (or `tglLs`, `tglRs`) |
| System Buttons | `start`, `back` (or uppercase `START`, `BACK`) |
| | Toggle versions: `tglStart`, `tglBack` (or `tglSTART`, `tglBACK`) |
### Analog Sticks ### Analog Sticks
-4
View File
@@ -14,10 +14,6 @@ export const buttonMap = {
rt: 7, rt: 7,
back: 8, back: 8,
start: 9, start: 9,
l3: 10,
ls: 10,
r3: 11,
rs: 11,
u: 12, u: 12,
up: 12, up: 12,
d: 13, d: 13,
+42 -59
View File
@@ -5,10 +5,9 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
import * as _WebMidi from 'webmidi'; import * as _WebMidi from 'webmidi';
import { Pattern, isPattern, logger, ref } from '@strudel/core'; import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
import { noteToMidi, getControlName } from '@strudel/core'; import { noteToMidi, getControlName } from '@strudel/core';
import { Note } from 'webmidi'; import { Note } from 'webmidi';
import { scheduleAtTime } from '../superdough/helpers.mjs';
// if you use WebMidi from outside of this package, make sure to import that instance: // if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi; export const { WebMidi } = _WebMidi;
@@ -191,7 +190,7 @@ function mapCC(mapping, value) {
} }
// sends a cc message to the given device on the given channel // sends a cc message to the given device on the given channel
function sendCC(ccn, ccv, device, midichan, targetTime) { function sendCC(ccn, ccv, device, midichan, timeOffsetString) {
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) { if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
throw new Error('expected ccv to be a number between 0 and 1'); throw new Error('expected ccv to be a number between 0 and 1');
} }
@@ -199,23 +198,19 @@ function sendCC(ccn, ccv, device, midichan, targetTime) {
throw new Error('expected ccn to be a number or a string'); throw new Error('expected ccn to be a number or a string');
} }
const scaled = Math.round(ccv * 127); const scaled = Math.round(ccv * 127);
scheduleAtTime(() => { device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
device.sendControlChange(ccn, scaled, midichan);
}, targetTime);
} }
// sends a program change message to the given device on the given channel // sends a program change message to the given device on the given channel
function sendProgramChange(progNum, device, midichan, targetTime) { function sendProgramChange(progNum, device, midichan, timeOffsetString) {
if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) { if (typeof progNum !== 'number' || progNum < 0 || progNum > 127) {
throw new Error('expected progNum (program change) to be a number between 0 and 127'); throw new Error('expected progNum (program change) to be a number between 0 and 127');
} }
scheduleAtTime(() => { device.sendProgramChange(progNum, midichan, { time: timeOffsetString });
device.sendProgramChange(progNum, midichan);
}, targetTime);
} }
// sends a sysex message to the given device on the given channel // sends a sysex message to the given device on the given channel
function sendSysex(sysexid, sysexdata, device, targetTime) { function sendSysex(sysexid, sysexdata, device, timeOffsetString) {
if (Array.isArray(sysexid)) { if (Array.isArray(sysexid)) {
if (!sysexid.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { if (!sysexid.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
throw new Error('all sysexid bytes must be integers between 0 and 255'); throw new Error('all sysexid bytes must be integers between 0 and 255');
@@ -230,13 +225,11 @@ function sendSysex(sysexid, sysexdata, device, targetTime) {
if (!sysexdata.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { if (!sysexdata.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
throw new Error('all sysex bytes must be integers between 0 and 255'); throw new Error('all sysex bytes must be integers between 0 and 255');
} }
scheduleAtTime(() => { device.sendSysex(sysexid, sysexdata, { time: timeOffsetString });
device.sendSysex(sysexid, sysexdata);
}, targetTime);
} }
// sends a NRPN message to the given device on the given channel // sends a NRPN message to the given device on the given channel
function sendNRPN(nrpnn, nrpv, device, midichan, targetTime) { function sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString) {
if (Array.isArray(nrpnn)) { if (Array.isArray(nrpnn)) {
if (!nrpnn.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { if (!nrpnn.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
throw new Error('all nrpnn bytes must be integers between 0 and 255'); throw new Error('all nrpnn bytes must be integers between 0 and 255');
@@ -244,34 +237,28 @@ function sendNRPN(nrpnn, nrpv, device, midichan, targetTime) {
} else if (!Number.isInteger(nrpv) || nrpv < 0 || nrpv > 255) { } else if (!Number.isInteger(nrpv) || nrpv < 0 || nrpv > 255) {
throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers'); throw new Error('A:sysexid must be an number between 0 and 255 or an array of such integers');
} }
scheduleAtTime(() => {
device.sendNRPN(nrpnn, nrpv, midichan); device.sendNRPN(nrpnn, nrpv, midichan, { time: timeOffsetString });
}, targetTime);
} }
// sends a pitch bend message to the given device on the given channel // sends a pitch bend message to the given device on the given channel
function sendPitchBend(midibend, device, midichan, targetTime) { function sendPitchBend(midibend, device, midichan, timeOffsetString) {
if (typeof midibend !== 'number' || midibend < -1 || midibend > 1) { if (typeof midibend !== 'number' || midibend < -1 || midibend > 1) {
throw new Error('expected midibend to be a number between -1 and 1'); throw new Error('expected midibend to be a number between -1 and 1');
} }
scheduleAtTime(() => { device.sendPitchBend(midibend, midichan, { time: timeOffsetString });
device.sendPitchBend(midibend, midichan);
}, targetTime);
} }
// sends a channel aftertouch message to the given device on the given channel // sends a channel aftertouch message to the given device on the given channel
function sendAftertouch(miditouch, device, midichan, targetTime) { function sendAftertouch(miditouch, device, midichan, timeOffsetString) {
if (typeof miditouch !== 'number' || miditouch < 0 || miditouch > 1) { if (typeof miditouch !== 'number' || miditouch < 0 || miditouch > 1) {
throw new Error('expected miditouch to be a number between 0 and 1'); throw new Error('expected miditouch to be a number between 0 and 1');
} }
device.sendChannelAftertouch(miditouch, midichan, { time: timeOffsetString });
scheduleAtTime(() => {
device.sendChannelAftertouch(miditouch, midichan);
}, targetTime);
} }
// sends a note message to the given device on the given channel // sends a note message to the given device on the given channel
function sendNote(note, velocity, duration, device, midichan, targetTime) { function sendNote(note, velocity, duration, device, midichan, timeOffsetString) {
if (note == null || note === '') { if (note == null || note === '') {
throw new Error('note cannot be null or empty'); throw new Error('note cannot be null or empty');
} }
@@ -281,12 +268,12 @@ function sendNote(note, velocity, duration, device, midichan, targetTime) {
if (duration != null && (typeof duration !== 'number' || duration < 0)) { if (duration != null && (typeof duration !== 'number' || duration < 0)) {
throw new Error('duration must be a positive number'); throw new Error('duration must be a positive number');
} }
const midiNumber = typeof note === 'number' ? note : noteToMidi(note); const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
const midiNote = new Note(midiNumber, { attack: velocity, duration }); const midiNote = new Note(midiNumber, { attack: velocity, duration });
device.playNote(midiNote, midichan, {
scheduleAtTime(() => { time: timeOffsetString,
device.playNote(midiNote, midichan); });
}, targetTime);
} }
/** /**
@@ -322,6 +309,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
let midiConfig = { let midiConfig = {
// Default configuration values // Default configuration values
isController: false, // Disable sending notes for midi controllers isController: false, // Disable sending notes for midi controllers
latencyMs: 34, // Default latency to get audio engine to line up in ms
noteOffsetMs: 10, // Default note-off offset to prevent glitching in ms noteOffsetMs: 10, // Default note-off offset to prevent glitching in ms
midichannel: 1, // Default MIDI channel midichannel: 1, // Default MIDI channel
velocity: 0.9, // Default velocity velocity: 0.9, // Default velocity
@@ -345,13 +333,18 @@ Pattern.prototype.midi = function (midiport, options = {}) {
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`), logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
}); });
return this.onTrigger((hap, _currentTime, cps, targetTime) => { return this.onTrigger((hap, currentTime, cps, targetTime) => {
if (!WebMidi.enabled) { if (!WebMidi.enabled) {
logger('Midi not enabled'); logger('Midi not enabled');
return; return;
} }
hap.ensureObjectValue(); hap.ensureObjectValue();
//magic number to get audio engine to line up, can probably be calculated somehow
const latencyMs = midiConfig.latencyMs;
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
const timeOffsetString = `+${getEventOffsetMs(targetTime, currentTime) + latencyMs}`;
// midi event values from hap with configurable defaults // midi event values from hap with configurable defaults
let { let {
note, note,
@@ -387,7 +380,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
// if midimap is set, send a cc messages from defined controls // if midimap is set, send a cc messages from defined controls
if (midicontrolMap.has(midimap)) { if (midicontrolMap.has(midimap)) {
const ccs = mapCC(midicontrolMap.get(midimap), hap.value); const ccs = mapCC(midicontrolMap.get(midimap), hap.value);
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, targetTime)); ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString));
} else if (midimap !== 'default') { } else if (midimap !== 'default') {
// Add warning when a non-existent midimap is specified // Add warning when a non-existent midimap is specified
logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`); logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`);
@@ -399,12 +392,12 @@ Pattern.prototype.midi = function (midiport, options = {}) {
// try to prevent glitching by subtracting noteOffsetMs from the duration length // try to prevent glitching by subtracting noteOffsetMs from the duration length
const duration = (hap.duration.valueOf() / cps) * 1000 - midiConfig.noteOffsetMs; const duration = (hap.duration.valueOf() / cps) * 1000 - midiConfig.noteOffsetMs;
sendNote(note, velocity, duration, device, midichan, targetTime); sendNote(note, velocity, duration, device, midichan, timeOffsetString);
} }
// Handle program change // Handle program change
if (progNum !== undefined) { if (progNum !== undefined) {
sendProgramChange(progNum, device, midichan, targetTime); sendProgramChange(progNum, device, midichan, timeOffsetString);
} }
// Handle sysex // Handle sysex
@@ -414,63 +407,53 @@ Pattern.prototype.midi = function (midiport, options = {}) {
// if sysexid is an array the first byte is 0x00 // if sysexid is an array the first byte is 0x00
if (sysexid !== undefined && sysexdata !== undefined) { if (sysexid !== undefined && sysexdata !== undefined) {
sendSysex(sysexid, sysexdata, device, targetTime); sendSysex(sysexid, sysexdata, device, timeOffsetString);
} }
// Handle control change // Handle control change
if (ccv !== undefined && ccn !== undefined) { if (ccv !== undefined && ccn !== undefined) {
sendCC(ccn, ccv, device, midichan, targetTime); sendCC(ccn, ccv, device, midichan, timeOffsetString);
} }
// Handle NRPN non-registered parameter number // Handle NRPN non-registered parameter number
if (nrpnn !== undefined && nrpv !== undefined) { if (nrpnn !== undefined && nrpv !== undefined) {
sendNRPN(nrpnn, nrpv, device, midichan, targetTime); sendNRPN(nrpnn, nrpv, device, midichan, timeOffsetString);
} }
// Handle midibend // Handle midibend
if (midibend !== undefined) { if (midibend !== undefined) {
sendPitchBend(midibend, device, midichan, targetTime); sendPitchBend(midibend, device, midichan, timeOffsetString);
} }
// Handle miditouch // Handle miditouch
if (miditouch !== undefined) { if (miditouch !== undefined) {
sendAftertouch(miditouch, device, midichan, targetTime); sendAftertouch(miditouch, device, midichan, timeOffsetString);
} }
// Handle midicmd // Handle midicmd
if (hap.whole.begin + 0 === 0) { if (hap.whole.begin + 0 === 0) {
// we need to start here because we have the timing info // we need to start here because we have the timing info
scheduleAtTime(() => { device.sendStart({ time: timeOffsetString });
device.sendStart();
}, targetTime);
} }
if (['clock', 'midiClock'].includes(midicmd)) { if (['clock', 'midiClock'].includes(midicmd)) {
scheduleAtTime(() => { device.sendClock({ time: timeOffsetString });
device.sendClock();
}, targetTime);
} else if (['start'].includes(midicmd)) { } else if (['start'].includes(midicmd)) {
scheduleAtTime(() => { device.sendStart({ time: timeOffsetString });
device.sendStart();
}, targetTime);
} else if (['stop'].includes(midicmd)) { } else if (['stop'].includes(midicmd)) {
scheduleAtTime(() => { device.sendStop({ time: timeOffsetString });
device.sendStop();
}, targetTime);
} else if (['continue'].includes(midicmd)) { } else if (['continue'].includes(midicmd)) {
scheduleAtTime(() => { device.sendContinue({ time: timeOffsetString });
device.sendContinue();
}, targetTime);
} else if (Array.isArray(midicmd)) { } else if (Array.isArray(midicmd)) {
if (midicmd[0] === 'progNum') { if (midicmd[0] === 'progNum') {
sendProgramChange(midicmd[1], device, midichan, targetTime); sendProgramChange(midicmd[1], device, midichan, timeOffsetString);
} else if (midicmd[0] === 'cc') { } else if (midicmd[0] === 'cc') {
if (midicmd.length === 2) { if (midicmd.length === 2) {
sendCC(midicmd[0], midicmd[1] / 127, device, midichan, targetTime); sendCC(midicmd[0], midicmd[1] / 127, device, midichan, timeOffsetString);
} }
} else if (midicmd[0] === 'sysex') { } else if (midicmd[0] === 'sysex') {
if (midicmd.length === 3) { if (midicmd.length === 3) {
const [_, id, data] = midicmd; const [_, id, data] = midicmd;
sendSysex(id, data, device, targetTime); sendSysex(id, data, device, timeOffsetString);
} }
} }
} }
+5 -8
View File
@@ -21,14 +21,13 @@ export class MondoParser {
close_curly: /^\}/, close_curly: /^\}/,
number: /^-?[0-9]*\.?[0-9]+/, // before pipe! number: /^-?[0-9]*\.?[0-9]+/, // before pipe!
// TODO: better error handling when "-" is used as rest, e.g "s [- bd]" // TODO: better error handling when "-" is used as rest, e.g "s [- bd]"
op: /^[*/:!@%?+\-&]|^\.{2}/, // * / : ! @ % ? .. op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? ..
// dollar: /^\$/, // dollar: /^\$/,
pipe: /^#/, pipe: /^#/,
stack: /^[,$]/, stack: /^[,$]/,
or: /^[|]/, or: /^[|]/,
plain: /^[a-zA-Z0-9-~_^#]+/, plain: /^[a-zA-Z0-9-~_^#]+/,
}; };
op_precedence = [['*', '/', ':', '!', '@', '%', '?', '+', '-', '..'], ['&']];
// matches next token // matches next token
next_token(code, offset = 0) { next_token(code, offset = 0) {
for (let type in this.token_types) { for (let type in this.token_types) {
@@ -151,9 +150,9 @@ export class MondoParser {
} }
return children; return children;
} }
desugar_ops(children, types) { desugar_ops(children) {
while (true) { while (true) {
let opIndex = children.findIndex((child) => child.type === 'op' && types.includes(child.value)); let opIndex = children.findIndex((child) => child.type === 'op');
if (opIndex === -1) break; if (opIndex === -1) break;
const op = { type: 'plain', value: children[opIndex].value }; const op = { type: 'plain', value: children[opIndex].value };
if (opIndex === children.length - 1) { if (opIndex === children.length - 1) {
@@ -264,10 +263,8 @@ export class MondoParser {
// the type we've removed before splitting needs to be added back // the type we've removed before splitting needs to be added back
children = [{ type: 'plain', value: type }, ...children]; children = [{ type: 'plain', value: type }, ...children];
} }
// for each precendence group, call desugar_ops once children = this.desugar_ops(children);
this.op_precedence.forEach((ops) => { // children = this.desugar_pipes(children, (children) => this.desugar_dollars(children));
children = this.desugar_ops(children, ops);
});
children = this.desugar_pipes(children); children = this.desugar_pipes(children);
return children; return children;
}), }),
-1
View File
@@ -117,7 +117,6 @@ describe('mondo sugar', () => {
it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)')); 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: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*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 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', () => expect(desguar('x $ y')).toEqual('(x y)'));
it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))')); it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))'));
-2
View File
@@ -11,7 +11,6 @@ import {
chooseIn, chooseIn,
degradeBy, degradeBy,
silence, silence,
bjork,
} from '@strudel/core'; } from '@strudel/core';
import { registerLanguage } from '@strudel/transpiler'; import { registerLanguage } from '@strudel/transpiler';
import { MondoRunner } from 'mondolang'; import { MondoRunner } from 'mondolang';
@@ -41,7 +40,6 @@ lib['!'] = replicate;
lib['@'] = expand; lib['@'] = expand;
lib['%'] = pace; lib['%'] = pace;
lib['?'] = degradeBy; // todo: default 0.5 not working.. lib['?'] = degradeBy; // todo: default 0.5 not working..
lib['&'] = bjork;
lib[':'] = tail; lib[':'] = tail;
lib['..'] = range; lib['..'] = range;
lib['def'] = () => silence; lib['def'] = () => silence;
-5
View File
@@ -280,11 +280,6 @@ export function getVibratoOscillator(param, value, t) {
return vibratoOscillator; return vibratoOscillator;
} }
} }
export function scheduleAtTime(callback, targetTime, audioContext = getAudioContext()) {
const currentTime = audioContext.currentTime;
webAudioTimeout(audioContext, callback, currentTime, targetTime);
}
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities // ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
// a bit of a hack, but it works very well :) // a bit of a hack, but it works very well :)
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
+1 -5
View File
@@ -16,17 +16,13 @@ export const tokenizeNote = (note) => {
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }; 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 }; 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) => { export const noteToMidi = (note, defaultOctave = 3) => {
const [pc, acc, oct = defaultOctave] = tokenizeNote(note); const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
if (!pc) { if (!pc) {
throw new Error('not a note: "' + note + '"'); throw new Error('not a note: "' + note + '"');
} }
const chroma = chromas[pc.toLowerCase()]; const chroma = chromas[pc.toLowerCase()];
const offset = getAccidentalsOffset(acc); const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset; return (Number(oct) + 1) * 12 + chroma + offset;
}; };
export const midiToFreq = (n) => { export const midiToFreq = (n) => {
+7
View File
@@ -61,6 +61,13 @@ describe('tonal', () => {
.firstCycleValues.map((h) => h.note), .firstCycleValues.map((h) => h.note),
).toEqual(['B2', 'Eb3', 'A2', 'G3', 'F3']); ).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', () => { it('snaps notes (upwards) to scale', () => {
const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb']; const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb'];
const expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3']; const expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3'];
+46 -45
View File
@@ -5,8 +5,9 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
import { Note, Interval, Scale } from '@tonaljs/tonal'; import { Note, Interval, Scale } from '@tonaljs/tonal';
import { register, _mod, logger, isNote, noteToMidi, removeUndefineds, getAccidentalsOffset } from '@strudel/core'; import { register, _mod, silence, logger, pure, isNote } from '@strudel/core';
import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs'; import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs';
import { noteToMidi } from '../core/util.mjs';
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P'; const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
@@ -184,15 +185,17 @@ function _convertStepToNumberAndOffset(step) {
step = String(step); step = String(step);
// Check to see if the step matches the expected format: // Check to see if the step matches the expected format:
// - A number (possibly negative) // - A number (possibly negative)
// - Some number of sharps or flats // - Some number of sharps or flats (but not both)
const match = /^(-?\d+)([#bsf]*)$/.exec(step); const match = /^(-?\d+)(#+|b+)?$/.exec(step);
if (!match) { if (!match) {
throw new Error(`invalid scale step "${step}", expected number or integer with optional # b suffixes`); throw new Error(`invalid scale step "${step}", expected number or integer with optional # b suffixes`);
} }
asNumber = Number(match[1]); asNumber = Number(match[1]);
const accidentals = match[2] || ''; // These decorations will determine the semitone offset based on the number of
offset = getAccidentalsOffset(accidentals); // sharps or flats
const decorations = match[2] || '';
offset = decorations[0] === '#' ? decorations.length : -decorations.length;
} }
return [asNumber, offset]; return [asNumber, offset];
} }
@@ -223,7 +226,7 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) {
* Turns numbers into notes in the scale (zero indexed) or quantizes notes to a scale. * 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 * When describing notes via numbers, note that negative numbers can be used to wrap backwards
* in the scale as well as sharps or flats to produce notes outside of the scale. * in the scale as well as sharps or flats (but not both) to produce notes outside of the scale.
* *
* Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}. * Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
* *
@@ -251,6 +254,7 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) {
* @example * @example
* note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3) * note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3)
*/ */
export const scale = register( export const scale = register(
'scale', 'scale',
function (scale, pat) { function (scale, pat) {
@@ -258,47 +262,44 @@ export const scale = register(
if (Array.isArray(scale)) { if (Array.isArray(scale)) {
scale = scale.flat().join(' '); scale = scale.flat().join(' ');
} }
return pat.withHaps((haps) => { return (
haps = haps.map((hap) => { pat
let hVal = hap.value; .fmap((value) => {
const isObject = typeof hVal === 'object'; const isObject = typeof value === 'object';
// If hVal is a pure value, place it on `n` so that we interpret it as a scale degree // The case where the note has been defined via `n` or `pure`
hVal = isObject ? hVal : { n: hVal }; if (!isObject || (isObject && ('n' in value || 'value' in value))) {
const { note, n, value, ...otherValues } = hVal; const step = isObject ? (value.n ?? value.value) : value;
const noteOrStep = note ?? n ?? value; delete value.n; // remove n so it won't cause trouble
if (noteOrStep === undefined) { if (isNote(step)) {
logger( // legacy..
`[tonal] Invalid value format for 'scale'. Value must contain n, note, or value but received keys [${Object.keys(hVal).join(', ')}]`, return pure(step);
'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);
} }
if (offset != 0) scaleNote = Note.transpose(scaleNote, Interval.fromSemitones(offset)); try {
} catch (err) { const [number, offset] = _convertStepToNumberAndOffset(step);
logger(`[tonal] ${err.message}`, 'error'); let note;
return; // will be removed 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;
} }
} // The case where the note has been defined via `note`
hap.value = isObject ? { ...otherValues, note: scaleNote } : scaleNote; else {
// Tag with scale for downsteam scale-aware operations const note = _getNearestScaleNote(scale, value.note);
return hap.setContext({ ...hap.context, scale }); return pure(isObject ? { ...value, note } : note);
}); }
return removeUndefineds(haps); })
}); .outerJoin()
// legacy:
.withHap((hap) => hap.setContext({ ...hap.context, scale }))
);
}, },
true, true,
true, // preserve step count true, // preserve step count
+1
View File
@@ -41,6 +41,7 @@ Tune.prototype.tonicize = function(newTonic) {
this.tonic = newTonic this.tonic = newTonic
} }
/* Return data in the mode you are in (freq, ratio, or midi) */ /* Return data in the mode you are in (freq, ratio, or midi) */
Tune.prototype.note = function(input,octave){ Tune.prototype.note = function(input,octave){
+20 -143
View File
@@ -840,31 +840,6 @@ exports[`runs examples > example "amp" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "anchor" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:E2 ]",
"[ 0/1 → 1/1 | note:C3 ]",
"[ 0/1 → 1/1 | note:E3 ]",
"[ 0/1 → 1/1 | note:G3 ]",
"[ 0/1 → 1/1 | note:C4 ]",
"[ 1/1 → 2/1 | note:C3 ]",
"[ 1/1 → 2/1 | note:G3 ]",
"[ 1/1 → 2/1 | note:C4 ]",
"[ 1/1 → 2/1 | note:E4 ]",
"[ 1/1 → 2/1 | note:G4 ]",
"[ 2/1 → 3/1 | note:E3 ]",
"[ 2/1 → 3/1 | note:C4 ]",
"[ 2/1 → 3/1 | note:E4 ]",
"[ 2/1 → 3/1 | note:G4 ]",
"[ 2/1 → 3/1 | note:C5 ]",
"[ 3/1 → 4/1 | note:C4 ]",
"[ 3/1 → 4/1 | note:G4 ]",
"[ 3/1 → 4/1 | note:C5 ]",
"[ 3/1 → 4/1 | note:E5 ]",
"[ 3/1 → 4/1 | note:G5 ]",
]
`;
exports[`runs examples > example "apply" example index 0 1`] = ` exports[`runs examples > example "apply" example index 0 1`] = `
[ [
"[ 0/1 → 1/1 | note:C3 ]", "[ 0/1 → 1/1 | note:C3 ]",
@@ -1078,10 +1053,10 @@ exports[`runs examples > example "begin" example index 0 1`] = `
exports[`runs examples > example "berlin" example index 0 1`] = ` exports[`runs examples > example "berlin" example index 0 1`] = `
[ [
"[ 0/1 → 1/16 | note:D3 ]", "[ 0/1 → 1/16 | note:A3 ]",
"[ 1/16 → 1/8 | note:E3 ]", "[ 1/16 → 1/8 | note:Bb3 ]",
"[ 1/8 → 3/16 | note:F3 ]", "[ 1/8 → 3/16 | note:C4 ]",
"[ 3/16 → 1/4 | note:G3 ]", "[ 3/16 → 1/4 | note:D4 ]",
"[ 1/4 → 5/16 | note:A3 ]", "[ 1/4 → 5/16 | note:A3 ]",
"[ 5/16 → 3/8 | note:C4 ]", "[ 5/16 → 3/8 | note:C4 ]",
"[ 3/8 → 7/16 | note:D4 ]", "[ 3/8 → 7/16 | note:D4 ]",
@@ -1872,31 +1847,6 @@ exports[`runs examples > example "chop" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "chord" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:A3 ]",
"[ 0/1 → 1/1 | note:C4 ]",
"[ 0/1 → 1/1 | note:E4 ]",
"[ 0/1 → 1/1 | note:A4 ]",
"[ 0/1 → 1/1 | note:C5 ]",
"[ 1/1 → 2/1 | note:E3 ]",
"[ 1/1 → 2/1 | note:C4 ]",
"[ 1/1 → 2/1 | note:E4 ]",
"[ 1/1 → 2/1 | note:G4 ]",
"[ 1/1 → 2/1 | note:C5 ]",
"[ 2/1 → 3/1 | note:D3 ]",
"[ 2/1 → 3/1 | note:A3 ]",
"[ 2/1 → 3/1 | note:D4 ]",
"[ 2/1 → 3/1 | note:Gb4 ]",
"[ 2/1 → 3/1 | note:A4 ]",
"[ 3/1 → 4/1 | note:F3 ]",
"[ 3/1 → 4/1 | note:C4 ]",
"[ 3/1 → 4/1 | note:F4 ]",
"[ 3/1 → 4/1 | note:A4 ]",
"[ 3/1 → 4/1 | note:C5 ]",
]
`;
exports[`runs examples > example "chorus" example index 0 1`] = ` exports[`runs examples > example "chorus" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | note:d s:sawtooth chorus:0.5 ]", "[ 0/1 → 1/4 | note:d s:sawtooth chorus:0.5 ]",
@@ -2789,23 +2739,6 @@ exports[`runs examples > example "detune" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "dictionary" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:A3 room:0.5 ]",
"[ 0/1 → 1/1 | note:C4 room:0.5 ]",
"[ 0/1 → 1/1 | note:E4 room:0.5 ]",
"[ 1/1 → 2/1 | note:G3 room:0.5 ]",
"[ 1/1 → 2/1 | note:C4 room:0.5 ]",
"[ 1/1 → 2/1 | note:E4 room:0.5 ]",
"[ 2/1 → 3/1 | note:A3 room:0.5 ]",
"[ 2/1 → 3/1 | note:D4 room:0.5 ]",
"[ 2/1 → 3/1 | note:Gb4 room:0.5 ]",
"[ 3/1 → 4/1 | note:A3 room:0.5 ]",
"[ 3/1 → 4/1 | note:C4 room:0.5 ]",
"[ 3/1 → 4/1 | note:F4 room:0.5 ]",
]
`;
exports[`runs examples > example "distort" example index 0 1`] = ` exports[`runs examples > example "distort" example index 0 1`] = `
[ [
"[ 0/1 → 1/8 | s:hh distort:0 ]", "[ 0/1 → 1/8 | s:hh distort:0 ]",
@@ -3905,8 +3838,8 @@ exports[`runs examples > example "fast" example index 0 1`] = `
exports[`runs examples > example "fastChunk" example index 0 1`] = ` exports[`runs examples > example "fastChunk" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | note:C2 color:red ]", "[ 0/1 → 1/4 | color:red note:0 ]",
"[ 1/4 → 1/2 | note:D2 color:red ]", "[ 1/4 → 1/2 | color:red note:1 ]",
"[ 1/2 → 3/4 | note:E2 ]", "[ 1/2 → 3/4 | note:E2 ]",
"[ 3/4 → 1/1 | note:F2 ]", "[ 3/4 → 1/1 | note:F2 ]",
"[ 1/1 → 5/4 | note:G2 ]", "[ 1/1 → 5/4 | note:G2 ]",
@@ -3915,8 +3848,8 @@ exports[`runs examples > example "fastChunk" example index 0 1`] = `
"[ 7/4 → 2/1 | note:C3 ]", "[ 7/4 → 2/1 | note:C3 ]",
"[ 2/1 → 9/4 | note:D3 ]", "[ 2/1 → 9/4 | note:D3 ]",
"[ 9/4 → 5/2 | note:D2 ]", "[ 9/4 → 5/2 | note:D2 ]",
"[ 5/2 → 11/4 | note:E2 color:red ]", "[ 5/2 → 11/4 | color:red note:2 ]",
"[ 11/4 → 3/1 | note:F2 color:red ]", "[ 11/4 → 3/1 | color:red note:3 ]",
"[ 3/1 → 13/4 | note:G2 ]", "[ 3/1 → 13/4 | note:G2 ]",
"[ 13/4 → 7/2 | note:A2 ]", "[ 13/4 → 7/2 | note:A2 ]",
"[ 7/2 → 15/4 | note:B2 ]", "[ 7/2 → 15/4 | note:B2 ]",
@@ -6563,28 +6496,6 @@ exports[`runs examples > example "miditouch" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "mode" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:E3 ]",
"[ 0/1 → 1/1 | note:C4 ]",
"[ 0/1 → 1/1 | note:E4 ]",
"[ 0/1 → 1/1 | note:G4 ]",
"[ 0/1 → 1/1 | note:C5 ]",
"[ 1/1 → 2/1 | note:C5 ]",
"[ 1/1 → 2/1 | note:G5 ]",
"[ 1/1 → 2/1 | note:C6 ]",
"[ 1/1 → 2/1 | note:E6 ]",
"[ 2/1 → 3/1 | note:E3 ]",
"[ 2/1 → 3/1 | note:C4 ]",
"[ 2/1 → 3/1 | note:E4 ]",
"[ 2/1 → 3/1 | note:G4 ]",
"[ 3/1 → 4/1 | note:C5 ]",
"[ 3/1 → 4/1 | note:G5 ]",
"[ 3/1 → 4/1 | note:C6 ]",
"[ 3/1 → 4/1 | note:E6 ]",
]
`;
exports[`runs examples > example "morph" example index 0 1`] = ` exports[`runs examples > example "morph" example index 0 1`] = `
[ [
"[ 0/1 → 1/8 | s:hh ]", "[ 0/1 → 1/8 | s:hh ]",
@@ -6903,31 +6814,6 @@ exports[`runs examples > example "octave" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "octaves" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:A3 ]",
"[ 0/1 → 1/1 | note:C4 ]",
"[ 0/1 → 1/1 | note:E4 ]",
"[ 0/1 → 1/1 | note:A4 ]",
"[ 0/1 → 1/1 | note:C5 ]",
"[ 1/1 → 2/1 | note:E3 ]",
"[ 1/1 → 2/1 | note:C4 ]",
"[ 1/1 → 2/1 | note:E4 ]",
"[ 1/1 → 2/1 | note:G4 ]",
"[ 1/1 → 2/1 | note:C5 ]",
"[ 2/1 → 3/1 | note:D3 ]",
"[ 2/1 → 3/1 | note:A3 ]",
"[ 2/1 → 3/1 | note:D4 ]",
"[ 2/1 → 3/1 | note:Gb4 ]",
"[ 2/1 → 3/1 | note:A4 ]",
"[ 3/1 → 4/1 | note:F3 ]",
"[ 3/1 → 4/1 | note:C4 ]",
"[ 3/1 → 4/1 | note:F4 ]",
"[ 3/1 → 4/1 | note:A4 ]",
"[ 3/1 → 4/1 | note:C5 ]",
]
`;
exports[`runs examples > example "off" example index 0 1`] = ` exports[`runs examples > example "off" example index 0 1`] = `
[ [
"[ -5/24 ⇜ (0/1 → 1/8) | note:62 ]", "[ -5/24 ⇜ (0/1 → 1/8) | note:62 ]",
@@ -6961,15 +6847,6 @@ exports[`runs examples > example "off" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "offset" example index 0 1`] = `
[
"[ 0/1 → 1/1 | chord:Am offset:0 ]",
"[ 1/1 → 2/1 | chord:C offset:1 ]",
"[ 2/1 → 3/1 | chord:D offset:2 ]",
"[ 3/1 → 4/1 | chord:F offset:3 ]",
]
`;
exports[`runs examples > example "often" example index 0 1`] = ` exports[`runs examples > example "often" example index 0 1`] = `
[ [
"[ 0/1 → 1/8 | s:hh speed:0.5 ]", "[ 0/1 → 1/8 | s:hh speed:0.5 ]",
@@ -7273,18 +7150,18 @@ exports[`runs examples > example "penv" example index 0 1`] = `
exports[`runs examples > example "perlin" example index 0 1`] = ` exports[`runs examples > example "perlin" example index 0 1`] = `
[ [
"[ 0/1 → 1/8 | s:hh cutoff:500 ]", "[ 0/1 → 1/8 | s:hh cutoff:4250 ]",
"[ 0/1 → 1/4 | s:bd cutoff:500 ]", "[ 0/1 → 1/4 | s:bd cutoff:4250 ]",
"[ 1/8 → 1/4 | s:hh cutoff:562.5486401770559 ]", "[ 1/8 → 1/4 | s:hh cutoff:4252.352717325493 ]",
"[ 1/4 → 3/8 | s:hh cutoff:903.3554895067937 ]", "[ 1/4 → 3/8 | s:hh cutoff:4265.171895756794 ]",
"[ 1/4 → 1/2 | s:bd cutoff:903.3554895067937 ]", "[ 1/4 → 1/2 | s:bd cutoff:4265.171895756794 ]",
"[ 3/8 → 1/2 | s:hh cutoff:1572.364329119182 ]", "[ 3/8 → 1/2 | s:hh cutoff:4290.3361308769945 ]",
"[ 1/2 → 5/8 | s:hh cutoff:2448.2831191271544 ]", "[ 1/2 → 5/8 | s:hh cutoff:4323.283119127154 ]",
"[ 1/2 → 3/4 | s:bd cutoff:2448.2831191271544 ]", "[ 1/2 → 3/4 | s:bd cutoff:4323.283119127154 ]",
"[ 5/8 → 3/4 | s:hh cutoff:3324.2019091351267 ]", "[ 5/8 → 3/4 | s:hh cutoff:4356.230107377314 ]",
"[ 3/4 → 7/8 | s:hh cutoff:3993.210748747515 ]", "[ 3/4 → 7/8 | s:hh cutoff:4381.394342497515 ]",
"[ 3/4 → 1/1 | s:bd cutoff:3993.210748747515 ]", "[ 3/4 → 1/1 | s:bd cutoff:4381.394342497515 ]",
"[ 7/8 → 1/1 | s:hh cutoff:4334.017598077253 ]", "[ 7/8 → 1/1 | s:hh cutoff:4394.213520928815 ]",
"[ 1/1 → 9/8 | s:hh cutoff:4396.566238254309 ]", "[ 1/1 → 9/8 | s:hh cutoff:4396.566238254309 ]",
"[ 1/1 → 5/4 | s:bd cutoff:4396.566238254309 ]", "[ 1/1 → 5/4 | s:bd cutoff:4396.566238254309 ]",
"[ 9/8 → 5/4 | s:hh cutoff:4449.536839055554 ]", "[ 9/8 → 5/4 | s:hh cutoff:4449.536839055554 ]",
File diff suppressed because it is too large Load Diff
@@ -117,10 +117,6 @@ Here's an example AST for `c3 [e3 g3]`
which translates to `seq(c3, seq(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 ## Scheduling Events
After an instance of `Pattern` is obtained from the user code, After an instance of `Pattern` is obtained from the user code,
@@ -1,37 +0,0 @@
---
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.
@@ -79,11 +79,13 @@ const updateCodeWindow = (context, patternData, reset = false) => {
context.handleUpdate(patternData, reset); context.handleUpdate(patternData, reset);
}; };
const autoResetPatternOnChange = !isUdels();
function UserPatterns({ context }) { function UserPatterns({ context }) {
const activePattern = useActivePattern(); const activePattern = useActivePattern();
const viewingPatternStore = useViewingPatternData(); const viewingPatternStore = useViewingPatternData();
const viewingPatternData = parseJSON(viewingPatternStore); const viewingPatternData = parseJSON(viewingPatternStore);
const { userPatterns, patternFilter, patternAutoStart } = useSettings(); const { userPatterns, patternFilter } = useSettings();
const viewingPatternID = viewingPatternData?.id; const viewingPatternID = viewingPatternData?.id;
return ( return (
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 "> <div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
@@ -133,13 +135,13 @@ function UserPatterns({ context }) {
<div className="overflow-auto h-full bg-background p-2 rounded-md"> <div className="overflow-auto h-full bg-background p-2 rounded-md">
{/* {patternFilter === patternFilterName.user && ( */} {/* {patternFilter === patternFilterName.user && ( */}
<PatternButtons <PatternButtons
onClick={(id) => { onClick={(id) =>
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart); updateCodeWindow(
context,
if (context.started && activePattern === id) { { ...userPatterns[id], collection: userPattern.collection },
context.handleEvaluate(); autoResetPatternOnChange,
} )
}} }
patterns={userPatterns} patterns={userPatterns}
started={context.started} started={context.started}
activePattern={activePattern} activePattern={activePattern}
@@ -186,14 +188,17 @@ function FeaturedPatterns({ context }) {
const examplePatterns = useExamplePatterns(); const examplePatterns = useExamplePatterns();
const collections = examplePatterns.collections; const collections = examplePatterns.collections;
const patterns = collections.get(patternFilterName.featured); const patterns = collections.get(patternFilterName.featured);
const { patternAutoStart } = useSettings();
return ( return (
<PatternPageWithPagination <PatternPageWithPagination
patterns={patterns} patterns={patterns}
context={context} context={context}
initialPage={featuredPageNum} initialPage={featuredPageNum}
patternOnClick={(id) => { patternOnClick={(id) => {
updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.featured }, patternAutoStart); updateCodeWindow(
context,
{ ...patterns[id], collection: patternFilterName.featured },
autoResetPatternOnChange,
);
}} }}
paginationOnChange={async (pageNum) => { paginationOnChange={async (pageNum) => {
await loadAndSetFeaturedPatterns(pageNum - 1); await loadAndSetFeaturedPatterns(pageNum - 1);
@@ -208,14 +213,13 @@ function LatestPatterns({ context }) {
const examplePatterns = useExamplePatterns(); const examplePatterns = useExamplePatterns();
const collections = examplePatterns.collections; const collections = examplePatterns.collections;
const patterns = collections.get(patternFilterName.public); const patterns = collections.get(patternFilterName.public);
const { patternAutoStart } = useSettings();
return ( return (
<PatternPageWithPagination <PatternPageWithPagination
patterns={patterns} patterns={patterns}
context={context} context={context}
initialPage={latestPageNum} initialPage={latestPageNum}
patternOnClick={(id) => { patternOnClick={(id) => {
updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.public }, patternAutoStart); updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.public }, autoResetPatternOnChange);
}} }}
paginationOnChange={async (pageNum) => { paginationOnChange={async (pageNum) => {
await loadAndSetPublicPatterns(pageNum - 1); await loadAndSetPublicPatterns(pageNum - 1);
@@ -112,7 +112,6 @@ export function SettingsTab({ started }) {
multiChannelOrbits, multiChannelOrbits,
isTabIndentationEnabled, isTabIndentationEnabled,
isMultiCursorEnabled, isMultiCursorEnabled,
patternAutoStart,
} = useSettings(); } = useSettings();
const shouldAlwaysSync = isUdels(); const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
@@ -305,11 +304,6 @@ export function SettingsTab({ started }) {
onChange={(cbEvent) => settingsMap.setKey('isCSSAnimationDisabled', cbEvent.target.checked)} onChange={(cbEvent) => settingsMap.setKey('isCSSAnimationDisabled', cbEvent.target.checked)}
value={isCSSAnimationDisabled} value={isCSSAnimationDisabled}
/> />
<Checkbox
label="Auto-start pattern on pattern change"
onChange={(cbEvent) => settingsMap.setKey('patternAutoStart', cbEvent.target.checked)}
value={patternAutoStart}
/>
</FormItem> </FormItem>
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem> <FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
<FormItem label="Reset Settings"> <FormItem label="Reset Settings">
@@ -62,9 +62,11 @@ export function SoundsTab() {
// stop current sound on mouseup // stop current sound on mouseup
useEvent('mouseup', () => { useEvent('mouseup', () => {
const ref = trigRef.current; const t = trigRef.current;
trigRef.current = undefined; trigRef.current = undefined;
ref?.stop?.(getAudioContext().currentTime + 0.01); t?.then((ref) => {
ref?.stop(getAudioContext().currentTime + 0.01);
});
}); });
return ( return (
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground"> <div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground">
@@ -122,19 +124,12 @@ export function SoundsTab() {
duration: 0.5, duration: 0.5,
}; };
soundPreviewIdx++; soundPreviewIdx++;
const time = ctx.currentTime + 0.05;
const onended = () => trigRef.current?.node?.disconnect(); const onended = () => trigRef.current?.node?.disconnect();
try { trigRef.current = Promise.resolve(onTrigger(time, params, onended));
// Pre-load the sample by calling onTrigger with a future time trigRef.current.then((ref) => {
// This triggers the loading but schedules playback for later connectToDestination(ref?.node);
const time = ctx.currentTime + 0.5; // Give 500ms for loading });
const ref = await onTrigger(time, params, onended);
trigRef.current = ref;
if (ref?.node) {
connectToDestination(ref.node);
}
} catch (err) {
console.warn('Failed to trigger sound:', err);
}
}} }}
> >
{' '} {' '}
+5 -1
View File
@@ -92,7 +92,11 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
async function blobToDataUrl(blob) { async function blobToDataUrl(blob) {
return new Promise((resolve) => { return new Promise((resolve) => {
resolve(URL.createObjectURL(blob)); var reader = new FileReader();
reader.onload = function (event) {
resolve(event.target.result);
};
reader.readAsDataURL(blob);
}); });
} }
-5
View File
@@ -96,11 +96,6 @@ export function useSettings() {
isPanelOpen: parseBoolean(state.isPanelOpen), isPanelOpen: parseBoolean(state.isPanelOpen),
userPatterns: userPatterns, userPatterns: userPatterns,
multiChannelOrbits: parseBoolean(state.multiChannelOrbits), multiChannelOrbits: parseBoolean(state.multiChannelOrbits),
patternAutoStart: isUdels()
? false
: state.patternAutoStart === undefined
? true
: parseBoolean(state.patternAutoStart),
}; };
} }