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