Compare commits

..

6 Commits

59 changed files with 342 additions and 2113 deletions
-37
View File
@@ -1,37 +0,0 @@
name: Build and Deploy to beta (warm.strudel.cc)
on: [workflow_dispatch]
# Allow one concurrent deployment
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
build:
runs-on: docker
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9.12.2
- uses: actions/setup-node@v4
with:
node-version: 20
# cache: "pnpm"
- name: Install Dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Deploy
run: |
eval $(ssh-agent -s)
echo "$SSH_PRIVATE_KEY" | ssh-add -
apt update && apt install -y rsync
mkdir ~/.ssh
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/warm.strudel.cc
+2 -2
View File
@@ -1,4 +1,4 @@
name: Build and Deploy to live (strudel.cc)
name: Build and Deploy
on: [workflow_dispatch]
@@ -34,4 +34,4 @@ jobs:
apt update && apt install -y rsync
mkdir ~/.ssh
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy
-14
View File
@@ -45,20 +45,6 @@ tidal-drum-machines
webaudiofontdata
src-tauri/target
### BEGIN Visual Studio Code ###
# Blanket, recursive exclude for .vscode directory and files
.vscode/**/*
# Unexclude specific files and directories within .vscode
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
### END Visual Studio Code ###
# BEGIN JetBrains -> END JetBrains
# for JetBrains IDE users, e.g. WebStorm. Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+12 -340
View File
@@ -1,9 +1,6 @@
import jsdoc from '../../doc.json';
import { autocompletion } from '@codemirror/autocomplete';
import { h } from './html';
import { Scale } from '@tonaljs/tonal';
import { soundMap } from 'superdough';
import { complex } from '@strudel/tonal';
const escapeHtml = (str) => {
const div = document.createElement('div');
@@ -78,51 +75,6 @@ const isValidDoc = (doc) => {
const hasExcludedTags = (doc) =>
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
export function bankCompletions() {
const soundDict = soundMap.get();
const banks = new Set();
for (const key of Object.keys(soundDict)) {
const [bank, suffix] = key.split('_');
if (suffix && bank) banks.add(bank);
}
return Array.from(banks)
.sort()
.map((name) => ({ label: name, type: 'bank' }));
}
// Attempt to get all scale names from Tonal
let scaleCompletions = [];
try {
scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' }));
} catch (e) {
console.warn('[autocomplete] Could not load scale names from Tonal:', e);
}
// Valid mode values for voicing
const modeCompletions = [
{ label: 'below', type: 'mode' },
{ label: 'above', type: 'mode' },
{ label: 'duck', type: 'mode' },
{ label: 'root', type: 'mode' },
];
// Valid chord symbols from ireal dictionary plus empty string for major triads
const chordSymbols = ['', ...Object.keys(complex)].sort();
const chordSymbolCompletions = chordSymbols.map((symbol) => {
if (symbol === '') {
return {
label: 'major',
apply: '',
type: 'chord-symbol',
};
}
return {
label: symbol,
apply: symbol,
type: 'chord-symbol',
};
});
export const getSynonymDoc = (doc, synonym) => {
const synonyms = doc.synonyms || [];
const docLabel = getDocLabel(doc);
@@ -161,299 +113,19 @@ const jsdocCompletions = (() => {
return completions;
})();
// --- Handler functions for each context ---
const pitchNames = [
'C',
'C#',
'Db',
'D',
'D#',
'Eb',
'E',
'E#',
'Fb',
'F',
'F#',
'Gb',
'G',
'G#',
'Ab',
'A',
'A#',
'Bb',
'B',
'B#',
'Cb',
];
// Cached regex patterns for scaleHandler
const SCALE_NO_QUOTES_REGEX = /scale\(\s*$/;
const SCALE_AFTER_COLON_REGEX = /scale\(\s*['"][^'"]*:[^'"]*$/;
const SCALE_PRE_COLON_REGEX = /scale\(\s*['"][^'"]*$/;
const SCALE_PITCH_MATCH_REGEX = /([A-Ga-g][#b]*)?$/;
const SCALE_SPACES_TO_COLON_REGEX = /\s+/g;
function scaleHandler(context) {
// First check for scale context without quotes - block with empty completions
let scaleNoQuotesContext = context.matchBefore(SCALE_NO_QUOTES_REGEX);
if (scaleNoQuotesContext) {
return {
from: scaleNoQuotesContext.to,
options: [],
};
}
// Check for after-colon context first (more specific)
let scaleAfterColonContext = context.matchBefore(SCALE_AFTER_COLON_REGEX);
if (scaleAfterColonContext) {
const text = scaleAfterColonContext.text;
const colonIdx = text.lastIndexOf(':');
if (colonIdx !== -1) {
const fragment = text.slice(colonIdx + 1);
const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment));
const options = filteredScales.map((s) => ({
...s,
apply: s.label.replace(SCALE_SPACES_TO_COLON_REGEX, ':'),
}));
const from = scaleAfterColonContext.from + colonIdx + 1;
return {
from,
options,
};
}
}
// Then check for pre-colon context
let scalePreColonContext = context.matchBefore(SCALE_PRE_COLON_REGEX);
if (scalePreColonContext) {
if (!scalePreColonContext.text.includes(':')) {
if (context.explicit) {
const text = scalePreColonContext.text;
const match = text.match(SCALE_PITCH_MATCH_REGEX);
const fragment = match ? match[0] : '';
const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
const from = scalePreColonContext.to - fragment.length;
const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
return { from, options };
} else {
return { from: scalePreColonContext.to, options: [] };
}
}
}
return null;
}
// Cached regex patterns for soundHandler
const SOUND_NO_QUOTES_REGEX = /(s|sound)\(\s*$/;
const SOUND_WITH_QUOTES_REGEX = /(s|sound)\(\s*['"][^'"]*$/;
const SOUND_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w]*)$/;
function soundHandler(context) {
// First check for sound context without quotes - block with empty completions
let soundNoQuotesContext = context.matchBefore(SOUND_NO_QUOTES_REGEX);
if (soundNoQuotesContext) {
return {
from: soundNoQuotesContext.to,
options: [],
};
}
// Then check for sound context with quotes - provide completions
let soundContext = context.matchBefore(SOUND_WITH_QUOTES_REGEX);
if (!soundContext) return null;
const text = soundContext.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
const fragMatch = inside.match(SOUND_FRAGMENT_MATCH_REGEX);
const fragment = fragMatch ? fragMatch[1] : inside;
const soundNames = Object.keys(soundMap.get()).sort();
const filteredSounds = soundNames.filter((name) => name.includes(fragment));
let options = filteredSounds.map((name) => ({ label: name, type: 'sound' }));
const from = soundContext.to - fragment.length;
return {
from,
options,
};
}
// Cached regex patterns for bankHandler
const BANK_NO_QUOTES_REGEX = /bank\(\s*$/;
const BANK_WITH_QUOTES_REGEX = /bank\(\s*['"][^'"]*$/;
function bankHandler(context) {
// First check for bank context without quotes - block with empty completions
let bankNoQuotesContext = context.matchBefore(BANK_NO_QUOTES_REGEX);
if (bankNoQuotesContext) {
return {
from: bankNoQuotesContext.to,
options: [],
};
}
// Then check for bank context with quotes - provide completions
let bankMatch = context.matchBefore(BANK_WITH_QUOTES_REGEX);
if (!bankMatch) return null;
const text = bankMatch.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
const fragment = inside;
let banks = bankCompletions();
const filteredBanks = banks.filter((b) => b.label.startsWith(fragment));
const from = bankMatch.to - fragment.length;
return {
from,
options: filteredBanks,
};
}
// Cached regex patterns for modeHandler
const MODE_NO_QUOTES_REGEX = /mode\(\s*$/;
const MODE_AFTER_COLON_REGEX = /mode\(\s*['"][^'"]*:[^'"]*$/;
const MODE_PRE_COLON_REGEX = /mode\(\s*['"][^'"]*$/;
const MODE_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w:]*)$/;
function modeHandler(context) {
// First check for mode context without quotes - block with empty completions
let modeNoQuotesContext = context.matchBefore(MODE_NO_QUOTES_REGEX);
if (modeNoQuotesContext) {
return {
from: modeNoQuotesContext.to,
options: [],
};
}
// Check for after-colon context first (more specific)
let modeAfterColonContext = context.matchBefore(MODE_AFTER_COLON_REGEX);
if (modeAfterColonContext) {
const text = modeAfterColonContext.text;
const colonIdx = text.lastIndexOf(':');
if (colonIdx !== -1) {
const fragment = text.slice(colonIdx + 1);
// For anchor after colon, we can suggest pitch names
const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
const from = modeAfterColonContext.from + colonIdx + 1;
return {
from,
options,
};
}
}
// Then check for pre-colon context
let modeContext = context.matchBefore(MODE_PRE_COLON_REGEX);
if (!modeContext) return null;
const text = modeContext.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
const fragMatch = inside.match(MODE_FRAGMENT_MATCH_REGEX);
const fragment = fragMatch ? fragMatch[1] : inside;
const filteredModes = modeCompletions.filter((m) => m.label.startsWith(fragment));
const from = modeContext.to - fragment.length;
return {
from,
options: filteredModes,
};
}
// Cached regex patterns for chordHandler
const CHORD_NO_QUOTES_REGEX = /chord\(\s*$/;
const CHORD_WITH_QUOTES_REGEX = /chord\(\s*['"][^'"]*$/;
const CHORD_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w#b+^:-]*)$/;
function chordHandler(context) {
// First check for chord context without quotes - block with empty completions
let chordNoQuotesContext = context.matchBefore(CHORD_NO_QUOTES_REGEX);
if (chordNoQuotesContext) {
return {
from: chordNoQuotesContext.to,
options: [],
};
}
// Then check for chord context with quotes - provide completions
let chordContext = context.matchBefore(CHORD_WITH_QUOTES_REGEX);
if (!chordContext) return null;
const text = chordContext.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
// Use same fragment matching as sound/mode for expressions like "<G Am>"
const fragMatch = inside.match(CHORD_FRAGMENT_MATCH_REGEX);
const fragment = fragMatch ? fragMatch[1] : inside;
// Check if fragment contains any pitch name at start (for root + symbol)
let rootMatch = null;
let symbolFragment = fragment;
for (const pitch of pitchNames) {
if (fragment.toLowerCase().startsWith(pitch.toLowerCase())) {
rootMatch = pitch;
symbolFragment = fragment.slice(pitch.length);
break;
}
}
if (rootMatch) {
// We have a root, now complete chord symbols
const filteredSymbols = chordSymbolCompletions.filter((s) =>
s.label.toLowerCase().startsWith(symbolFragment.toLowerCase()),
);
// Create completions that replace the entire chord, not just the symbol part
const options = filteredSymbols;
const from = chordContext.to - symbolFragment.length;
return { from, options };
} else {
// No root yet, complete with pitch names
const filteredPitches = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
const options = filteredPitches.map((p) => ({ label: p, type: 'pitch' }));
const from = chordContext.to - fragment.length;
return { from, options };
}
}
// Cached regex patterns for fallbackHandler
const FALLBACK_WORD_REGEX = /\w*/;
function fallbackHandler(context) {
const word = context.matchBefore(FALLBACK_WORD_REGEX);
if (word && word.from === word.to && !context.explicit) return null;
if (word) {
return {
from: word.from,
options: jsdocCompletions,
};
}
return null;
}
const handlers = [
soundHandler,
bankHandler,
chordHandler,
scaleHandler,
modeHandler,
// this handler *must* be last
fallbackHandler,
];
export const strudelAutocomplete = (context) => {
for (const handler of handlers) {
const result = handler(context);
if (result) {
return result;
}
}
return null;
const word = context.matchBefore(/\w*/);
if (word.from === word.to && !context.explicit) return null;
return {
from: word.from,
options: jsdocCompletions,
/* options: [
{ label: 'match', type: 'keyword' },
{ label: 'hello', type: 'variable', info: '(World)' },
{ label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
], */
};
};
export const isAutoCompletionEnabled = (enabled) =>
+24 -70
View File
@@ -1,31 +1,30 @@
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 { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
import { Compartment, EditorState, Prec } from '@codemirror/state';
import {
drawSelection,
EditorView,
highlightActiveLine,
highlightActiveLineGutter,
highlightActiveLine,
keymap,
lineNumbers,
drawSelection,
} from '@codemirror/view';
import { persistentAtom } from '@nanostores/persistent';
import { logger, registerControl, repl } from '@strudel/core';
import { cleanupDraw, Drawer } from '@strudel/draw';
import { repl, registerControl } from '@strudel/core';
import { Drawer, cleanupDraw } from '@strudel/draw';
import { isAutoCompletionEnabled } from './autocomplete.mjs';
import { basicSetup } from './basicSetup.mjs';
import { isTooltipEnabled } from './tooltip.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 { 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';
import { widgetPlugin, updateWidgets } from './widget.mjs';
import { persistentAtom } from '@nanostores/persistent';
import { basicSetup } from './basicSetup.mjs';
const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
@@ -95,8 +94,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 }),
@@ -120,13 +119,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?.()),
}, */
]),
),
],
@@ -207,8 +206,7 @@ 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
@@ -247,33 +245,6 @@ 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));
@@ -300,16 +271,6 @@ 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();
@@ -385,18 +346,11 @@ 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;
+2 -107
View File
@@ -1,12 +1,11 @@
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, Vim } from '@replit/codemirror-vim';
import { vim } from '@replit/codemirror-vim';
// import { vim } from './vim_test.mjs';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
import { logger } from '@strudel/core';
import { defaultKeymap } from '@codemirror/commands';
const vscodePlugin = ViewPlugin.fromClass(
class {
@@ -20,110 +19,6 @@ 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,
+2 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
"version": "1.2.6",
"version": "1.2.5",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
@@ -46,11 +46,8 @@
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@tonaljs/tonal": "^4.10.0",
"nanostores": "^0.11.3",
"superdough": "workspace:*"
"nanostores": "^0.11.3"
},
"devDependencies": {
"vite": "^6.0.11"
+12 -13
View File
@@ -2321,23 +2321,22 @@ export const { miditouch } = registerControl('miditouch');
export const { polyTouch } = registerControl('polyTouch');
/**
* The host to send open sound control messages to. Requires running the OSC bridge.
* @name oschost
* @param {string | Pattern} oschost e.g. 'localhost'
* @example
* note("c4").oschost('127.0.0.1').oscport(57120).osc();
* Checks if a control name exists in the controlAlias map.
* @name hasControlName
* @param {string} alias The control name to check
* @returns {boolean} True if the control name exists, false otherwise
*/
export const { oschost } = registerControl('oschost');
export const hasControlName = (alias) => {
// Check if the name exists as a key (alias) or value (main control name)
return controlAlias.has(alias) || Array.from(controlAlias.values()).includes(alias);
};
/**
* The port to send open sound control messages to. Requires running the OSC bridge.
* @name oscport
* @param {number | Pattern} oscport e.g. 57120
* @example
* note("c4").oschost('127.0.0.1').oscport(57120).osc();
* Gets the control name from the controlAlias map.
* @name getControlName
* @param {string} alias The control name to get
* @returns {string} The control name
*/
export const { oscport } = registerControl('oscport');
export const getControlName = (alias) => {
if (controlAlias.has(alias)) {
return controlAlias.get(alias);
+7 -7
View File
@@ -35,18 +35,18 @@ const right = function (n, x) {
return result;
};
const _bjorklund = function (n, x) {
const _bjork = function (n, x) {
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 absOns = Math.abs(ons);
const offs = steps - absOns;
const ones = Array(absOns).fill([1]);
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]));
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 b = bjorklund(pulses, steps);
const b = bjork(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 bjork = register('bjork', function (euc, pat) {
export const e = register('e', 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(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);
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/core",
"version": "1.2.5",
"version": "1.2.4",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
+1 -19
View File
@@ -2574,8 +2574,8 @@ export const { chunkBack, chunkback } = register(
* @returns Pattern
* @example
* "<0 8> 1 2 3 4 5 6 7"
* .scale("C2:major").note()
* .fastChunk(4, x => x.color('red')).slow(2)
* .scale("C2:major").note()
*/
export const { fastchunk, fastChunk } = register(
['fastchunk', 'fastChunk'],
@@ -2998,24 +2998,6 @@ export const extend = stepRegister('extend', function (factor, pat) {
return pat.fast(factor).expand(factor);
});
/**
* *Experimental*
*
* `replicate` is similar to `fast` in that it increases its density, but it also increases the step count
* accordingly. So `stepcat("a b".replicate(2), "c d")` would be the same as `"a b a b c d"`, whereas
* `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`.
*
* TODO: find out how this function differs from extend
* @example
* stepcat(
* sound("bd bd - cp").replicate(2),
* sound("bd - sd -")
* ).pace(8)
*/
export const replicate = stepRegister('replicate', function (factor, pat) {
return pat.repeatCycles(factor).fast(factor).expand(factor);
});
/**
* *Experimental*
*
+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 { fastcat } from '../pattern.mjs';
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]);
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]);
});
});
+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';
// returns true if the given string is a note
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 isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[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,10 +23,6 @@ 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);
@@ -34,7 +30,7 @@ export const noteToMidi = (note, defaultOctave = 3) => {
throw new Error('not a note: "' + note + '"');
}
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;
};
export const midiToFreq = (n) => {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
"version": "1.2.6",
"version": "1.2.5",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
-9
View File
@@ -84,18 +84,9 @@ Pattern.prototype.onPaint = function (painter) {
state.controls.painters = [];
}
state.controls.painters.push(painter);
return state;
});
};
// TODO - Why isn't this pure deep copy not working?
// Pattern.prototype.onPaint = function (painter) {
// return this.withState((state) => {
// const painters = state.controls.painters ? [...state.controls.painters, painter] : [painter];
// return new State(state.span, { ...state.controls, painters });
// });
// };
Pattern.prototype.getPainters = function () {
let painters = [];
this.queryArc(0, 0, { painters });
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/draw",
"version": "1.2.5",
"version": "1.2.4",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
+1 -33
View File
@@ -14,12 +14,7 @@ npm i @strudel/gamepad --save
import { gamepad } from '@strudel/gamepad';
// Initialize gamepad (optional index parameter, defaults to 0)
const pad = gamepad(0); // Default mapping is XBOX {a: 0, b: 1, x: 2, y: 3}
//const pad = gamepad(0, 'XBOX'); // XBOX button mapping {a: 0, b: 1, x: 2, y: 3}
//const pad = gamepad(0, 'NES'); // Nintendo button mapping {a: 1, b: 0, x: 3, y: 2}
//const pad = gamepad(0, {a: 0, b: 1, x: 2, y: 3}); // Custom mapping
const pad = gamepad(0);
// Use gamepad inputs in patterns
const pattern = sequence([
@@ -91,35 +86,8 @@ $: sound("hadoken").gain(pad.checkSequence(HADOKEN))
## Multiple Gamepads
You can connect multiple gamepads by specifying the gamepad index:
Make sure to press buttons on all connected gamepads before hitting play, so the browser can properly detect them.
```javascript
const pad1 = gamepad(0); // First gamepad
const pad2 = gamepad(1); // Second gamepad
```
## Vibration
You can use the `vibrate` control to provide haptic feedback to the gamepad.
The `vibrate` control is a patternable control, so you can use it to control the vibration of the gamepad.
The vibrate control has several parameters:
- `vibGamepadIndex` - Index of the gamepad to vibrate (0-3)
- `vibStrong` - Intensity of the strong/left motor (0-1)
- `vibWeak` - Intensity of the weak/right motor (0-1)
- `vibDuration` - Duration of vibration in milliseconds
- `vibEnable` - Enable/disable vibration (0 or 1)
```javascript
// Vibration with gamepad
// vibrate(gamepadId, { strong: 1, weak: 0.5, duration: 100 })
// vibrate("gamepadId")
// vibrate(gamepadId)
$: note("c4*4").vibrate(0, { strong: 1, weak: 0.5, duration: 100 }) // gamepad index 0
$: note("c4*4").vibrate("0:1:0.5:200") // Array format
$: note("c4*4").vibrate("<0 1 0 1>") // select gamepad index
$: note("c4*4").vibStrong("<0.1 0.2 0.5 1>").vibWeak("<0.1 0.2 0.5 1>").vibDuration("<10 20 50 100>").vibrate(0) // gamepad index 0
```
+1 -24
View File
@@ -107,32 +107,9 @@ $: s("free_hadouken -").slow(2)
samples({free_hadouken: 'https://cdn.freesound.org/previews/67/67674_111920-lq.mp3'})
`} />
### Button Sequences
### Button Mappings
The gamepad module supports different button mapping configurations to accommodate various gamepad layouts:
- **XBOX** (Default): Standard Xbox-style mapping where A=0, B=1, X=2, Y=3
- **NES**: Nintendo-style mapping where B=0, A=1, Y=2, X=3
- **Custom**: Define your own button mapping by passing an object with button assignments
You can specify the mapping when initializing the gamepad:
<MiniRepl
client:idle
tune={`
const pad1 = gamepad(0); // Default mapping is XBOX {a: 0, b: 1, x: 2, y: 3}
const pad2 = gamepad(1, 'XBOX'); // XBOX button mapping {a: 0, b: 1, x: 2, y: 3}
const pad3 = gamepad(2, 'NES'); // Nintendo button mapping {a: 1, b: 0, x: 3, y: 2}
const pad4 = gamepad(3, {a: 0, b: 1, x: 2, y: 3}); // Custom mapping
`}
/>
### Multiple Gamepads
## Multiple Gamepads
Strudel supports multiple gamepads. You can specify the gamepad index to connect to different devices.
Make sure to press buttons on all connected gamepads before hitting play, so the browser can properly detect them.
<MiniRepl
client:idle
+32 -147
View File
@@ -1,77 +1,35 @@
// @strudel/gamepad/index.mjs
import { signal, Pattern, logger, registerControl, register, isPattern } from '@strudel/core';
import { vibrationSupported, getGamepadVibrationActuator } from './vibration.mjs';
import { signal } from '@strudel/core';
// Button mapping for Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)
export const buttonMapSettings = {
XBOX: {
// XBOX mapping default
a: 0,
b: 1,
x: 2,
y: 3,
lb: 4,
rb: 5,
lt: 6,
rt: 7,
back: 8,
start: 9,
lstick: 10,
rstick: 11,
ls: 10,
rs: 11,
u: 12,
up: 12,
d: 13,
down: 13,
l: 14,
left: 14,
r: 15,
right: 15,
xbox: 16,
},
NES: {
// Nintendo mapping
a: 1,
b: 0,
x: 3,
y: 2,
lb: 4,
rb: 5,
zl: 6,
zr: 7,
lt: 6,
rt: 7,
back: 8,
minus: 8,
start: 9,
plus: 9,
lstick: 10,
rstick: 11,
ls: 10,
rs: 11,
u: 12,
up: 12,
d: 13,
down: 13,
l: 14,
left: 14,
r: 15,
right: 15,
home: 16,
select: 17,
},
export const buttonMap = {
a: 0,
b: 1,
x: 2,
y: 3,
lb: 4,
rb: 5,
lt: 6,
rt: 7,
back: 8,
start: 9,
u: 12,
up: 12,
d: 13,
down: 13,
l: 14,
left: 14,
r: 15,
right: 15,
};
class ButtonSequenceDetector {
constructor(timeWindow = 1000, mapping) {
constructor(timeWindow = 1000) {
this.sequence = [];
this.timeWindow = timeWindow;
this.lastInputTime = 0;
this.buttonStates = Array(Object.keys(mapping).length).fill(0); // Track previous state of each button
this.buttonMap = mapping;
this.buttonStates = Array(16).fill(0); // Track previous state of each button
// Button mapping for character inputs
}
@@ -86,8 +44,7 @@ class ButtonSequenceDetector {
}
// Store the button name instead of index
const buttonName =
Object.keys(this.buttonMap).find((key) => this.buttonMap[key] === buttonIndex) || buttonIndex.toString();
const buttonName = Object.keys(buttonMap).find((key) => buttonMap[key] === buttonIndex) || buttonIndex.toString();
this.sequence.push({
input: buttonName,
@@ -130,9 +87,9 @@ class ButtonSequenceDetector {
// Check if either the input matches directly or they refer to the same button in the map
return (
input === target ||
this.buttonMap[input] === this.buttonMap[target] ||
buttonMap[input] === buttonMap[target] ||
// Also check if the numerical index matches
this.buttonMap[input] === parseInt(target)
buttonMap[input] === parseInt(target)
);
})
? 1
@@ -141,13 +98,12 @@ class ButtonSequenceDetector {
}
class GamepadHandler {
constructor(index = 0, mapping) {
constructor(index = 0) {
// Add index parameter
this._gamepads = {};
this._mapping = mapping;
this._activeGamepad = index; // Use provided index
this._axes = [0, 0, 0, 0];
this._buttons = Array(Object.keys(mapping).length).fill(0);
this._buttons = Array(16).fill(0);
this.setupEventListeners();
}
@@ -187,66 +143,12 @@ class GamepadHandler {
}
}
// Add utility function to list all connected gamepads
export const listGamepads = () => {
const gamepads = navigator.getGamepads();
const connectedGamepads = Array.from(gamepads)
.filter((gp) => gp !== null)
.map((gp) => ({
index: gp.index,
id: gp.id,
mapping: gp.mapping,
buttons: gp.buttons.length,
axes: gp.axes.length,
connected: gp.connected,
timestamp: gp.timestamp,
}));
// Format the gamepads info into a readable string
const gamepadsInfo = connectedGamepads.map((gp) => `${gp.index}: ${gp.id}`).join('\n');
logger(`[gamepad] available gamepads:\n${gamepadsInfo}`);
return connectedGamepads;
};
// Module-level state store for toggle states
const gamepadStates = new Map();
export const gamepad = (index = 0, mapping = 'XBOX') => {
// list connected gamepads
const connectedGamepads = listGamepads();
// Check if the requested gamepad index exists
const requestedGamepad = connectedGamepads.find((gp) => gp.index === index);
if (!requestedGamepad) {
throw new Error(
`[gamepad] gamepad at index ${index} not found. available gamepads: ${connectedGamepads.map((gp) => gp.index).join(', ')}`,
);
}
// Handle button mapping
let buttonMap = buttonMapSettings.XBOX;
if (typeof mapping === 'string') {
buttonMap = buttonMapSettings[mapping.toUpperCase()];
} else if (typeof mapping === 'object') {
buttonMap = { ...buttonMapSettings.XBOX, ...mapping };
// Check that all mapping values are valid button indices
const maxButtons = requestedGamepad.buttons;
for (const [key, value] of Object.entries(mapping)) {
if (typeof value !== 'number' || value < 0 || value >= maxButtons) {
throw new Error(
`[gamepad] invalid button mapping for '${key}': ${value}. Must be a number between 0 and ${maxButtons - 1}`,
);
}
}
}
if (!buttonMap) {
throw new Error(`[gamepad] button mapping '${mapping}' not found`);
}
const handler = new GamepadHandler(index, buttonMap);
const sequenceDetector = new ButtonSequenceDetector(2000, buttonMap);
export const gamepad = (index = 0) => {
const handler = new GamepadHandler(index);
const sequenceDetector = new ButtonSequenceDetector(2000);
// Base signal that polls gamepad state and handles sequence detection
const baseSignal = signal((t) => {
@@ -277,7 +179,7 @@ export const gamepad = (index = 0, mapping = 'XBOX') => {
axes.y2_2 = axes.y2.toBipolar();
// Create button patterns
const buttons = Array(Object.keys(buttonMap).length)
const buttons = Array(16)
.fill(null)
.map((_, i) => {
// Create unique key for this gamepad+button combination
@@ -316,22 +218,8 @@ export const gamepad = (index = 0, mapping = 'XBOX') => {
return baseSignal.fmap(() => sequenceDetector.checkSequence(sequence));
};
const checkSequence = btnSequence;
const sequence = btnSequence;
const btnSeq = btnSequence;
const btnseq = btnSequence;
const seq = btnSequence;
// Create vibration pattern
const vibration = (pattern) => {
return signal(() => {
// Return vibration enable/disable pattern
return pattern;
});
};
logger(
`[gamepad] connected to gamepad ${index} (${requestedGamepad.id}) with ${typeof mapping === 'object' ? 'custom' : mapping} mapping${vibrationSupported(index) ? ' (vibration supported)' : ''}`,
);
const btnseq = btnSeq;
// Return an object with all controls
return {
@@ -346,12 +234,9 @@ export const gamepad = (index = 0, mapping = 'XBOX') => {
]),
),
checkSequence,
sequence,
btnSequence,
btnSeq,
btnseq,
seq,
vibration,
raw: baseSignal,
};
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/gamepad",
"version": "1.2.5",
"version": "1.2.4",
"description": "Gamepad Inputs for strudel",
"main": "index.mjs",
"type": "module",
-288
View File
@@ -1,288 +0,0 @@
// Gamepad Vibration Controls
import { registerControl, register, logger, isPattern } from '@strudel/core';
import { Pattern } from '@strudel/core';
/**
* Controls the strong motor intensity for gamepad vibration.
*
* @name vibStrong
* @param {number | Pattern} intensity between 0 and 1
* @synonyms vibS
* @example
* s("bd hh sd hh").vibStrong("1 0 0.5 0").vibrate(0)
*/
export const { vibStrong, vibS } = registerControl('vibStrong', 'vibS');
/**
* Controls the weak motor intensity for gamepad vibration.
*
* @name vibWeak
* @param {number | Pattern} intensity between 0 and 1
* @synonyms vibW
* @example
* s("bd hh sd hh").vibWeak("0.5 0 0.8 0").vibrate(0)
*/
export const { vibWeak, vibW } = registerControl('vibWeak', 'vibW');
/**
* Controls the duration of gamepad vibration in milliseconds.
*
* @name vibDuration
* @param {number | Pattern} duration in milliseconds
* @synonyms vibDur
* @example
* s("bd hh sd hh").vibDuration("200 0 100 0").vibrate(0)
*/
export const { vibDuration, vibDur } = registerControl('vibDuration', 'vibDur');
/**
* Enables or disables gamepad vibration for individual events.
*
* @name vibEnable
* @param {number | Pattern} enable 1 to enable, 0 to disable
* @synonyms vibE
* @example
* s("bd hh sd hh").vibEnable("1 0 1 0").vibrate(0)
*/
export const { vibEnable, vibEn } = registerControl('vibEnable', 'vibEn');
/**
* Sets the gamepad index for vibration events.
*
* @name vibGamepadIndex
* @param {number | Pattern} index gamepad index (0-3)
* @synonyms vibGpId, vibGamepadIndex, vibGamepadId
* @example
* s("bd hh sd hh").vibGamepadIndex("0 1 0 1").vibrate()
*/
export const { vibGamepadIndex, vibGpId, vibGamepadId } = registerControl('vibGamepadIndex', 'vibGpId', 'vibGamepadId');
/**
* Sets both strong and weak vibration intensity at once.
*
* @name vibIntensity
* @param {number | Pattern} intensity between 0 and 1, applied to both motors
* @example
* s("bd*4").vibIntensity(sine.slow(2).range(0, 1)).vibrate(0)
*/
export const vibIntensity = register('vibIntensity', (intensity, pat) => {
return pat.vibStrong(intensity).vibWeak(intensity);
});
export const vibrationSupported = (index) => {
const gamepad = navigator.getGamepads()[index];
return gamepad?.vibrationActuator?.type === 'dual-rumble';
};
export const getGamepadVibrationActuator = (index) => {
const gamepad = navigator.getGamepads()[index];
if (!gamepad) {
throw new Error(`[gamepad] gamepad at index ${index} not found`);
}
if (!gamepad.vibrationActuator) {
throw new Error(`[gamepad] gamepad at index ${index} does not support vibration`);
}
return gamepad.vibrationActuator;
};
// Default vibration latency in milliseconds
const DEFAULT_VIBRATION_LATENCY = 10;
/**
* Parses vibration string format "id:strong:weak:duration"
* @param {string} vibString - The vibration string to parse
* @returns {object} Parsed vibration parameters
*/
function parseVibrationString(vibString) {
if (typeof vibString !== 'string') {
return null;
}
const parts = vibString.split(':');
if (parts.length < 1) {
return null;
}
const [id, strong, weak, duration] = parts;
return {
vibGamepadIndex: parseInt(id) || 0,
vibStrong: parseFloat(strong) || 0.8,
vibWeak: parseFloat(weak) || 0.4,
vibDuration: parseInt(duration) || 100,
vibEnable: 1,
};
}
/**
* Sends vibration commands to a gamepad based on pattern events.
*
* @name vibrate
* @memberof Pattern
* @param {number | string | Pattern} gamepadIndex - Index of the gamepad to vibrate (0-3), can be patterned, or vibration string
* @param {object} options - Default vibration options
* @param {number} options.strong - Default strong motor intensity (0-1)
* @param {number} options.weak - Default weak motor intensity (0-1)
* @param {number} options.duration - Default duration in milliseconds
* @param {number} options.latency - Latency compensation in milliseconds
* @returns {Pattern}
* @example
* // Basic vibration on beat
* s("bd hh sd hh").vibrate(0)
*
* @example
* // Using hap values for vibration control
* s("<bd hh sd hh>").vibStrong("<1 0.1 0.5 0>").vibWeak("<0.5 0 0.8 0>").vibrate(0)
*
* @example
* // With default options
* s("bd*4").vibrate(0, { strong: 1, weak: 0.5, duration: 100 })
*
* @example
* // Using gamepad input to control vibration
* let gp = gamepad(0);
* note("c a f e").gain(gp.a).vibrate(0, { strong: gp.x1, duration: 200 })
*
* @example
* // Patternable gamepad index - alternate between gamepads
* s("bd hh sd hh").vibrate("<0 1 0 1>")
*
* @example
* // Complex gamepad switching pattern
* s("bd*8").vibrate("0 1 0 [1 0] 1 0 1 0")
*
* @example
* // Using control parameter for gamepad index
* s("bd hh sd hh").vibGamepad("<0 1 0 1>").vibrate()
*
* @example
* // Assignable vibration syntax: "id:strong:weak:duration"
* s("bd hh sd hh").vibrate("0:1:0.5:200 1:0.8:0.3:150")
*
* @example
* // Complex assignable patterns
* s("bd*8").vibrate("0:0.2:0.4:100 0:0.2:1:100 1:1:0.5:200 1:0.5:1:150")
*/
export const { vibrate } = registerControl(['vibGamepad', 'vibStrong', 'vibWeak', 'vibDuration']);
Pattern.prototype.vibrate = function (gamepadIndex, options = {}) {
// Default options
const defaultOptions = {
strong: 0.8,
weak: 0.4,
duration: 100,
latency: DEFAULT_VIBRATION_LATENCY,
...options,
};
// If gamepadIndex is a pattern, handle it based on its content
if (isPattern(gamepadIndex)) {
return this.set(
gamepadIndex.fmap((value) => {
// Simple gamepad index pattern
return { vibGamepadIndex: value };
}),
).onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
return vibrateHandler(hap, currentTime, cps, targetTime, defaultOptions);
}, false);
}
// If gamepadIndex is provided as static value, use it directly
// If undefined, will be handled in vibrateHandler (using vibGamepad control or default 0)
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
return vibrateHandler(hap, currentTime, cps, targetTime, defaultOptions, gamepadIndex);
}, false);
};
// Helper function to handle the actual vibration logic
function vibrateHandler(hap, currentTime, cps, targetTime, defaultOptions, staticGamepadIndex = null) {
try {
// Determine gamepad index - from hap value or static parameter
let gamepadIndex;
if (staticGamepadIndex !== null && staticGamepadIndex !== undefined) {
gamepadIndex = staticGamepadIndex;
} else if (typeof hap.value === 'object' && ('vibGamepadIndex' in hap.value || 'vibGamepad' in hap.value)) {
const vibGamepadValue = hap.value.vibGamepadIndex;
gamepadIndex = Array.isArray(vibGamepadValue) ? vibGamepadValue[0] : vibGamepadValue;
} else {
gamepadIndex = 0; // fallback default
}
// Validate gamepad index
gamepadIndex = Math.max(0, Math.min(3, Math.floor(Number(gamepadIndex) || 0)));
// Check if vibration is supported
if (!vibrationSupported(gamepadIndex)) {
logger(`[gamepad] vibration not supported on gamepad ${gamepadIndex}`, 'warning');
return;
}
const vibrationActuator = getGamepadVibrationActuator(gamepadIndex);
// Extract vibration parameters from hap value or use defaults
let vibStrong, vibWeak, vibDuration, vibEnable;
if (typeof hap.value === 'object') {
const vibGamepadValue = hap.value.vibGamepadIndex;
// Handle array format: [index, strong, weak, duration]
if (Array.isArray(vibGamepadValue)) {
vibStrong = vibGamepadValue[1] !== undefined ? vibGamepadValue[1] : defaultOptions.strong;
vibWeak = vibGamepadValue[2] !== undefined ? vibGamepadValue[2] : defaultOptions.weak;
vibDuration = vibGamepadValue[3] !== undefined ? vibGamepadValue[3] : defaultOptions.duration;
vibEnable = 1;
} else {
// Regular object destructuring for non-array values
({
vibStrong = defaultOptions.strong,
vibWeak = defaultOptions.weak,
vibDuration = defaultOptions.duration,
vibEnable = 1,
} = hap.value);
}
} else {
// If hap.value is a primitive, use it as vibEnable (0 = off, 1 = on)
vibEnable = hap.value;
vibStrong = defaultOptions.strong;
vibWeak = defaultOptions.weak;
vibDuration = defaultOptions.duration;
}
// Skip vibration if disabled
if (!vibEnable || vibEnable === 0) {
return;
}
// Validate parameters
vibStrong = Math.max(0, Math.min(1, Number(vibStrong) || 0));
vibWeak = Math.max(0, Math.min(1, Number(vibWeak) || 0));
vibDuration = Math.max(0, Number(vibDuration) || defaultOptions.duration);
// Calculate timing offset
const offset = (targetTime - currentTime + defaultOptions.latency / 1000) * 1000;
// Schedule vibration
window.setTimeout(
() => {
try {
vibrationActuator
.playEffect('dual-rumble', {
duration: vibDuration,
strongMagnitude: vibStrong,
weakMagnitude: vibWeak,
})
.catch((err) => {
logger(`[gamepad] vibration failed: ${err.message}`, 'warning');
});
} catch (err) {
logger(`[gamepad] vibration error: ${err.message}`, 'warning');
}
},
Math.max(0, offset),
);
} catch (err) {
logger(`[gamepad] vibration setup failed: ${err.message}`, 'warning');
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/hydra",
"version": "1.2.5",
"version": "1.2.4",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"type": "module",
+8
View File
@@ -8,6 +8,14 @@ This package adds midi functionality to strudel Patterns.
npm i @strudel/midi --save
```
## Enabling MIDI for Local Development in Chrome
1. Open Chrome and navigate to `chrome://flags`
2. Search for "Insecure origins treated as secure"
3. In the text field that appears, add your development origin (e.g., http://localhost:3000)
4. Enable the flag
5. Restart Chrome
## Available Controls
The following MIDI controls are available:
+40 -7
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import * as _WebMidi from 'webmidi';
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
import { noteToMidi, getControlName } from '@strudel/core';
import { noteToMidi, hasControlName, getControlName, registerControl } from '@strudel/core';
import { Note } from 'webmidi';
// if you use WebMidi from outside of this package, make sure to import that instance:
@@ -100,10 +100,34 @@ export const midicontrolMap = new Map();
function unifyMapping(mapping) {
return Object.fromEntries(
Object.entries(mapping).map(([key, mapping]) => {
// Convert number to object with ccn property
if (typeof mapping === 'number') {
mapping = { ccn: mapping };
}
return [getControlName(key), mapping];
// Get the non-aliased control name from the key
const controlName = getControlName(key);
// Check if the key or controlName already exists in the controlAlias map
if (hasControlName(key) || hasControlName(controlName)) {
// Show warning and carry on.
logger(`[midimap] '[${key}, ${controlName}]' overwrites a Strudel API.`);
// Throw error to stop the music
//throw new Error(`[midimap] '${key}' overwrites a Strudel API.`);
}
// Register the control in the midicontrolMap if it doesn't exist
if (!midicontrolMap.has(controlName)) {
try {
registerControl(controlName);
} catch (err) {
throw new Error(`[midimap] Failed to register midimap control '${controlName}': ${err.message}`);
}
} else {
logger(`[midimap] '${controlName}' already registered as a midimap control. Skipping registration.`);
}
return [controlName, mapping];
}),
);
}
@@ -162,7 +186,14 @@ export async function midimaps(map) {
map = await loadCache[map];
}
if (typeof map === 'object') {
Object.entries(map).forEach(([name, mapping]) => midicontrolMap.set(name, unifyMapping(mapping)));
Object.entries(map).forEach(([name, mapping]) => {
try {
midicontrolMap.set(name, unifyMapping(mapping));
} catch (err) {
logger(`[midi] Error setting midimap '${name}': ${err.message}`);
throw err;
}
});
}
}
@@ -324,18 +355,18 @@ Pattern.prototype.midi = function (midiport, options = {}) {
const device = getDevice(midiConfig.midiport, outputs);
const otherOutputs = outputs.filter((o) => o.name !== device.name);
logger(
`Midi enabled! Using "${device.name}". ${
`[midi] Midi enabled! Using "${device.name}". ${
otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : ''
}`,
);
},
onDisconnected: ({ outputs }) =>
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
logger(`[midi] Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
});
return this.onTrigger((hap, currentTime, cps, targetTime) => {
if (!WebMidi.enabled) {
logger('Midi not enabled');
logger('[midi] Midi not enabled');
return;
}
hap.ensureObjectValue();
@@ -383,7 +414,9 @@ Pattern.prototype.midi = function (midiport, options = {}) {
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString));
} else if (midimap !== 'default') {
// Add warning when a non-existent midimap is specified
logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`);
throw new Error(
`[midimap] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`,
);
}
// Handle note
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
"version": "1.2.6",
"version": "1.2.5",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mini",
"version": "1.2.5",
"version": "1.2.4",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
+5 -8
View File
@@ -21,14 +21,13 @@ 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) {
@@ -151,9 +150,9 @@ export class MondoParser {
}
return children;
}
desugar_ops(children, types) {
desugar_ops(children) {
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;
const op = { type: 'plain', value: children[opIndex].value };
if (opIndex === children.length - 1) {
@@ -264,10 +263,8 @@ export class MondoParser {
// the type we've removed before splitting needs to be added back
children = [{ type: 'plain', value: type }, ...children];
}
// for each precendence group, call desugar_ops once
this.op_precedence.forEach((ops) => {
children = this.desugar_ops(children, ops);
});
children = this.desugar_ops(children);
// children = this.desugar_pipes(children, (children) => this.desugar_dollars(children));
children = this.desugar_pipes(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: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))'));
+2 -4
View File
@@ -5,13 +5,12 @@ import {
slow,
seq,
stepcat,
replicate,
extend,
expand,
pace,
chooseIn,
degradeBy,
silence,
bjork,
} from '@strudel/core';
import { registerLanguage } from '@strudel/transpiler';
import { MondoRunner } from 'mondolang';
@@ -37,11 +36,10 @@ lib.square = (...args) => stepcat(...args).setSteps(1);
lib.angle = (...args) => stepcat(...args).pace(1);
lib['*'] = fast;
lib['/'] = slow;
lib['!'] = replicate;
lib['!'] = extend;
lib['@'] = expand;
lib['%'] = pace;
lib['?'] = degradeBy; // todo: default 0.5 not working..
lib['&'] = bjork;
lib[':'] = tail;
lib['..'] = range;
lib['def'] = () => silence;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mondo",
"version": "1.1.5",
"version": "1.1.4",
"description": "mondo notation for strudel",
"main": "mondough.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/motion",
"version": "1.2.5",
"version": "1.2.4",
"description": "DeviceMotion API for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mqtt",
"version": "1.2.5",
"version": "1.2.4",
"description": "MQTT API for strudel",
"main": "mqtt.mjs",
"type": "module",
+16 -17
View File
@@ -4,6 +4,8 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import OSC from 'osc-js';
import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core';
let connection; // Promise<OSC>
@@ -11,18 +13,19 @@ function connect() {
if (!connection) {
// make sure this runs only once
connection = new Promise((resolve, reject) => {
const ws = new WebSocket('ws://localhost:8080');
ws.addEventListener('open', (event) => {
logger(`[osc] websocket connected`);
resolve(ws);
const osc = new OSC();
osc.open();
osc.on('open', () => {
const url = osc.options?.plugin?.socket?.url;
logger(`[osc] connected${url ? ` to ${url}` : ''}`);
resolve(osc);
});
ws.addEventListener('close', (event) => {
logger(`[osc] websocket closed`);
osc.on('close', () => {
connection = undefined; // allows new connection afterwards
console.log('[osc] disconnected');
reject('OSC connection closed');
});
ws.addEventListener('error', (err) => reject(err));
osc.on('error', (err) => reject(err));
}).catch((err) => {
connection = undefined;
throw new Error('Could not connect to OSC server. Is it running?');
@@ -58,19 +61,15 @@ export function parseControlsFromHap(hap, cps) {
const collator = new ClockCollator({});
export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
const ws = await connect();
const osc = await connect();
const controls = parseControlsFromHap(hap, cps);
const keyvals = Object.entries(controls).flat();
const ts = collator.calculateTimestamp(currentTime, targetTime) * 1000;
const msg = { address: '/dirt/play', args: keyvals, timestamp: ts };
if ('oschost' in hap.value) {
msg['host'] = hap.value['oschost'];
}
if ('oscport' in hap.value) {
msg['port'] = hap.value['oscport'];
}
ws.send(JSON.stringify(msg));
const ts = Math.round(collator.calculateTimestamp(currentTime, targetTime) * 1000);
const message = new OSC.Message('/dirt/play', ...keyvals);
const bundle = new OSC.Bundle([message], ts);
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
osc.send(bundle);
}
/**
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/osc",
"version": "1.3.0",
"version": "1.2.10",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"bin": "./server.js",
@@ -38,8 +38,7 @@
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"osc": "^2.4.5",
"ws": "^8.18.3"
"osc-js": "^2.4.1"
},
"devDependencies": {
"pkg": "^5.8.1",
Executable → Regular
+58 -47
View File
@@ -6,59 +6,70 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// import OSC from 'osc-js';
import OSC from 'osc-js';
import { WebSocketServer } from 'ws';
import osc from 'osc';
const args = process.argv.slice(2);
function getArgValue(flag) {
const i = args.indexOf(flag);
if (i !== -1) {
const nextIsFlag = args[i + 1]?.startsWith('--') ?? true;
if (nextIsFlag) return true;
return args[i + 1];
}
}
const WS_PORT = 8080; // WebSocket server port
const OSC_REMOTE_IP = '127.0.0.1';
const OSC_REMOTE_PORT = 57120;
let udpClientPort = Number(getArgValue('--port')) || 57120;
let debug = Number(getArgValue('--debug')) || 0;
const udpPort = new osc.UDPPort({
localAddress: '0.0.0.0',
localPort: 0,
remoteAddress: OSC_REMOTE_IP,
remotePort: OSC_REMOTE_PORT,
});
const config = {
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
udpServer: {
host: 'localhost', // @param {string} Hostname of udp server to bind to
port: 57121, // @param {number} Port of udp client for messaging
// enabling the following line will receive tidal messages:
// port: 57120, // @param {number} Port of udp client for messaging
exclusive: false, // @param {boolean} Exclusive flag
},
udpClient: {
host: 'localhost', // @param {string} Hostname of udp client for messaging
port: udpClientPort, // @param {number} Port of udp client for messaging
},
wsServer: {
host: 'localhost', // @param {string} Hostname of WebSocket server
port: 8080, // @param {number} Port of WebSocket server
},
};
udpPort.open();
console.log(`[Sending OSC] ${OSC_REMOTE_IP}:${OSC_REMOTE_PORT}`);
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
udpPort.on('error', (e) => {
console.log('Error: ', e);
});
const wss = new WebSocketServer({ port: WS_PORT });
console.log(`[Listening WS] ws://localhost:${WS_PORT}`);
wss.on('connection', (ws) => {
console.log('New WebSocket connection');
ws.on('message', (message) => {
let osc_host = '127.0.0.1';
let osc_port = 57120;
try {
const data = JSON.parse(message);
if ('host' in data) {
osc_host = data['host'];
}
if ('port' in data) {
osc_port = data['port'];
}
let msg = { address: data['address'], args: data['args'] };
if ('timestamp' in data) {
msg = { timeTag: osc.timeTag(0, data['timestamp']), packets: [msg] };
}
udpPort.send(msg, osc_host, osc_port);
} catch (err) {
console.error('Error parsing message:', err);
if (debug) {
osc.on('*', (message) => {
const { address, args } = message;
let str = '';
for (let i = 0; i < args.length; i += 2) {
str += `${args[i]}: ${args[i + 1]} `;
}
console.log(`${address} ${str}`);
});
}
ws.on('close', () => {
console.log('WebSocket connection closed');
});
osc.on('error', (message) => {
if (message.toString().includes('EADDRINUSE')) {
console.log(`------ ERROR -------
a server is already running on port 57121! to stop it:
1. run "lsof -ti :57121 | xargs kill -9" (macos / linux)
2. re-run the osc server
`);
} else {
console.log(message);
}
});
osc.open();
console.log('osc client running on port', config.udpClient.port);
console.log('osc server running on port', config.udpServer.port);
console.log('websocket server running on port', config.wsServer.port);
if (debug) {
console.log('debug logs enabled. incoming messages will appear below');
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/repl",
"version": "1.2.7",
"version": "1.2.6",
"description": "Strudel REPL as a Web Component",
"module": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/serial",
"version": "1.2.5",
"version": "1.2.4",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/soundfonts",
"version": "1.2.6",
"version": "1.2.5",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "1.2.6",
"version": "1.2.5",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
+3 -1
View File
@@ -129,7 +129,9 @@ function githubPath(base, subpath = '') {
let repo = components.length >= 2 ? components[1] : 'samples';
let branch = components.length >= 3 ? components[2] : 'main';
let other = components.slice(3);
other.push(subpath ? subpath : '');
if (subpath) {
other.push(subpath);
}
other = other.join('/');
return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`;
-1
View File
@@ -630,7 +630,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// effects
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
if (tremolosync != null) {
+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 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 = getAccidentalsOffset(acc);
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset;
};
export const midiToFreq = (n) => {
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "supradough",
"version": "1.2.4",
"version": "1.2.3",
"description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.",
"main": "index.mjs",
"type": "module",
@@ -32,5 +32,6 @@
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*",
"wav-encoder": "^1.3.0"
}
},
"dependencies": {}
}
+2 -2
View File
@@ -1,9 +1,9 @@
import './tonal.mjs';
import './voicings.mjs';
import './ireal.mjs';
export * from './tonal.mjs';
export * from './voicings.mjs';
export * from './ireal.mjs';
import './ireal.mjs';
export const packageName = '@strudel/tonal';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/tonal",
"version": "1.2.5",
"version": "1.2.4",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
+7
View File
@@ -61,6 +61,13 @@ 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'];
+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 { 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 { noteToMidi } from '../core/util.mjs';
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
@@ -184,15 +185,17 @@ 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
const match = /^(-?\d+)([#bsf]*)$/.exec(step);
// - Some number of sharps or flats (but not both)
const match = /^(-?\d+)(#+|b+)?$/.exec(step);
if (!match) {
throw new Error(`invalid scale step "${step}", expected number or integer with optional # b suffixes`);
}
asNumber = Number(match[1]);
const accidentals = match[2] || '';
offset = getAccidentalsOffset(accidentals);
// 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;
}
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.
*
* 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}.
*
@@ -251,6 +254,7 @@ 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) {
@@ -258,47 +262,44 @@ export const scale = register(
if (Array.isArray(scale)) {
scale = scale.flat().join(' ');
}
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);
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);
}
if (offset != 0) scaleNote = Note.transpose(scaleNote, Interval.fromSemitones(offset));
} catch (err) {
logger(`[tonal] ${err.message}`, 'error');
return; // will be removed
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;
}
}
hap.value = isObject ? { ...otherValues, note: scaleNote } : scaleNote;
// Tag with scale for downsteam scale-aware operations
return hap.setContext({ ...hap.context, scale });
});
return removeUndefineds(haps);
});
// 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 }))
);
},
true,
true, // preserve step count
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/transpiler",
"version": "1.2.5",
"version": "1.2.4",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/web",
"version": "1.2.6",
"version": "1.2.5",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"module": "web.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/webaudio",
"version": "1.2.6",
"version": "1.2.5",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/xen",
"version": "1.2.5",
"version": "1.2.4",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"type": "module",
+8 -246
View File
@@ -41,7 +41,7 @@ importers:
version: 2.2.7
'@vitest/coverage-v8':
specifier: 3.0.4
version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))
version: 3.0.4(vitest@3.0.4)
'@vitest/ui':
specifier: ^3.0.4
version: 3.0.4(vitest@3.0.4)
@@ -212,21 +212,12 @@ importers:
'@strudel/draw':
specifier: workspace:*
version: link:../draw
'@strudel/tonal':
specifier: workspace:*
version: link:../tonal
'@strudel/transpiler':
specifier: workspace:*
version: link:../transpiler
'@tonaljs/tonal':
specifier: ^4.10.0
version: 4.10.0
nanostores:
specifier: ^0.11.3
version: 0.11.3
superdough:
specifier: workspace:*
version: link:../superdough
devDependencies:
vite:
specifier: ^6.0.11
@@ -415,12 +406,9 @@ importers:
'@strudel/core':
specifier: workspace:*
version: link:../core
osc:
specifier: ^2.4.5
version: 2.4.5
ws:
specifier: ^8.18.3
version: 8.18.3
osc-js:
specifier: ^2.4.1
version: 2.4.1
devDependencies:
pkg:
specifier: ^5.8.1
@@ -2432,70 +2420,6 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
'@serialport/binding-mock@10.2.2':
resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==}
engines: {node: '>=12.0.0'}
'@serialport/bindings-cpp@12.0.1':
resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==}
engines: {node: '>=16.0.0'}
'@serialport/bindings-interface@1.2.2':
resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==}
engines: {node: ^12.22 || ^14.13 || >=16}
'@serialport/parser-byte-length@12.0.0':
resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==}
engines: {node: '>=12.0.0'}
'@serialport/parser-cctalk@12.0.0':
resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==}
engines: {node: '>=12.0.0'}
'@serialport/parser-delimiter@11.0.0':
resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==}
engines: {node: '>=12.0.0'}
'@serialport/parser-delimiter@12.0.0':
resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==}
engines: {node: '>=12.0.0'}
'@serialport/parser-inter-byte-timeout@12.0.0':
resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==}
engines: {node: '>=12.0.0'}
'@serialport/parser-packet-length@12.0.0':
resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==}
engines: {node: '>=8.6.0'}
'@serialport/parser-readline@11.0.0':
resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==}
engines: {node: '>=12.0.0'}
'@serialport/parser-readline@12.0.0':
resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==}
engines: {node: '>=12.0.0'}
'@serialport/parser-ready@12.0.0':
resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==}
engines: {node: '>=12.0.0'}
'@serialport/parser-regex@12.0.0':
resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==}
engines: {node: '>=12.0.0'}
'@serialport/parser-slip-encoder@12.0.0':
resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==}
engines: {node: '>=12.0.0'}
'@serialport/parser-spacepacket@12.0.0':
resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==}
engines: {node: '>=12.0.0'}
'@serialport/stream@12.0.0':
resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==}
engines: {node: '>=12.0.0'}
'@shikijs/core@1.29.1':
resolution: {integrity: sha512-Mo1gGGkuOYjDu5H8YwzmOuly9vNr8KDVkqj9xiKhhhFS8jisAtDSEWB9hzqRHLVQgFdA310e8XRJcW4tYhRB2A==}
@@ -3748,15 +3672,6 @@ packages:
supports-color:
optional: true
debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
debug@4.4.0:
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
engines: {node: '>=6.0'}
@@ -5366,9 +5281,6 @@ packages:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
engines: {node: '>=10'}
long@4.0.0:
resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==}
longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
@@ -5752,9 +5664,6 @@ packages:
ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -5813,9 +5722,6 @@ packages:
resolution: {integrity: sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==}
engines: {node: '>=10'}
node-addon-api@7.0.0:
resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==}
node-addon-api@8.3.0:
resolution: {integrity: sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==}
engines: {node: ^18 || ^20 || >= 21}
@@ -5854,10 +5760,6 @@ packages:
resolution: {integrity: sha512-yqkmYrMbK1wPrfz7mgeYvA4tBperLg9FQ4S3Sau3nSAkpOA0x0zC8nQ1siBwozy1f4SE8vq2n1WKv99r+PCa1Q==}
engines: {node: '>= 0.6.0'}
node-gyp-build@4.6.0:
resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==}
hasBin: true
node-gyp-build@4.8.4:
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
hasBin: true
@@ -6016,8 +5918,8 @@ packages:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'}
osc@2.4.5:
resolution: {integrity: sha512-Nc4/qcl+vA/CMxiKS1xrYgzjfnyB3W94gZnrkn3eTzihlndbEml6+wj1YQNKBI4r+qrw3obCcEoU7SVH/OxpxA==}
osc-js@2.4.1:
resolution: {integrity: sha512-QlSeRKJclL47FNvO1MUCAAp9frmCF9zcYbnf6R9HpcklAst8ZyX3ISsk1v/Vghr/5GmXn0bhVjFXF9h+hfnl4Q==}
own-keys@1.0.1:
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
@@ -6778,10 +6680,6 @@ packages:
serialize-javascript@4.0.0:
resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==}
serialport@12.0.0:
resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==}
engines: {node: '>=16.0.0'}
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
@@ -6869,9 +6767,6 @@ packages:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
slip@1.0.2:
resolution: {integrity: sha512-XrcHe3NAcyD3wO+O4I13RcS4/3AF+S9RvGNj9JhJeS02HyImwD2E3QWLrmn9hBfL+fB6yapagwxRkeyYzhk98g==}
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
@@ -7747,9 +7642,6 @@ packages:
resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==}
engines: {node: '>=18'}
wolfy87-eventemitter@5.2.9:
resolution: {integrity: sha512-P+6vtWyuDw+MB01X7UeF8TaHBvbCovf4HPEMF/SV7BdDc1SMTiBy13SRD71lQh4ExFTG1d/WNzDGDCyOKSMblw==}
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
@@ -7872,18 +7764,6 @@ packages:
utf-8-validate:
optional: true
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
xmlcreate@2.0.4:
resolution: {integrity: sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==}
@@ -9880,76 +9760,6 @@ snapshots:
'@rtsao/scc@1.1.0': {}
'@serialport/binding-mock@10.2.2':
dependencies:
'@serialport/bindings-interface': 1.2.2
debug: 4.4.0
transitivePeerDependencies:
- supports-color
optional: true
'@serialport/bindings-cpp@12.0.1':
dependencies:
'@serialport/bindings-interface': 1.2.2
'@serialport/parser-readline': 11.0.0
debug: 4.3.4
node-addon-api: 7.0.0
node-gyp-build: 4.6.0
transitivePeerDependencies:
- supports-color
optional: true
'@serialport/bindings-interface@1.2.2':
optional: true
'@serialport/parser-byte-length@12.0.0':
optional: true
'@serialport/parser-cctalk@12.0.0':
optional: true
'@serialport/parser-delimiter@11.0.0':
optional: true
'@serialport/parser-delimiter@12.0.0':
optional: true
'@serialport/parser-inter-byte-timeout@12.0.0':
optional: true
'@serialport/parser-packet-length@12.0.0':
optional: true
'@serialport/parser-readline@11.0.0':
dependencies:
'@serialport/parser-delimiter': 11.0.0
optional: true
'@serialport/parser-readline@12.0.0':
dependencies:
'@serialport/parser-delimiter': 12.0.0
optional: true
'@serialport/parser-ready@12.0.0':
optional: true
'@serialport/parser-regex@12.0.0':
optional: true
'@serialport/parser-slip-encoder@12.0.0':
optional: true
'@serialport/parser-spacepacket@12.0.0':
optional: true
'@serialport/stream@12.0.0':
dependencies:
'@serialport/bindings-interface': 1.2.2
debug: 4.3.4
transitivePeerDependencies:
- supports-color
optional: true
'@shikijs/core@1.29.1':
dependencies:
'@shikijs/engine-javascript': 1.29.1
@@ -10554,7 +10364,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))':
'@vitest/coverage-v8@3.0.4(vitest@3.0.4)':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
@@ -11462,11 +11272,6 @@ snapshots:
dependencies:
ms: 2.1.3
debug@4.3.4:
dependencies:
ms: 2.1.2
optional: true
debug@4.4.0:
dependencies:
ms: 2.1.3
@@ -13336,8 +13141,6 @@ snapshots:
chalk: 4.1.2
is-unicode-supported: 0.1.0
long@4.0.0: {}
longest-streak@3.1.0: {}
loupe@3.1.2: {}
@@ -14015,9 +13818,6 @@ snapshots:
ms@2.0.0: {}
ms@2.1.2:
optional: true
ms@2.1.3: {}
multimatch@5.0.0:
@@ -14067,9 +13867,6 @@ snapshots:
dependencies:
semver: 7.6.3
node-addon-api@7.0.0:
optional: true
node-addon-api@8.3.0: {}
node-domexception@1.0.0: {}
@@ -14096,9 +13893,6 @@ snapshots:
node-getopt@0.3.2: {}
node-gyp-build@4.6.0:
optional: true
node-gyp-build@4.8.4: {}
node-gyp@10.3.1:
@@ -14350,17 +14144,11 @@ snapshots:
os-tmpdir@1.0.2: {}
osc@2.4.5:
osc-js@2.4.1:
dependencies:
long: 4.0.0
slip: 1.0.2
wolfy87-eventemitter: 5.2.9
ws: 8.18.0
optionalDependencies:
serialport: 12.0.0
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
own-keys@1.0.1:
@@ -15255,26 +15043,6 @@ snapshots:
dependencies:
randombytes: 2.1.0
serialport@12.0.0:
dependencies:
'@serialport/binding-mock': 10.2.2
'@serialport/bindings-cpp': 12.0.1
'@serialport/parser-byte-length': 12.0.0
'@serialport/parser-cctalk': 12.0.0
'@serialport/parser-delimiter': 12.0.0
'@serialport/parser-inter-byte-timeout': 12.0.0
'@serialport/parser-packet-length': 12.0.0
'@serialport/parser-readline': 12.0.0
'@serialport/parser-ready': 12.0.0
'@serialport/parser-regex': 12.0.0
'@serialport/parser-slip-encoder': 12.0.0
'@serialport/parser-spacepacket': 12.0.0
'@serialport/stream': 12.0.0
debug: 4.3.4
transitivePeerDependencies:
- supports-color
optional: true
set-blocking@2.0.0: {}
set-function-length@1.2.2:
@@ -15417,8 +15185,6 @@ snapshots:
slash@3.0.0: {}
slip@1.0.2: {}
smart-buffer@4.2.0: {}
socks-proxy-agent@8.0.5:
@@ -16328,8 +16094,6 @@ snapshots:
dependencies:
string-width: 7.2.0
wolfy87-eventemitter@5.2.9: {}
word-wrap@1.2.5: {}
wordwrap@0.0.3: {}
@@ -16529,8 +16293,6 @@ snapshots:
ws@8.18.0: {}
ws@8.18.3: {}
xmlcreate@2.0.4: {}
xtend@4.0.2: {}
+4 -514
View File
@@ -3838,8 +3838,8 @@ exports[`runs examples > example "fast" example index 0 1`] = `
exports[`runs examples > example "fastChunk" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:C2 color:red ]",
"[ 1/4 → 1/2 | note:D2 color:red ]",
"[ 0/1 → 1/4 | color:red note:0 ]",
"[ 1/4 → 1/2 | color:red note:1 ]",
"[ 1/2 → 3/4 | note:E2 ]",
"[ 3/4 → 1/1 | note:F2 ]",
"[ 1/1 → 5/4 | note:G2 ]",
@@ -3848,8 +3848,8 @@ exports[`runs examples > example "fastChunk" example index 0 1`] = `
"[ 7/4 → 2/1 | note:C3 ]",
"[ 2/1 → 9/4 | note:D3 ]",
"[ 9/4 → 5/2 | note:D2 ]",
"[ 5/2 → 11/4 | note:E2 color:red ]",
"[ 11/4 → 3/1 | note:F2 color:red ]",
"[ 5/2 → 11/4 | color:red note:2 ]",
"[ 11/4 → 3/1 | color:red note:3 ]",
"[ 3/1 → 13/4 | note:G2 ]",
"[ 13/4 → 7/2 | note:A2 ]",
"[ 7/2 → 15/4 | note:B2 ]",
@@ -6958,24 +6958,6 @@ exports[`runs examples > example "orbit" example index 0 1`] = `
]
`;
exports[`runs examples > example "oschost" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
"[ 1/1 → 2/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
"[ 2/1 → 3/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
"[ 3/1 → 4/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
]
`;
exports[`runs examples > example "oscport" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
"[ 1/1 → 2/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
"[ 2/1 → 3/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
"[ 3/1 → 4/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
]
`;
exports[`runs examples > example "outside" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:A3 ]",
@@ -8325,33 +8307,6 @@ exports[`runs examples > example "repeatCycles" example index 0 1`] = `
]
`;
exports[`runs examples > example "replicate" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:bd ]",
"[ 1/8 → 1/4 | s:bd ]",
"[ 3/8 → 1/2 | s:cp ]",
"[ 1/2 → 5/8 | s:bd ]",
"[ 5/8 → 3/4 | s:bd ]",
"[ 7/8 → 1/1 | s:cp ]",
"[ 1/1 → 9/8 | s:bd ]",
"[ 5/4 → 11/8 | s:sd ]",
"[ 3/2 → 13/8 | s:bd ]",
"[ 13/8 → 7/4 | s:bd ]",
"[ 15/8 → 2/1 | s:cp ]",
"[ 2/1 → 17/8 | s:bd ]",
"[ 17/8 → 9/4 | s:bd ]",
"[ 19/8 → 5/2 | s:cp ]",
"[ 5/2 → 21/8 | s:bd ]",
"[ 11/4 → 23/8 | s:sd ]",
"[ 3/1 → 25/8 | s:bd ]",
"[ 25/8 → 13/4 | s:bd ]",
"[ 27/8 → 7/2 | s:cp ]",
"[ 7/2 → 29/8 | s:bd ]",
"[ 29/8 → 15/4 | s:bd ]",
"[ 31/8 → 4/1 | s:cp ]",
]
`;
exports[`runs examples > example "reset" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh ]",
@@ -11780,279 +11735,6 @@ exports[`runs examples > example "vib" example index 1 1`] = `
]
`;
exports[`runs examples > example "vibDuration" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibDuration:200 ]",
"[ 1/4 → 1/2 | s:hh vibDuration:0 ]",
"[ 1/2 → 3/4 | s:sd vibDuration:100 ]",
"[ 3/4 → 1/1 | s:hh vibDuration:0 ]",
"[ 1/1 → 5/4 | s:bd vibDuration:200 ]",
"[ 5/4 → 3/2 | s:hh vibDuration:0 ]",
"[ 3/2 → 7/4 | s:sd vibDuration:100 ]",
"[ 7/4 → 2/1 | s:hh vibDuration:0 ]",
"[ 2/1 → 9/4 | s:bd vibDuration:200 ]",
"[ 9/4 → 5/2 | s:hh vibDuration:0 ]",
"[ 5/2 → 11/4 | s:sd vibDuration:100 ]",
"[ 11/4 → 3/1 | s:hh vibDuration:0 ]",
"[ 3/1 → 13/4 | s:bd vibDuration:200 ]",
"[ 13/4 → 7/2 | s:hh vibDuration:0 ]",
"[ 7/2 → 15/4 | s:sd vibDuration:100 ]",
"[ 15/4 → 4/1 | s:hh vibDuration:0 ]",
]
`;
exports[`runs examples > example "vibDuration" example index 0 2`] = `
[
"[ 0/1 → 1/4 | s:bd vibDuration:200 ]",
"[ 1/4 → 1/2 | s:hh vibDuration:0 ]",
"[ 1/2 → 3/4 | s:sd vibDuration:100 ]",
"[ 3/4 → 1/1 | s:hh vibDuration:0 ]",
"[ 1/1 → 5/4 | s:bd vibDuration:200 ]",
"[ 5/4 → 3/2 | s:hh vibDuration:0 ]",
"[ 3/2 → 7/4 | s:sd vibDuration:100 ]",
"[ 7/4 → 2/1 | s:hh vibDuration:0 ]",
"[ 2/1 → 9/4 | s:bd vibDuration:200 ]",
"[ 9/4 → 5/2 | s:hh vibDuration:0 ]",
"[ 5/2 → 11/4 | s:sd vibDuration:100 ]",
"[ 11/4 → 3/1 | s:hh vibDuration:0 ]",
"[ 3/1 → 13/4 | s:bd vibDuration:200 ]",
"[ 13/4 → 7/2 | s:hh vibDuration:0 ]",
"[ 7/2 → 15/4 | s:sd vibDuration:100 ]",
"[ 15/4 → 4/1 | s:hh vibDuration:0 ]",
]
`;
exports[`runs examples > example "vibEnable" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibEnable:1 ]",
"[ 1/4 → 1/2 | s:hh vibEnable:0 ]",
"[ 1/2 → 3/4 | s:sd vibEnable:1 ]",
"[ 3/4 → 1/1 | s:hh vibEnable:0 ]",
"[ 1/1 → 5/4 | s:bd vibEnable:1 ]",
"[ 5/4 → 3/2 | s:hh vibEnable:0 ]",
"[ 3/2 → 7/4 | s:sd vibEnable:1 ]",
"[ 7/4 → 2/1 | s:hh vibEnable:0 ]",
"[ 2/1 → 9/4 | s:bd vibEnable:1 ]",
"[ 9/4 → 5/2 | s:hh vibEnable:0 ]",
"[ 5/2 → 11/4 | s:sd vibEnable:1 ]",
"[ 11/4 → 3/1 | s:hh vibEnable:0 ]",
"[ 3/1 → 13/4 | s:bd vibEnable:1 ]",
"[ 13/4 → 7/2 | s:hh vibEnable:0 ]",
"[ 7/2 → 15/4 | s:sd vibEnable:1 ]",
"[ 15/4 → 4/1 | s:hh vibEnable:0 ]",
]
`;
exports[`runs examples > example "vibEnable" example index 0 2`] = `
[
"[ 0/1 → 1/4 | s:bd vibEnable:1 ]",
"[ 1/4 → 1/2 | s:hh vibEnable:0 ]",
"[ 1/2 → 3/4 | s:sd vibEnable:1 ]",
"[ 3/4 → 1/1 | s:hh vibEnable:0 ]",
"[ 1/1 → 5/4 | s:bd vibEnable:1 ]",
"[ 5/4 → 3/2 | s:hh vibEnable:0 ]",
"[ 3/2 → 7/4 | s:sd vibEnable:1 ]",
"[ 7/4 → 2/1 | s:hh vibEnable:0 ]",
"[ 2/1 → 9/4 | s:bd vibEnable:1 ]",
"[ 9/4 → 5/2 | s:hh vibEnable:0 ]",
"[ 5/2 → 11/4 | s:sd vibEnable:1 ]",
"[ 11/4 → 3/1 | s:hh vibEnable:0 ]",
"[ 3/1 → 13/4 | s:bd vibEnable:1 ]",
"[ 13/4 → 7/2 | s:hh vibEnable:0 ]",
"[ 7/2 → 15/4 | s:sd vibEnable:1 ]",
"[ 15/4 → 4/1 | s:hh vibEnable:0 ]",
]
`;
exports[`runs examples > example "vibGamepad" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibGamepad:0 ]",
"[ 1/4 → 1/2 | s:hh vibGamepad:1 ]",
"[ 1/2 → 3/4 | s:sd vibGamepad:0 ]",
"[ 3/4 → 1/1 | s:hh vibGamepad:1 ]",
"[ 1/1 → 5/4 | s:bd vibGamepad:0 ]",
"[ 5/4 → 3/2 | s:hh vibGamepad:1 ]",
"[ 3/2 → 7/4 | s:sd vibGamepad:0 ]",
"[ 7/4 → 2/1 | s:hh vibGamepad:1 ]",
"[ 2/1 → 9/4 | s:bd vibGamepad:0 ]",
"[ 9/4 → 5/2 | s:hh vibGamepad:1 ]",
"[ 5/2 → 11/4 | s:sd vibGamepad:0 ]",
"[ 11/4 → 3/1 | s:hh vibGamepad:1 ]",
"[ 3/1 → 13/4 | s:bd vibGamepad:0 ]",
"[ 13/4 → 7/2 | s:hh vibGamepad:1 ]",
"[ 7/2 → 15/4 | s:sd vibGamepad:0 ]",
"[ 15/4 → 4/1 | s:hh vibGamepad:1 ]",
]
`;
exports[`runs examples > example "vibGamepad" example index 0 2`] = `
[
"[ 0/1 → 1/4 | s:bd vibGamepad:0 ]",
"[ 1/4 → 1/2 | s:hh vibGamepad:1 ]",
"[ 1/2 → 3/4 | s:sd vibGamepad:0 ]",
"[ 3/4 → 1/1 | s:hh vibGamepad:1 ]",
"[ 1/1 → 5/4 | s:bd vibGamepad:0 ]",
"[ 5/4 → 3/2 | s:hh vibGamepad:1 ]",
"[ 3/2 → 7/4 | s:sd vibGamepad:0 ]",
"[ 7/4 → 2/1 | s:hh vibGamepad:1 ]",
"[ 2/1 → 9/4 | s:bd vibGamepad:0 ]",
"[ 9/4 → 5/2 | s:hh vibGamepad:1 ]",
"[ 5/2 → 11/4 | s:sd vibGamepad:0 ]",
"[ 11/4 → 3/1 | s:hh vibGamepad:1 ]",
"[ 3/1 → 13/4 | s:bd vibGamepad:0 ]",
"[ 13/4 → 7/2 | s:hh vibGamepad:1 ]",
"[ 7/2 → 15/4 | s:sd vibGamepad:0 ]",
"[ 15/4 → 4/1 | s:hh vibGamepad:1 ]",
]
`;
exports[`runs examples > example "vibGamepadIndex" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibGamepadIndex:0 ]",
"[ 1/4 → 1/2 | s:hh vibGamepadIndex:1 ]",
"[ 1/2 → 3/4 | s:sd vibGamepadIndex:0 ]",
"[ 3/4 → 1/1 | s:hh vibGamepadIndex:1 ]",
"[ 1/1 → 5/4 | s:bd vibGamepadIndex:0 ]",
"[ 5/4 → 3/2 | s:hh vibGamepadIndex:1 ]",
"[ 3/2 → 7/4 | s:sd vibGamepadIndex:0 ]",
"[ 7/4 → 2/1 | s:hh vibGamepadIndex:1 ]",
"[ 2/1 → 9/4 | s:bd vibGamepadIndex:0 ]",
"[ 9/4 → 5/2 | s:hh vibGamepadIndex:1 ]",
"[ 5/2 → 11/4 | s:sd vibGamepadIndex:0 ]",
"[ 11/4 → 3/1 | s:hh vibGamepadIndex:1 ]",
"[ 3/1 → 13/4 | s:bd vibGamepadIndex:0 ]",
"[ 13/4 → 7/2 | s:hh vibGamepadIndex:1 ]",
"[ 7/2 → 15/4 | s:sd vibGamepadIndex:0 ]",
"[ 15/4 → 4/1 | s:hh vibGamepadIndex:1 ]",
]
`;
exports[`runs examples > example "vibIntensity" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 1/4 → 1/2 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 1/2 → 3/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 3/4 → 1/1 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 1/1 → 5/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 5/4 → 3/2 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 3/2 → 7/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 7/4 → 2/1 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 2/1 → 9/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 9/4 → 5/2 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 5/2 → 11/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 11/4 → 3/1 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 3/1 → 13/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 13/4 → 7/2 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 7/2 → 15/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 15/4 → 4/1 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
]
`;
exports[`runs examples > example "vibIntensity" example index 0 2`] = `
[
"[ 0/1 → 1/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 1/4 → 1/2 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 1/2 → 3/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 3/4 → 1/1 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 1/1 → 5/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 5/4 → 3/2 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 3/2 → 7/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 7/4 → 2/1 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 2/1 → 9/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 9/4 → 5/2 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 5/2 → 11/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 11/4 → 3/1 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 3/1 → 13/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 13/4 → 7/2 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 7/2 → 15/4 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
"[ 15/4 → 4/1 | s:bd vibStrong:0.4999999999999999 vibWeak:0.4999999999999999 ]",
]
`;
exports[`runs examples > example "vibStrong" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibStrong:1 ]",
"[ 1/4 → 1/2 | s:hh vibStrong:0 ]",
"[ 1/2 → 3/4 | s:sd vibStrong:0.5 ]",
"[ 3/4 → 1/1 | s:hh vibStrong:0 ]",
"[ 1/1 → 5/4 | s:bd vibStrong:1 ]",
"[ 5/4 → 3/2 | s:hh vibStrong:0 ]",
"[ 3/2 → 7/4 | s:sd vibStrong:0.5 ]",
"[ 7/4 → 2/1 | s:hh vibStrong:0 ]",
"[ 2/1 → 9/4 | s:bd vibStrong:1 ]",
"[ 9/4 → 5/2 | s:hh vibStrong:0 ]",
"[ 5/2 → 11/4 | s:sd vibStrong:0.5 ]",
"[ 11/4 → 3/1 | s:hh vibStrong:0 ]",
"[ 3/1 → 13/4 | s:bd vibStrong:1 ]",
"[ 13/4 → 7/2 | s:hh vibStrong:0 ]",
"[ 7/2 → 15/4 | s:sd vibStrong:0.5 ]",
"[ 15/4 → 4/1 | s:hh vibStrong:0 ]",
]
`;
exports[`runs examples > example "vibStrong" example index 0 2`] = `
[
"[ 0/1 → 1/4 | s:bd vibStrong:1 ]",
"[ 1/4 → 1/2 | s:hh vibStrong:0 ]",
"[ 1/2 → 3/4 | s:sd vibStrong:0.5 ]",
"[ 3/4 → 1/1 | s:hh vibStrong:0 ]",
"[ 1/1 → 5/4 | s:bd vibStrong:1 ]",
"[ 5/4 → 3/2 | s:hh vibStrong:0 ]",
"[ 3/2 → 7/4 | s:sd vibStrong:0.5 ]",
"[ 7/4 → 2/1 | s:hh vibStrong:0 ]",
"[ 2/1 → 9/4 | s:bd vibStrong:1 ]",
"[ 9/4 → 5/2 | s:hh vibStrong:0 ]",
"[ 5/2 → 11/4 | s:sd vibStrong:0.5 ]",
"[ 11/4 → 3/1 | s:hh vibStrong:0 ]",
"[ 3/1 → 13/4 | s:bd vibStrong:1 ]",
"[ 13/4 → 7/2 | s:hh vibStrong:0 ]",
"[ 7/2 → 15/4 | s:sd vibStrong:0.5 ]",
"[ 15/4 → 4/1 | s:hh vibStrong:0 ]",
]
`;
exports[`runs examples > example "vibWeak" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibWeak:0.5 ]",
"[ 1/4 → 1/2 | s:hh vibWeak:0 ]",
"[ 1/2 → 3/4 | s:sd vibWeak:0.8 ]",
"[ 3/4 → 1/1 | s:hh vibWeak:0 ]",
"[ 1/1 → 5/4 | s:bd vibWeak:0.5 ]",
"[ 5/4 → 3/2 | s:hh vibWeak:0 ]",
"[ 3/2 → 7/4 | s:sd vibWeak:0.8 ]",
"[ 7/4 → 2/1 | s:hh vibWeak:0 ]",
"[ 2/1 → 9/4 | s:bd vibWeak:0.5 ]",
"[ 9/4 → 5/2 | s:hh vibWeak:0 ]",
"[ 5/2 → 11/4 | s:sd vibWeak:0.8 ]",
"[ 11/4 → 3/1 | s:hh vibWeak:0 ]",
"[ 3/1 → 13/4 | s:bd vibWeak:0.5 ]",
"[ 13/4 → 7/2 | s:hh vibWeak:0 ]",
"[ 7/2 → 15/4 | s:sd vibWeak:0.8 ]",
"[ 15/4 → 4/1 | s:hh vibWeak:0 ]",
]
`;
exports[`runs examples > example "vibWeak" example index 0 2`] = `
[
"[ 0/1 → 1/4 | s:bd vibWeak:0.5 ]",
"[ 1/4 → 1/2 | s:hh vibWeak:0 ]",
"[ 1/2 → 3/4 | s:sd vibWeak:0.8 ]",
"[ 3/4 → 1/1 | s:hh vibWeak:0 ]",
"[ 1/1 → 5/4 | s:bd vibWeak:0.5 ]",
"[ 5/4 → 3/2 | s:hh vibWeak:0 ]",
"[ 3/2 → 7/4 | s:sd vibWeak:0.8 ]",
"[ 7/4 → 2/1 | s:hh vibWeak:0 ]",
"[ 2/1 → 9/4 | s:bd vibWeak:0.5 ]",
"[ 9/4 → 5/2 | s:hh vibWeak:0 ]",
"[ 5/2 → 11/4 | s:sd vibWeak:0.8 ]",
"[ 11/4 → 3/1 | s:hh vibWeak:0 ]",
"[ 3/1 → 13/4 | s:bd vibWeak:0.5 ]",
"[ 13/4 → 7/2 | s:hh vibWeak:0 ]",
"[ 7/2 → 15/4 | s:sd vibWeak:0.8 ]",
"[ 15/4 → 4/1 | s:hh vibWeak:0 ]",
]
`;
exports[`runs examples > example "vibmod" example index 0 1`] = `
[
"[ 0/1 → 1/2 | note:a vib:4 vibmod:0.25 ]",
@@ -12079,198 +11761,6 @@ exports[`runs examples > example "vibmod" example index 1 1`] = `
]
`;
exports[`runs examples > example "vibrate" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd ]",
"[ 1/4 → 1/2 | s:hh ]",
"[ 1/2 → 3/4 | s:sd ]",
"[ 3/4 → 1/1 | s:hh ]",
"[ 1/1 → 5/4 | s:bd ]",
"[ 5/4 → 3/2 | s:hh ]",
"[ 3/2 → 7/4 | s:sd ]",
"[ 7/4 → 2/1 | s:hh ]",
"[ 2/1 → 9/4 | s:bd ]",
"[ 9/4 → 5/2 | s:hh ]",
"[ 5/2 → 11/4 | s:sd ]",
"[ 11/4 → 3/1 | s:hh ]",
"[ 3/1 → 13/4 | s:bd ]",
"[ 13/4 → 7/2 | s:hh ]",
"[ 7/2 → 15/4 | s:sd ]",
"[ 15/4 → 4/1 | s:hh ]",
]
`;
exports[`runs examples > example "vibrate" example index 1 1`] = `
[
"[ 0/1 → 1/1 | s:bd vibStrong:1 vibWeak:0.5 ]",
"[ 1/1 → 2/1 | s:hh vibStrong:0.1 vibWeak:0 ]",
"[ 2/1 → 3/1 | s:sd vibStrong:0.5 vibWeak:0.8 ]",
"[ 3/1 → 4/1 | s:hh vibStrong:0 vibWeak:0 ]",
]
`;
exports[`runs examples > example "vibrate" example index 2 1`] = `
[
"[ 0/1 → 1/4 | s:bd ]",
"[ 1/4 → 1/2 | s:bd ]",
"[ 1/2 → 3/4 | s:bd ]",
"[ 3/4 → 1/1 | s:bd ]",
"[ 1/1 → 5/4 | s:bd ]",
"[ 5/4 → 3/2 | s:bd ]",
"[ 3/2 → 7/4 | s:bd ]",
"[ 7/4 → 2/1 | s:bd ]",
"[ 2/1 → 9/4 | s:bd ]",
"[ 9/4 → 5/2 | s:bd ]",
"[ 5/2 → 11/4 | s:bd ]",
"[ 11/4 → 3/1 | s:bd ]",
"[ 3/1 → 13/4 | s:bd ]",
"[ 13/4 → 7/2 | s:bd ]",
"[ 7/2 → 15/4 | s:bd ]",
"[ 15/4 → 4/1 | s:bd ]",
]
`;
exports[`runs examples > example "vibrate" example index 4 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibGamepadIndex:0 ]",
"[ 1/4 → 1/2 | s:hh vibGamepadIndex:0 ]",
"[ 1/2 → 3/4 | s:sd vibGamepadIndex:0 ]",
"[ 3/4 → 1/1 | s:hh vibGamepadIndex:0 ]",
"[ 1/1 → 5/4 | s:bd vibGamepadIndex:1 ]",
"[ 5/4 → 3/2 | s:hh vibGamepadIndex:1 ]",
"[ 3/2 → 7/4 | s:sd vibGamepadIndex:1 ]",
"[ 7/4 → 2/1 | s:hh vibGamepadIndex:1 ]",
"[ 2/1 → 9/4 | s:bd vibGamepadIndex:0 ]",
"[ 9/4 → 5/2 | s:hh vibGamepadIndex:0 ]",
"[ 5/2 → 11/4 | s:sd vibGamepadIndex:0 ]",
"[ 11/4 → 3/1 | s:hh vibGamepadIndex:0 ]",
"[ 3/1 → 13/4 | s:bd vibGamepadIndex:1 ]",
"[ 13/4 → 7/2 | s:hh vibGamepadIndex:1 ]",
"[ 7/2 → 15/4 | s:sd vibGamepadIndex:1 ]",
"[ 15/4 → 4/1 | s:hh vibGamepadIndex:1 ]",
]
`;
exports[`runs examples > example "vibrate" example index 5 1`] = `
[
"[ 0/1 → 1/8 | s:bd vibGamepadIndex:0 ]",
"[ 1/8 → 1/4 | s:bd vibGamepadIndex:1 ]",
"[ 1/4 → 3/8 | s:bd vibGamepadIndex:0 ]",
"[ (3/8 → 7/16) ⇝ 1/2 | s:bd vibGamepadIndex:1 ]",
"[ 3/8 ⇜ (7/16 → 1/2) | s:bd vibGamepadIndex:0 ]",
"[ 1/2 → 5/8 | s:bd vibGamepadIndex:1 ]",
"[ 5/8 → 3/4 | s:bd vibGamepadIndex:0 ]",
"[ 3/4 → 7/8 | s:bd vibGamepadIndex:1 ]",
"[ 7/8 → 1/1 | s:bd vibGamepadIndex:0 ]",
"[ 1/1 → 9/8 | s:bd vibGamepadIndex:0 ]",
"[ 9/8 → 5/4 | s:bd vibGamepadIndex:1 ]",
"[ 5/4 → 11/8 | s:bd vibGamepadIndex:0 ]",
"[ (11/8 → 23/16) ⇝ 3/2 | s:bd vibGamepadIndex:1 ]",
"[ 11/8 ⇜ (23/16 → 3/2) | s:bd vibGamepadIndex:0 ]",
"[ 3/2 → 13/8 | s:bd vibGamepadIndex:1 ]",
"[ 13/8 → 7/4 | s:bd vibGamepadIndex:0 ]",
"[ 7/4 → 15/8 | s:bd vibGamepadIndex:1 ]",
"[ 15/8 → 2/1 | s:bd vibGamepadIndex:0 ]",
"[ 2/1 → 17/8 | s:bd vibGamepadIndex:0 ]",
"[ 17/8 → 9/4 | s:bd vibGamepadIndex:1 ]",
"[ 9/4 → 19/8 | s:bd vibGamepadIndex:0 ]",
"[ (19/8 → 39/16) ⇝ 5/2 | s:bd vibGamepadIndex:1 ]",
"[ 19/8 ⇜ (39/16 → 5/2) | s:bd vibGamepadIndex:0 ]",
"[ 5/2 → 21/8 | s:bd vibGamepadIndex:1 ]",
"[ 21/8 → 11/4 | s:bd vibGamepadIndex:0 ]",
"[ 11/4 → 23/8 | s:bd vibGamepadIndex:1 ]",
"[ 23/8 → 3/1 | s:bd vibGamepadIndex:0 ]",
"[ 3/1 → 25/8 | s:bd vibGamepadIndex:0 ]",
"[ 25/8 → 13/4 | s:bd vibGamepadIndex:1 ]",
"[ 13/4 → 27/8 | s:bd vibGamepadIndex:0 ]",
"[ (27/8 → 55/16) ⇝ 7/2 | s:bd vibGamepadIndex:1 ]",
"[ 27/8 ⇜ (55/16 → 7/2) | s:bd vibGamepadIndex:0 ]",
"[ 7/2 → 29/8 | s:bd vibGamepadIndex:1 ]",
"[ 29/8 → 15/4 | s:bd vibGamepadIndex:0 ]",
"[ 15/4 → 31/8 | s:bd vibGamepadIndex:1 ]",
"[ 31/8 → 4/1 | s:bd vibGamepadIndex:0 ]",
]
`;
exports[`runs examples > example "vibrate" example index 6 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibGamepad:0 ]",
"[ 1/4 → 1/2 | s:hh vibGamepad:0 ]",
"[ 1/2 → 3/4 | s:sd vibGamepad:0 ]",
"[ 3/4 → 1/1 | s:hh vibGamepad:0 ]",
"[ 1/1 → 5/4 | s:bd vibGamepad:1 ]",
"[ 5/4 → 3/2 | s:hh vibGamepad:1 ]",
"[ 3/2 → 7/4 | s:sd vibGamepad:1 ]",
"[ 7/4 → 2/1 | s:hh vibGamepad:1 ]",
"[ 2/1 → 9/4 | s:bd vibGamepad:0 ]",
"[ 9/4 → 5/2 | s:hh vibGamepad:0 ]",
"[ 5/2 → 11/4 | s:sd vibGamepad:0 ]",
"[ 11/4 → 3/1 | s:hh vibGamepad:0 ]",
"[ 3/1 → 13/4 | s:bd vibGamepad:1 ]",
"[ 13/4 → 7/2 | s:hh vibGamepad:1 ]",
"[ 7/2 → 15/4 | s:sd vibGamepad:1 ]",
"[ 15/4 → 4/1 | s:hh vibGamepad:1 ]",
]
`;
exports[`runs examples > example "vibrate" example index 7 1`] = `
[
"[ 0/1 → 1/4 | s:bd vibGamepadIndex:[0 1 0.5 200] ]",
"[ 1/4 → 1/2 | s:hh vibGamepadIndex:[0 1 0.5 200] ]",
"[ 1/2 → 3/4 | s:sd vibGamepadIndex:[1 0.8 0.3 150] ]",
"[ 3/4 → 1/1 | s:hh vibGamepadIndex:[1 0.8 0.3 150] ]",
"[ 1/1 → 5/4 | s:bd vibGamepadIndex:[0 1 0.5 200] ]",
"[ 5/4 → 3/2 | s:hh vibGamepadIndex:[0 1 0.5 200] ]",
"[ 3/2 → 7/4 | s:sd vibGamepadIndex:[1 0.8 0.3 150] ]",
"[ 7/4 → 2/1 | s:hh vibGamepadIndex:[1 0.8 0.3 150] ]",
"[ 2/1 → 9/4 | s:bd vibGamepadIndex:[0 1 0.5 200] ]",
"[ 9/4 → 5/2 | s:hh vibGamepadIndex:[0 1 0.5 200] ]",
"[ 5/2 → 11/4 | s:sd vibGamepadIndex:[1 0.8 0.3 150] ]",
"[ 11/4 → 3/1 | s:hh vibGamepadIndex:[1 0.8 0.3 150] ]",
"[ 3/1 → 13/4 | s:bd vibGamepadIndex:[0 1 0.5 200] ]",
"[ 13/4 → 7/2 | s:hh vibGamepadIndex:[0 1 0.5 200] ]",
"[ 7/2 → 15/4 | s:sd vibGamepadIndex:[1 0.8 0.3 150] ]",
"[ 15/4 → 4/1 | s:hh vibGamepadIndex:[1 0.8 0.3 150] ]",
]
`;
exports[`runs examples > example "vibrate" example index 8 1`] = `
[
"[ 0/1 → 1/8 | s:bd vibGamepadIndex:[0 0.2 0.4 100] ]",
"[ 1/8 → 1/4 | s:bd vibGamepadIndex:[0 0.2 0.4 100] ]",
"[ 1/4 → 3/8 | s:bd vibGamepadIndex:[0 0.2 1 100] ]",
"[ 3/8 → 1/2 | s:bd vibGamepadIndex:[0 0.2 1 100] ]",
"[ 1/2 → 5/8 | s:bd vibGamepadIndex:[1 1 0.5 200] ]",
"[ 5/8 → 3/4 | s:bd vibGamepadIndex:[1 1 0.5 200] ]",
"[ 3/4 → 7/8 | s:bd vibGamepadIndex:[1 0.5 1 150] ]",
"[ 7/8 → 1/1 | s:bd vibGamepadIndex:[1 0.5 1 150] ]",
"[ 1/1 → 9/8 | s:bd vibGamepadIndex:[0 0.2 0.4 100] ]",
"[ 9/8 → 5/4 | s:bd vibGamepadIndex:[0 0.2 0.4 100] ]",
"[ 5/4 → 11/8 | s:bd vibGamepadIndex:[0 0.2 1 100] ]",
"[ 11/8 → 3/2 | s:bd vibGamepadIndex:[0 0.2 1 100] ]",
"[ 3/2 → 13/8 | s:bd vibGamepadIndex:[1 1 0.5 200] ]",
"[ 13/8 → 7/4 | s:bd vibGamepadIndex:[1 1 0.5 200] ]",
"[ 7/4 → 15/8 | s:bd vibGamepadIndex:[1 0.5 1 150] ]",
"[ 15/8 → 2/1 | s:bd vibGamepadIndex:[1 0.5 1 150] ]",
"[ 2/1 → 17/8 | s:bd vibGamepadIndex:[0 0.2 0.4 100] ]",
"[ 17/8 → 9/4 | s:bd vibGamepadIndex:[0 0.2 0.4 100] ]",
"[ 9/4 → 19/8 | s:bd vibGamepadIndex:[0 0.2 1 100] ]",
"[ 19/8 → 5/2 | s:bd vibGamepadIndex:[0 0.2 1 100] ]",
"[ 5/2 → 21/8 | s:bd vibGamepadIndex:[1 1 0.5 200] ]",
"[ 21/8 → 11/4 | s:bd vibGamepadIndex:[1 1 0.5 200] ]",
"[ 11/4 → 23/8 | s:bd vibGamepadIndex:[1 0.5 1 150] ]",
"[ 23/8 → 3/1 | s:bd vibGamepadIndex:[1 0.5 1 150] ]",
"[ 3/1 → 25/8 | s:bd vibGamepadIndex:[0 0.2 0.4 100] ]",
"[ 25/8 → 13/4 | s:bd vibGamepadIndex:[0 0.2 0.4 100] ]",
"[ 13/4 → 27/8 | s:bd vibGamepadIndex:[0 0.2 1 100] ]",
"[ 27/8 → 7/2 | s:bd vibGamepadIndex:[0 0.2 1 100] ]",
"[ 7/2 → 29/8 | s:bd vibGamepadIndex:[1 1 0.5 200] ]",
"[ 29/8 → 15/4 | s:bd vibGamepadIndex:[1 1 0.5 200] ]",
"[ 15/4 → 31/8 | s:bd vibGamepadIndex:[1 0.5 1 150] ]",
"[ 31/8 → 4/1 | s:bd vibGamepadIndex:[1 0.5 1 150] ]",
]
`;
exports[`runs examples > example "voicing" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:52 ]",
-8
View File
@@ -18,14 +18,6 @@ const skippedExamples = [
'accelerationZ',
'accelerationY',
'accelerationX',
'vibrate',
'vibGamepadIndex',
'vibGamepadId',
'vibEnable',
'vibWeak',
'vibStrong',
'vibDuration',
'vibIntensity',
'defaultmidimap',
'midimaps',
];
+2 -2
View File
@@ -23,7 +23,7 @@ import '@strudel/xen/xen.mjs';
import '../website/src/repl/piano';
//import * as motionHelpers from '../packages/motion/index.mjs';
//import * as geolocationHelpers from '../packages/geolocation/index.mjs';
//import * as gamepadHelpers from '../packages/gamepad/index.mjs';
import * as gamepadHelpers from '../packages/gamepad/index.mjs';
class MockedNode {
chain() {
@@ -141,8 +141,8 @@ evalScope(
uiHelpersMocked,
webaudio,
tonalHelpers,
/*
gamepadHelpers,
/*
toneHelpers,
voicingHelpers,
drawHelpers,
+1 -1
View File
@@ -157,7 +157,7 @@ It has 20 parameters in total, here is a snippet that uses all:
.pitchJump(0) // +/- pitch change after pitchJumpTime
.pitchJumpTime(0) // >0 time after pitchJump is applied
.lfo(0) // >0 resets slide + pitchJump + sets tremolo speed
.tremolo(0.5) // 0-1 lfo volume modulation amount
.tremolo(0) // 0-1 lfo volume modulation amount
//.duration(.2) // overwrite strudel event duration
//.gain(1) // change volume
._scope() // vizualise waveform (not zzfx related)
@@ -117,10 +117,6 @@ 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,
@@ -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.
+8 -9
View File
@@ -6,7 +6,6 @@ import './files.mjs';
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
const baseCDN = 'https://strudel.b-cdn.net';
export async function prebake() {
// https://archive.org/details/SalamanderGrandPianoV3
@@ -20,23 +19,23 @@ export async function prebake() {
// => getting "window is not defined", as soon as "@strudel/soundfonts" is imported statically
// seems to be a problem with soundfont2
import('@strudel/soundfonts').then(({ registerSoundfonts }) => registerSoundfonts()),
samples(`${baseCDN}/piano.json`, `${baseCDN}/piano/`, { prebake: true }),
samples(`${baseNoTrailing}/piano.json`, undefined, { prebake: true }),
// https://github.com/sgossner/VCSL/
// https://api.github.com/repositories/126427031/contents/
// LICENSE: CC0 general-purpose
samples(`${baseCDN}/vcsl.json`, `${baseCDN}/VCSL/`, { prebake: true }),
samples(`${baseCDN}/tidal-drum-machines.json`, `${baseCDN}/tidal-drum-machines/machines/`, {
samples(`${baseNoTrailing}/vcsl.json`, 'github:sgossner/VCSL/master/', { prebake: true }),
samples(`${baseNoTrailing}/tidal-drum-machines.json`, 'github:ritchse/tidal-drum-machines/main/machines/', {
prebake: true,
tag: 'drum-machines',
}),
samples(`${baseCDN}/uzu-drumkit.json`, `${baseCDN}/uzu-drumkit/`, {
samples(`${baseNoTrailing}/uzu-drumkit.json`, undefined, {
prebake: true,
tag: 'drum-machines',
}),
samples(`${baseCDN}/uzu-wavetables.json`, `${baseCDN}/uzu-wavetables/`, {
samples(`${baseNoTrailing}/uzu-wavetables.json`, undefined, {
prebake: true,
}),
samples(`${baseCDN}/mridangam.json`, `${baseCDN}/mrid/`, { prebake: true, tag: 'drum-machines' }),
samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }),
samples(
{
casio: ['casio/high.wav', 'casio/low.wav', 'casio/noise.wav'],
@@ -146,14 +145,14 @@ export async function prebake() {
'num/20.wav',
],
},
`${baseCDN}/Dirt-Samples/`,
'github:tidalcycles/dirt-samples',
{
prebake: true,
},
),
]);
aliasBank(`${baseCDN}/tidal-drum-machines-alias.json`);
aliasBank(`${baseNoTrailing}/tidal-drum-machines-alias.json`);
}
const maxPan = noteToMidi('C8');