mirror of
https://codeberg.org/uzu/strudel
synced 2026-10-06 09:06:16 -04:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9258857e36 | |||
| ad7e301394 | |||
| 7acb9a3ac7 | |||
| 9665b233a1 | |||
| 6c67851909 | |||
| 109ea3093b | |||
| 8d991ad8ae | |||
| 0b69ce21c7 | |||
| 4f05674cb3 | |||
| feab3f5c86 | |||
| 90c05ec38a | |||
| 7e0eed87cb | |||
| 1a1197f67a | |||
| 3c5afb32f3 | |||
| 76cbd23859 | |||
| b436ae789c | |||
| 275731afc7 | |||
| 5cb2214d88 |
@@ -11,6 +11,7 @@ import {
|
|||||||
keymap,
|
keymap,
|
||||||
lineNumbers,
|
lineNumbers,
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
|
import {repeatCharKeymap} from './repeatcharacter.mjs';
|
||||||
import { persistentAtom } from '@nanostores/persistent';
|
import { persistentAtom } from '@nanostores/persistent';
|
||||||
import { logger, registerControl, repl } from '@strudel/core';
|
import { logger, registerControl, repl } from '@strudel/core';
|
||||||
import { cleanupDraw, cleanupDrawContext, Drawer } from '@strudel/draw';
|
import { cleanupDraw, cleanupDrawContext, Drawer } from '@strudel/draw';
|
||||||
@@ -20,11 +21,16 @@ import { evalBlock } from './block_utilities.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 { jumpToCharacter } from './labelJump.mjs';
|
|
||||||
import { getSliderWidgets, sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
import { getSliderWidgets, sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||||
import { activateTheme, initTheme, theme } from './themes.mjs';
|
import { activateTheme, initTheme, theme } from './themes.mjs';
|
||||||
import { isTooltipEnabled } from './tooltip.mjs';
|
import { isTooltipEnabled } from './tooltip.mjs';
|
||||||
import { getActiveWidgets, updateWidgets, widgetPlugin } from './widget.mjs';
|
import { getActiveWidgets, updateWidgets, widgetPlugin } from './widget.mjs';
|
||||||
|
import {
|
||||||
|
deleteAllInlineBeforeCharacter,
|
||||||
|
InsertCharBeforeChar,
|
||||||
|
jumpToCharacter,
|
||||||
|
jumpToNextCharacter,
|
||||||
|
} from './labelJump.mjs';
|
||||||
|
|
||||||
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
||||||
|
|
||||||
@@ -75,6 +81,10 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS
|
|||||||
decode: JSON.parse,
|
decode: JSON.parse,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ANON_LABEL = '$';
|
||||||
|
const SOLO_LABEL = 'S';
|
||||||
|
const MUTE_LABEL = '_';
|
||||||
|
|
||||||
// https://codemirror.net/docs/guide/
|
// https://codemirror.net/docs/guide/
|
||||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo, strudelMirror }) {
|
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo, strudelMirror }) {
|
||||||
const settings = codemirrorSettings.get();
|
const settings = codemirrorSettings.get();
|
||||||
@@ -102,6 +112,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
|||||||
syntaxHighlighting(defaultHighlightStyle),
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
EditorView.updateListener.of((v) => onChange(v)),
|
EditorView.updateListener.of((v) => onChange(v)),
|
||||||
drawSelection({ cursorBlinkRate: 0 }),
|
drawSelection({ cursorBlinkRate: 0 }),
|
||||||
|
repeatCharKeymap,
|
||||||
Prec.highest(
|
Prec.highest(
|
||||||
keymap.of([
|
keymap.of([
|
||||||
{
|
{
|
||||||
@@ -138,12 +149,74 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Alt-w',
|
key: 'Alt-w',
|
||||||
run: (view) => jumpToCharacter(view, '$', 1),
|
run: (view) => jumpToNextCharacter(view, ANON_LABEL, 1),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Alt-q',
|
key: 'Alt-q',
|
||||||
run: (view) => jumpToCharacter(view, '$', -1),
|
run: (view) => {
|
||||||
|
return jumpToNextCharacter(view, ANON_LABEL, -1);
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
// clear all muted
|
||||||
|
{
|
||||||
|
key: `Alt-Ctrl-0`,
|
||||||
|
run: (view) => {
|
||||||
|
return deleteAllInlineBeforeCharacter(view, MUTE_LABEL + ANON_LABEL);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// clear all solod
|
||||||
|
{
|
||||||
|
key: `Alt-Shift-0`,
|
||||||
|
run: (view) => {
|
||||||
|
return deleteAllInlineBeforeCharacter(view, SOLO_LABEL + ANON_LABEL);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// clear all solo and mute
|
||||||
|
{
|
||||||
|
key: `Ctrl-Shift-0`,
|
||||||
|
run: (view) => {
|
||||||
|
return deleteAllInlineBeforeCharacter(view, ANON_LABEL);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...Array.from({ length: 9 }).map((_, i) => {
|
||||||
|
let num = i + 1;
|
||||||
|
return {
|
||||||
|
key: `Alt-${num}`,
|
||||||
|
run: (view) => {
|
||||||
|
return jumpToCharacter(view, ANON_LABEL, i);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
// handle solo toggles 1-9
|
||||||
|
...Array.from({ length: 9 }).map((_, i) => {
|
||||||
|
let num = i + 1;
|
||||||
|
return {
|
||||||
|
key: `Alt-Shift-${num}`,
|
||||||
|
run: (view) => {
|
||||||
|
return InsertCharBeforeChar(view, ANON_LABEL, SOLO_LABEL, i);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
// handle mute toggles 1-9
|
||||||
|
...Array.from({ length: 9 }).map((_, i) => {
|
||||||
|
let num = i + 1;
|
||||||
|
return {
|
||||||
|
key: `Alt-Ctrl-${num}`,
|
||||||
|
run: (view) => {
|
||||||
|
return InsertCharBeforeChar(view, ANON_LABEL, MUTE_LABEL, i);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
// Handle clearing mutes and solos 1-9
|
||||||
|
...Array.from({ length: 9 }).map((_, i) => {
|
||||||
|
let num = i + 1;
|
||||||
|
return {
|
||||||
|
key: `Ctrl-Shift-${num}`,
|
||||||
|
run: (view) => {
|
||||||
|
return InsertCharBeforeChar(view, ANON_LABEL, '', i);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
/* {
|
/* {
|
||||||
key: 'Ctrl-Shift-.',
|
key: 'Ctrl-Shift-.',
|
||||||
run: () => (onPanic ? onPanic() : onStop?.()),
|
run: () => (onPanic ? onPanic() : onStop?.()),
|
||||||
|
|||||||
@@ -1,18 +1,54 @@
|
|||||||
import { EditorSelection } from '@codemirror/state';
|
import { EditorSelection } from '@codemirror/state';
|
||||||
import { SearchCursor } from '@codemirror/search';
|
import { SearchCursor } from '@codemirror/search';
|
||||||
|
import { EditorView } from '@codemirror/view';
|
||||||
|
import { syntaxTree } from '@codemirror/language';
|
||||||
|
|
||||||
export function jumpToCharacter(view, character, direction = 1) {
|
/**
|
||||||
const { state, dispatch } = view;
|
* gets all of the positions of a character in a document, excluding commented out lines
|
||||||
const pos = state.selection.main.head;
|
* @param { EditorState} state
|
||||||
|
* @param {String} character
|
||||||
|
* @returns {number[]}
|
||||||
|
*/
|
||||||
|
function getCharacterPositions(state, character) {
|
||||||
const cursor = new SearchCursor(state.doc, character);
|
const cursor = new SearchCursor(state.doc, character);
|
||||||
|
|
||||||
let characterPositions = [];
|
const characterPositions = [];
|
||||||
let jumpPos;
|
|
||||||
while (!cursor.next().done) {
|
while (!cursor.next().done) {
|
||||||
|
|
||||||
|
const linestartpos = state.doc.lineAt(cursor.value.to).from
|
||||||
|
if (!isLineCommentedOut(state, linestartpos)) {
|
||||||
|
|
||||||
characterPositions.push(cursor.value.to);
|
characterPositions.push(cursor.value.to);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return characterPositions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLineCommentedOut(state, pos) {
|
||||||
|
|
||||||
|
const line = state.doc.lineAt(pos);
|
||||||
|
// remove white space
|
||||||
|
pos = line.from + line.text.search(/\S/)
|
||||||
|
|
||||||
|
const tree = syntaxTree(state);
|
||||||
|
const node = tree.resolveInner(pos, 1)
|
||||||
|
return node.name.includes("Comment")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* jump to the next character in a document
|
||||||
|
* @param {EditorView} view
|
||||||
|
* @param {String} character
|
||||||
|
* @param {number} direction 0 or 1
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function jumpToNextCharacter(view, character, direction = 1) {
|
||||||
|
const { state, dispatch } = view;
|
||||||
|
const pos = state.selection.main.head;
|
||||||
|
let jumpPos;
|
||||||
|
const characterPositions = getCharacterPositions(state, character);
|
||||||
if (!characterPositions.length) {
|
if (!characterPositions.length) {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
if (direction > 0) {
|
if (direction > 0) {
|
||||||
jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience
|
jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience
|
||||||
@@ -21,11 +57,97 @@ export function jumpToCharacter(view, character, direction = 1) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (jumpPos == null) {
|
if (jumpPos == null) {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
const selection = EditorSelection.cursor(jumpPos - 1);
|
||||||
dispatch({
|
dispatch({
|
||||||
selection: EditorSelection.cursor(jumpPos - 1),
|
selection,
|
||||||
scrollIntoView: true,
|
effects: EditorView.scrollIntoView(
|
||||||
|
selection.head,
|
||||||
|
{ y: "start" }
|
||||||
|
)
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {EditorView} view
|
||||||
|
* @param {String} character
|
||||||
|
* @param {number} index the instance of the character
|
||||||
|
* @returns {true}
|
||||||
|
*/
|
||||||
|
export function jumpToCharacter(view, character, index) {
|
||||||
|
const { state, dispatch } = view;
|
||||||
|
const characterPositions = getCharacterPositions(state, character);
|
||||||
|
const pos = characterPositions.at(index) ?? characterPositions.at(-1);
|
||||||
|
if (pos == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = EditorSelection.cursor(pos - 1);
|
||||||
|
dispatch({
|
||||||
|
selection,
|
||||||
|
effects: EditorView.scrollIntoView(
|
||||||
|
selection.head,
|
||||||
|
{ y: "start" }
|
||||||
|
)
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {EditorView} view
|
||||||
|
* @param {String} character
|
||||||
|
* @returns {true}
|
||||||
|
*/
|
||||||
|
export function deleteAllInlineBeforeCharacter(view, character) {
|
||||||
|
const { state, dispatch } = view;
|
||||||
|
const characterPositions = getCharacterPositions(state, character);
|
||||||
|
|
||||||
|
const changes = [];
|
||||||
|
characterPositions.forEach((pos) => {
|
||||||
|
const line = state.doc.lineAt(pos);
|
||||||
|
if (state.doc.sliceString(line.from, line.from + 2) === COMMENT_STRING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
changes.push({
|
||||||
|
from: line.from,
|
||||||
|
to: pos - 1,
|
||||||
|
insert: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
dispatch({ changes });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {EditorView} view
|
||||||
|
* @param {String} character
|
||||||
|
* @param {String} character2
|
||||||
|
* @param {number} index
|
||||||
|
* @returns {true}
|
||||||
|
*/
|
||||||
|
export function InsertCharBeforeChar(view, character, character2, index) {
|
||||||
|
const { state, dispatch } = view;
|
||||||
|
|
||||||
|
const changes = [];
|
||||||
|
const characterPositions = getCharacterPositions(state, character);
|
||||||
|
const labelpos = characterPositions.at(index) ?? characterPositions.at(-1);
|
||||||
|
const line = state.doc.lineAt(labelpos);
|
||||||
|
|
||||||
|
//delete preceeding characters
|
||||||
|
changes.push({
|
||||||
|
from: line.from,
|
||||||
|
to: labelpos - 1,
|
||||||
|
insert: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
changes.push({
|
||||||
|
insert: character2,
|
||||||
|
from: line.from,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({ changes });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Compartment } from "@codemirror/state";
|
||||||
|
import { keymap } from "@codemirror/view";
|
||||||
|
|
||||||
|
const repeatMode = new Compartment();
|
||||||
|
|
||||||
|
function repeatPreviousChar(times) {
|
||||||
|
return ({ state, dispatch }) => {
|
||||||
|
const { from, empty } = state.selection.main;
|
||||||
|
if (!empty || from === 0) return false;
|
||||||
|
|
||||||
|
const prevChar = state.doc.sliceString(from - 1, from);
|
||||||
|
const text = Array(times).fill(prevChar).join(" ");
|
||||||
|
|
||||||
|
dispatch(state.update({
|
||||||
|
changes: {
|
||||||
|
from,
|
||||||
|
insert: " " + text
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitRepeatMode(view) {
|
||||||
|
view.dispatch({
|
||||||
|
effects: repeatMode.reconfigure([])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const digitBindings = Array.from({ length: 9 }, (_, i) => ({
|
||||||
|
key: String(i + 1),
|
||||||
|
run(view) {
|
||||||
|
repeatPreviousChar(i + 1)(view);
|
||||||
|
exitRepeatMode(view);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const repeatCharKeymap = [
|
||||||
|
|
||||||
|
repeatMode.of([]),
|
||||||
|
|
||||||
|
keymap.of([
|
||||||
|
{
|
||||||
|
key: "Alt-r",
|
||||||
|
run(view) {
|
||||||
|
view.dispatch({
|
||||||
|
effects: repeatMode.reconfigure(keymap.of(digitBindings))
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
];
|
||||||
@@ -10,7 +10,7 @@ import { errorLogger, logger } from './logger.mjs';
|
|||||||
export class Cyclist {
|
export class Cyclist {
|
||||||
constructor({
|
constructor({
|
||||||
interval,
|
interval,
|
||||||
onSchedule,
|
onTrigger,
|
||||||
onToggle,
|
onToggle,
|
||||||
onError,
|
onError,
|
||||||
getTime,
|
getTime,
|
||||||
@@ -63,7 +63,12 @@ export class Cyclist {
|
|||||||
if (hap.hasOnset()) {
|
if (hap.hasOnset()) {
|
||||||
const targetTime =
|
const targetTime =
|
||||||
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
|
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
|
||||||
onSchedule?.(hap, targetTime, this.cps);
|
const duration = hap.duration / this.cps;
|
||||||
|
// the following line is dumb and only here for backwards compatibility
|
||||||
|
// see https://codeberg.org/uzu/strudel/pulls/1004
|
||||||
|
const deadline = targetTime - phase;
|
||||||
|
// this onTrigger has another signature
|
||||||
|
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
|
||||||
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
|
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
|
||||||
this.cps = hap.value.cps;
|
this.cps = hap.value.cps;
|
||||||
this.num_ticks_since_cps_change = 0;
|
this.num_ticks_since_cps_change = 0;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { logger } from './logger.mjs';
|
|||||||
import { ClockCollator, cycleToSeconds } from './util.mjs';
|
import { ClockCollator, cycleToSeconds } from './util.mjs';
|
||||||
|
|
||||||
export class NeoCyclist {
|
export class NeoCyclist {
|
||||||
constructor({ onSchedule, onToggle, getTime }) {
|
constructor({ onTrigger, onToggle, getTime }) {
|
||||||
this.started = false;
|
this.started = false;
|
||||||
this.cps = 0.5;
|
this.cps = 0.5;
|
||||||
this.getTime = getTime; // get absolute time
|
this.getTime = getTime; // get absolute time
|
||||||
@@ -43,7 +43,8 @@ export class NeoCyclist {
|
|||||||
if (hap.hasOnset()) {
|
if (hap.hasOnset()) {
|
||||||
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
|
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
|
||||||
const targetTime = timeUntilTrigger + currentTime + this.latency;
|
const targetTime = timeUntilTrigger + currentTime + this.latency;
|
||||||
onSchedule?.(hap, targetTime, this.cps);
|
const duration = cycleToSeconds(hap.duration, this.cps);
|
||||||
|
onTrigger?.(hap, 0, duration, this.cps, targetTime);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+27
-26
@@ -27,7 +27,6 @@ import {
|
|||||||
import drawLine from './drawLine.mjs';
|
import drawLine from './drawLine.mjs';
|
||||||
import { errorLogger, logger } from './logger.mjs';
|
import { errorLogger, logger } from './logger.mjs';
|
||||||
import { strudelScope } from './evaluate.mjs';
|
import { strudelScope } from './evaluate.mjs';
|
||||||
import { getTime } from './schedulerState.mjs';
|
|
||||||
|
|
||||||
let stringParser;
|
let stringParser;
|
||||||
|
|
||||||
@@ -873,41 +872,23 @@ export class Pattern {
|
|||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// Context methods - ones that deal with metadata
|
// Context methods - ones that deal with metadata
|
||||||
|
|
||||||
/**
|
onTrigger(onTrigger, dominant = true) {
|
||||||
* Attaches a function that is called for each hap onset.
|
|
||||||
* @name onSchedule
|
|
||||||
* @memberof Pattern
|
|
||||||
* @example
|
|
||||||
* s("bd sd").onSchedule((hap, time, cps) => {
|
|
||||||
* console.log('hap scheduled', time, cps);
|
|
||||||
* })
|
|
||||||
*/
|
|
||||||
onSchedule(onSchedule, dominant = true) {
|
|
||||||
return this.withHap((hap) =>
|
return this.withHap((hap) =>
|
||||||
hap.setContext({
|
hap.setContext({
|
||||||
...hap.context,
|
...hap.context,
|
||||||
onSchedule: (scheduleHap, t, cps) => {
|
onTrigger: (...args) => {
|
||||||
// run previously set trigger, if it exists
|
// run previously set trigger, if it exists
|
||||||
hap.context.onSchedule?.(scheduleHap, t, cps);
|
hap.context.onTrigger?.(...args);
|
||||||
onSchedule(scheduleHap, t, cps);
|
onTrigger(...args);
|
||||||
},
|
},
|
||||||
// if dominantScheduler is set to true, the default output (webaudio) will be disabled
|
// if dominantTrigger is set to true, the default output (webaudio) will be disabled
|
||||||
// when using multiple triggers, you cannot flip this flag to false again!
|
// when using multiple triggers, you cannot flip this flag to false again!
|
||||||
// example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same
|
// example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same
|
||||||
dominantScheduler: hap.context.dominantScheduler || dominant,
|
dominantTrigger: hap.context.dominantTrigger || dominant,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// legacy:
|
|
||||||
onTrigger(onTrigger, dominant = true) {
|
|
||||||
console.warn('onTrigger is deprecated.. use onSchedule(hap, time, cps) instead');
|
|
||||||
return this.onSchedule((hap, t, cps) => {
|
|
||||||
const currentTime = getTime();
|
|
||||||
return onTrigger(hap, currentTime, cps, t);
|
|
||||||
}, dominant);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the content of the current event to the console (visible in the side menu).
|
* Writes the content of the current event to the console (visible in the side menu).
|
||||||
* @tags visualization
|
* @tags visualization
|
||||||
@@ -917,7 +898,7 @@ export class Pattern {
|
|||||||
* s("bd sd").log()
|
* s("bd sd").log()
|
||||||
*/
|
*/
|
||||||
log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) {
|
log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) {
|
||||||
return this.onSchedule((...args) => {
|
return this.onTrigger((...args) => {
|
||||||
logger(func(...args), undefined, getData(...args));
|
logger(func(...args), undefined, getData(...args));
|
||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
@@ -3654,6 +3635,26 @@ export const slice = register(
|
|||||||
false, // turns off auto-patternification
|
false, // turns off auto-patternification
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* make something happen on event time
|
||||||
|
* uses browser timeout which is innacurate for audio tasks
|
||||||
|
* @name onTriggerTime
|
||||||
|
* @tags external_io
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* s("bd!8").onTriggerTime((hap) => {console.log(hap)})
|
||||||
|
*/
|
||||||
|
Pattern.prototype.onTriggerTime = function (func) {
|
||||||
|
return this.onTrigger((hap, currentTime, _cps, targetTime) => {
|
||||||
|
const diff = targetTime - currentTime;
|
||||||
|
window.setTimeout(() => {
|
||||||
|
func(hap);
|
||||||
|
}, diff * 1000);
|
||||||
|
}, false);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
|
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
|
||||||
* @name splice
|
* @name splice
|
||||||
|
|||||||
+14
-11
@@ -7,7 +7,7 @@ import {
|
|||||||
setIsStarted,
|
setIsStarted,
|
||||||
setPattern as exposeSchedulerPattern,
|
setPattern as exposeSchedulerPattern,
|
||||||
setTime,
|
setTime,
|
||||||
setSchedulerFunc,
|
setTriggerFunc,
|
||||||
} from './schedulerState.mjs';
|
} from './schedulerState.mjs';
|
||||||
import { evalScope } from './evaluate.mjs';
|
import { evalScope } from './evaluate.mjs';
|
||||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||||
@@ -55,7 +55,7 @@ export function repl({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const schedulerOptions = {
|
const schedulerOptions = {
|
||||||
onSchedule: getScheduler({ defaultOutput }),
|
onTrigger: getTrigger({ defaultOutput, getTime }),
|
||||||
getTime,
|
getTime,
|
||||||
onToggle: (started) => {
|
onToggle: (started) => {
|
||||||
updateState({ started });
|
updateState({ started });
|
||||||
@@ -73,7 +73,7 @@ export function repl({
|
|||||||
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
|
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
|
||||||
const scheduler =
|
const scheduler =
|
||||||
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
|
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
|
||||||
setSchedulerFunc(schedulerOptions.onSchedule);
|
setTriggerFunc(schedulerOptions.onTrigger);
|
||||||
setCpsFunc(() => scheduler.cps);
|
setCpsFunc(() => scheduler.cps);
|
||||||
let pPatterns = {};
|
let pPatterns = {};
|
||||||
let anonymousIndex = 0;
|
let anonymousIndex = 0;
|
||||||
@@ -560,17 +560,20 @@ export function repl({
|
|||||||
return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state };
|
return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getScheduler =
|
export const getTrigger =
|
||||||
({ defaultOutput }) =>
|
({ getTime, defaultOutput }) =>
|
||||||
async (hap, t, cps) => {
|
async (hap, deadline, duration, cps, t) => {
|
||||||
|
// ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
|
||||||
|
// TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
|
||||||
try {
|
try {
|
||||||
if (!hap.context.onSchedule || !hap.context.dominantScheduler) {
|
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||||
await defaultOutput(hap, t, cps);
|
await defaultOutput(hap, deadline, duration, cps, t);
|
||||||
}
|
}
|
||||||
if (hap.context.onSchedule) {
|
if (hap.context.onTrigger) {
|
||||||
await hap.context.onSchedule(hap, t, cps);
|
// call signature of output / onTrigger is different...
|
||||||
|
await hap.context.onTrigger(hap, getTime(), cps, t);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorLogger(err, 'getScheduler');
|
errorLogger(err, 'getTrigger');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
let time;
|
let time;
|
||||||
let cpsFunc;
|
let cpsFunc;
|
||||||
let pattern;
|
let pattern;
|
||||||
let schedulerFunc;
|
let triggerFunc;
|
||||||
let isStarted;
|
let isStarted;
|
||||||
export function getTime() {
|
export function getTime() {
|
||||||
if (!time) {
|
if (!time) {
|
||||||
@@ -36,12 +36,12 @@ export function getPattern() {
|
|||||||
return pattern;
|
return pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setSchedulerFunc(func) {
|
export function setTriggerFunc(func) {
|
||||||
schedulerFunc = func;
|
triggerFunc = func;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSchedulerFunc() {
|
export function getTriggerFunc() {
|
||||||
return schedulerFunc;
|
return triggerFunc;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setIsStarted(val) {
|
export function setIsStarted(val) {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function triggerSpeech(words, lang, voice) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const speak = register('speak', function (lang, voice, pat) {
|
export const speak = register('speak', function (lang, voice, pat) {
|
||||||
return pat.onSchedule((hap) => {
|
return pat.onTrigger((hap) => {
|
||||||
triggerSpeech(hap.value, lang, voice);
|
triggerSpeech(hap.value, lang, voice);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1363,7 +1363,7 @@ describe('Pattern', () => {
|
|||||||
|
|
||||||
// Force a trigger
|
// Force a trigger
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
hap.context?.onSchedule?.(hap);
|
hap.context?.onTrigger?.(hap);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockConsoleLog).toHaveBeenCalledWith(
|
expect(mockConsoleLog).toHaveBeenCalledWith(
|
||||||
@@ -1381,7 +1381,7 @@ describe('Pattern', () => {
|
|||||||
|
|
||||||
// Force a trigger
|
// Force a trigger
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
hap.context?.onSchedule?.(hap);
|
hap.context?.onTrigger?.(hap);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockConsoleLog).toHaveBeenCalledWith(
|
expect(mockConsoleLog).toHaveBeenCalledWith(
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => {
|
|||||||
instrument = instrument || 'triangle';
|
instrument = instrument || 'triangle';
|
||||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||||
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
|
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
|
||||||
return pat.onSchedule((hap, targetTime) => {
|
return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
|
||||||
if (!_csound) {
|
if (!_csound) {
|
||||||
logger('[csound] not loaded yet', 'warning');
|
logger('[csound] not loaded yet', 'warning');
|
||||||
return;
|
return;
|
||||||
@@ -36,10 +36,9 @@ export const csound = register('csound', (instrument, pat) => {
|
|||||||
const controls = Object.entries({ ...hap.value, freq })
|
const controls = Object.entries({ ...hap.value, freq })
|
||||||
.flat()
|
.flat()
|
||||||
.join('/');
|
.join('/');
|
||||||
|
|
||||||
// TODO: find out how to send a precise ctx based time
|
// TODO: find out how to send a precise ctx based time
|
||||||
// http://www.csounds.com/manual/html/i.html
|
// http://www.csounds.com/manual/html/i.html
|
||||||
const timeOffset = targetTime - getAudioContext().currentTime; // latency ?
|
const timeOffset = targetTime - currentTime; // latency ?
|
||||||
//const timeOffset = time_deprecate - getAudioContext().currentTime
|
//const timeOffset = time_deprecate - getAudioContext().currentTime
|
||||||
const params = [
|
const params = [
|
||||||
`"${instrument}"`, // p1: instrument name
|
`"${instrument}"`, // p1: instrument name
|
||||||
@@ -145,7 +144,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
|
|||||||
p1 = `"${instrument}"`;
|
p1 = `"${instrument}"`;
|
||||||
}
|
}
|
||||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||||
return pat.onSchedule((hap, targetTime) => {
|
return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
|
||||||
if (!_csound) {
|
if (!_csound) {
|
||||||
logger('[csound] not loaded yet', 'warning');
|
logger('[csound] not loaded yet', 'warning');
|
||||||
return;
|
return;
|
||||||
@@ -153,7 +152,6 @@ export const csoundm = register('csoundm', (instrument, pat) => {
|
|||||||
if (typeof hap.value !== 'object') {
|
if (typeof hap.value !== 'object') {
|
||||||
throw new Error('csound only support objects as hap values');
|
throw new Error('csound only support objects as hap values');
|
||||||
}
|
}
|
||||||
const currentTime = getAudioContext().currentTime;
|
|
||||||
// Time in seconds counting from now.
|
// Time in seconds counting from now.
|
||||||
const p2 = targetTime - currentTime;
|
const p2 = targetTime - currentTime;
|
||||||
const p3 = hap.duration.valueOf() + 0;
|
const p3 = hap.duration.valueOf() + 0;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { getAudioContext } from '@strudel/webaudio/index.mjs';
|
|
||||||
import { Invoke } from './utils.mjs';
|
import { Invoke } from './utils.mjs';
|
||||||
import { Pattern, getEventOffsetMs, noteToMidi } from '@strudel/core';
|
import { Pattern, getEventOffsetMs, noteToMidi } from '@strudel/core';
|
||||||
|
|
||||||
@@ -7,8 +6,7 @@ const OFF_MESSAGE = 0x80;
|
|||||||
const CC_MESSAGE = 0xb0;
|
const CC_MESSAGE = 0xb0;
|
||||||
|
|
||||||
Pattern.prototype.midi = function (output) {
|
Pattern.prototype.midi = function (output) {
|
||||||
return this.onSchedule((hap, targetTime, cps) => {
|
return this.onTrigger((hap, currentTime, cps, targetTime) => {
|
||||||
const currentTime = getAudioContext().currentTime;
|
|
||||||
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
|
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
|
||||||
//magic number to get audio engine to line up, can probably be calculated somehow
|
//magic number to get audio engine to line up, can probably be calculated somehow
|
||||||
const latencyMs = 34;
|
const latencyMs = 34;
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { Pattern, ClockCollator } from '@strudel/core';
|
import { Pattern, ClockCollator } from '@strudel/core';
|
||||||
import { parseControlsFromHap } from '@strudel/osc/osc.mjs';
|
import { parseControlsFromHap } from 'node_modules/@strudel/osc/osc.mjs';
|
||||||
import { Invoke } from './utils.mjs';
|
import { Invoke } from './utils.mjs';
|
||||||
import { getAudioContext } from '@strudel/webaudio/index.mjs';
|
|
||||||
|
|
||||||
const collator = new ClockCollator({});
|
const collator = new ClockCollator({});
|
||||||
|
|
||||||
export async function oscSchedulerTauri(hap, targetTime, cps = 1) {
|
export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) {
|
||||||
const controls = parseControlsFromHap(hap, cps);
|
const controls = parseControlsFromHap(hap, cps);
|
||||||
const params = [];
|
const params = [];
|
||||||
const currentTime = getAudioContext().currentTime;
|
|
||||||
const timestamp = collator.calculateTimestamp(currentTime, targetTime);
|
const timestamp = collator.calculateTimestamp(currentTime, targetTime);
|
||||||
|
|
||||||
Object.keys(controls).forEach((key) => {
|
Object.keys(controls).forEach((key) => {
|
||||||
@@ -34,5 +32,5 @@ export async function oscSchedulerTauri(hap, targetTime, cps = 1) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
Pattern.prototype.osc = function () {
|
Pattern.prototype.osc = function () {
|
||||||
return this.onSchedule(oscSchedulerTauri);
|
return this.onTrigger(oscTriggerTauri);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/webaudio": "workspace:*",
|
|
||||||
"@tauri-apps/api": "^2.2.0"
|
"@tauri-apps/api": "^2.2.0"
|
||||||
},
|
},
|
||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function initDough() {
|
|||||||
return D.ready;
|
return D.ready;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function doughScheduler(hap, targetTime, cps = 1) {
|
export async function doughTrigger(hap, _currentTime, cps = 1, targetTime) {
|
||||||
const offset = D.context_offset?.[0];
|
const offset = D.context_offset?.[0];
|
||||||
if (!offset) {
|
if (!offset) {
|
||||||
return; // not ready
|
return; // not ready
|
||||||
@@ -71,5 +71,5 @@ export async function doughScheduler(hap, targetTime, cps = 1) {
|
|||||||
export const dough = register('dough', (pat) => {
|
export const dough = register('dough', (pat) => {
|
||||||
initDough();
|
initDough();
|
||||||
ensureMinimalOutput();
|
ensureMinimalOutput();
|
||||||
return pat.onSchedule(doughScheduler);
|
return pat.onTrigger(doughTrigger);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
getIsStarted,
|
getIsStarted,
|
||||||
getPattern,
|
getPattern,
|
||||||
getTime,
|
getTime,
|
||||||
getSchedulerFunc,
|
getTriggerFunc,
|
||||||
isPattern,
|
isPattern,
|
||||||
logger,
|
logger,
|
||||||
ref,
|
ref,
|
||||||
@@ -342,7 +342,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
|||||||
|
|
||||||
ensureMinimalOutput();
|
ensureMinimalOutput();
|
||||||
|
|
||||||
return this.sortHapsByPart().onSchedule((hap, targetTime, cps) => {
|
return this.sortHapsByPart().onTrigger((hap, _currentTime, cps, targetTime) => {
|
||||||
if (!WebMidi.enabled) {
|
if (!WebMidi.enabled) {
|
||||||
logger('Midi not enabled');
|
logger('Midi not enabled');
|
||||||
return;
|
return;
|
||||||
@@ -570,8 +570,8 @@ const kListeners = {};
|
|||||||
|
|
||||||
function _triggerKeyboard(input, cps, now, latencyCycles) {
|
function _triggerKeyboard(input, cps, now, latencyCycles) {
|
||||||
const pattern = getPattern();
|
const pattern = getPattern();
|
||||||
const schedule = getSchedulerFunc();
|
const trigger = getTriggerFunc();
|
||||||
if (!pattern || !schedule) {
|
if (!pattern || !trigger) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const t = now + latencyCycles;
|
const t = now + latencyCycles;
|
||||||
@@ -588,7 +588,8 @@ function _triggerKeyboard(input, cps, now, latencyCycles) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const t = ctxNow + (hap.whole.begin - now) / cps;
|
const t = ctxNow + (hap.whole.begin - now) / cps;
|
||||||
schedule(hap, t, cps);
|
const duration = hap.duration / cps;
|
||||||
|
trigger(hap, t - ctxNow, duration, cps, t);
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -623,7 +624,7 @@ export async function midikeys(input) {
|
|||||||
noteoff ||= noteon && velocity === 0; // handle devices which may use velocity = 0 to signal noteoff
|
noteoff ||= noteon && velocity === 0; // handle devices which may use velocity = 0 to signal noteoff
|
||||||
const key = `${input}_${note}`;
|
const key = `${input}_${note}`;
|
||||||
const cps = getCps() ?? 0.5;
|
const cps = getCps() ?? 0.5;
|
||||||
const triggerAvailable = !!(getPattern() && getSchedulerFunc());
|
const triggerAvailable = !!(getPattern() && getTriggerFunc());
|
||||||
const latencySeconds = triggerAvailable ? 0.01 : 0.06; // avoid missing notes due to cyclist / trigger latency
|
const latencySeconds = triggerAvailable ? 0.01 : 0.06; // avoid missing notes due to cyclist / trigger latency
|
||||||
const now = getTime();
|
const now = getTime();
|
||||||
const t = now + latencySeconds * cps;
|
const t = now + latencySeconds * cps;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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/>.
|
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 { Pattern, getTime, createParams } from '@strudel/core';
|
import { Pattern, isPattern, createParams } from '@strudel/core';
|
||||||
import Paho from 'paho-mqtt';
|
import Paho from 'paho-mqtt';
|
||||||
|
|
||||||
const connections = {};
|
const connections = {};
|
||||||
@@ -80,7 +80,8 @@ Pattern.prototype.mqtt = function (
|
|||||||
}
|
}
|
||||||
cx.connect(props);
|
cx.connect(props);
|
||||||
}
|
}
|
||||||
return this.onSchedule((hap, targetTime, cps) => {
|
return this.withHap((hap) => {
|
||||||
|
const onTrigger = (hap, currentTime, cps, targetTime) => {
|
||||||
let msg_topic = topic;
|
let msg_topic = topic;
|
||||||
if (!cx || !cx.isConnected()) {
|
if (!cx || !cx.isConnected()) {
|
||||||
return;
|
return;
|
||||||
@@ -108,11 +109,13 @@ Pattern.prototype.mqtt = function (
|
|||||||
message = new Paho.Message(message);
|
message = new Paho.Message(message);
|
||||||
message.destinationName = msg_topic;
|
message.destinationName = msg_topic;
|
||||||
|
|
||||||
const offset = (targetTime - getTime() + latency) * 1000;
|
const offset = (targetTime - currentTime + latency) * 1000;
|
||||||
|
|
||||||
window.setTimeout(function () {
|
window.setTimeout(function () {
|
||||||
cx.send(message);
|
cx.send(message);
|
||||||
}, offset);
|
}, offset);
|
||||||
|
};
|
||||||
|
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function parseControlsFromHap(hap, cps) {
|
|||||||
return controls;
|
return controls;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function oscScheduler(hap, targetTime, cps = 1) {
|
export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
|
||||||
const ws = await connect();
|
const ws = await connect();
|
||||||
|
|
||||||
const timeMs = getClockBridge().getPerformanceTime(targetTime);
|
const timeMs = getClockBridge().getPerformanceTime(targetTime);
|
||||||
@@ -90,5 +90,5 @@ export async function oscScheduler(hap, targetTime, cps = 1) {
|
|||||||
*/
|
*/
|
||||||
export const osc = register('osc', (pat) => {
|
export const osc = register('osc', (pat) => {
|
||||||
ensureMinimalOutput();
|
ensureMinimalOutput();
|
||||||
return pat.onSchedule(oscScheduler);
|
return pat.onTrigger(oscTrigger);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
|
/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
|
||||||
import { isTauri } from '../desktopbridge/utils.mjs'; */
|
import { isTauri } from '../desktopbridge/utils.mjs'; */
|
||||||
import { oscScheduler } from './osc.mjs';
|
import { oscTrigger } from './osc.mjs';
|
||||||
|
|
||||||
const scheduler = /* isTauri() ? oscTriggerTauri : */ oscScheduler;
|
const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger;
|
||||||
|
|
||||||
export const superdirtOutput = (hap, targetTime, cps) => scheduler(hap, targetTime, cps);
|
export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => {
|
||||||
|
const currentTime = performance.now() / 1000;
|
||||||
|
return trigger(hap, currentTime, cps, targetTime);
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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/>.
|
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 { Pattern, getTime } from '@strudel/core';
|
import { Pattern, isPattern } from '@strudel/core';
|
||||||
|
|
||||||
var writeMessagers = {};
|
var writeMessagers = {};
|
||||||
var choosing = false;
|
var choosing = false;
|
||||||
@@ -64,10 +64,11 @@ function crc16(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids = false, name = 'default') {
|
Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids = false, name = 'default') {
|
||||||
|
return this.withHap((hap) => {
|
||||||
if (!(name in writeMessagers)) {
|
if (!(name in writeMessagers)) {
|
||||||
getWriter(name, br);
|
getWriter(name, br);
|
||||||
}
|
}
|
||||||
return this.onSchedule((hap, targetTime) => {
|
const onTrigger = (hap, currentTime, _cps, targetTime) => {
|
||||||
var message = '';
|
var message = '';
|
||||||
var chk = 0;
|
var chk = 0;
|
||||||
if (typeof hap.value === 'object') {
|
if (typeof hap.value === 'object') {
|
||||||
@@ -104,10 +105,12 @@ Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids
|
|||||||
} else {
|
} else {
|
||||||
message = hap.value;
|
message = hap.value;
|
||||||
}
|
}
|
||||||
const offset = (targetTime - getTime() + latency) * 1000;
|
const offset = (targetTime - currentTime + latency) * 1000;
|
||||||
|
|
||||||
window.setTimeout(function () {
|
window.setTimeout(function () {
|
||||||
writeMessagers[name](message, chk);
|
writeMessagers[name](message, chk);
|
||||||
}, offset);
|
}, offset);
|
||||||
|
};
|
||||||
|
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getAudioContext, registerSound } from '@strudel/webaudio';
|
|||||||
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
|
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
|
||||||
|
|
||||||
Pattern.prototype.soundfont = function (sf, n = 0) {
|
Pattern.prototype.soundfont = function (sf, n = 0) {
|
||||||
return this.onSchedule((h, targetTime) => {
|
return this.onTrigger((h, ct, cps, targetTime) => {
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
const note = getPlayableNoteValue(h);
|
const note = getPlayableNoteValue(h);
|
||||||
const preset = sf.presets[n % sf.presets.length];
|
const preset = sf.presets[n % sf.presets.length];
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ export const rawdsp = async (code) => {
|
|||||||
worklet.node.connect(ac.destination);
|
worklet.node.connect(ac.destination);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function rawdspScheduler(hap, time, cps) {
|
export function rawdspTrigger(hap, currentTime, cps, targetTime) {
|
||||||
const currentTime = getAudioContext().currentTime;
|
window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps });
|
||||||
window.postMessage({ time, dough: hap.value, currentTime, duration: hap.duration, cps });
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
import { BASE_MIDI_NOTE, getBaseURL, getCommonSampleInfo, noteToFreq, noteToMidi } from './util.mjs';
|
||||||
import { registerSound, registerWaveTable, soundMap } from './index.mjs';
|
import { registerSound, registerWaveTable, soundMap } from './index.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import {
|
import {
|
||||||
@@ -14,6 +14,13 @@ import { logger } from './logger.mjs';
|
|||||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||||
const loadCache = {}; // string: Promise<ArrayBuffer>
|
const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @typedef {Object} SampleMetaData
|
||||||
|
* @property {string} url
|
||||||
|
* @property {midi} number
|
||||||
|
*/
|
||||||
|
|
||||||
export const getCachedBuffer = (url) => bufferCache[url];
|
export const getCachedBuffer = (url) => bufferCache[url];
|
||||||
|
|
||||||
function humanFileSize(bytes, si) {
|
function humanFileSize(bytes, si) {
|
||||||
@@ -55,8 +62,9 @@ export const getDur = getDuration;
|
|||||||
|
|
||||||
export function getSampleInfo(hapValue, bank) {
|
export function getSampleInfo(hapValue, bank) {
|
||||||
const { speed = 1.0 } = hapValue;
|
const { speed = 1.0 } = hapValue;
|
||||||
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
|
const { transpose, url, index, midi, label, baseFrequency } = getCommonSampleInfo(hapValue, bank);
|
||||||
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
|
||||||
|
const playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
||||||
return { transpose, url, index, midi, label, playbackRate };
|
return { transpose, url, index, midi, label, playbackRate };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +187,12 @@ export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '')
|
|||||||
if (baseUrl.startsWith('github:')) {
|
if (baseUrl.startsWith('github:')) {
|
||||||
baseUrl = githubPath(baseUrl, '');
|
baseUrl = githubPath(baseUrl, '');
|
||||||
}
|
}
|
||||||
const fullUrl = (v) => baseUrl + v;
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} v
|
||||||
|
* @returns {SampleMetaData}
|
||||||
|
*/
|
||||||
|
const fullUrl = (v) => ({ url: baseUrl + v, midi: extractMidiNoteFromString(v) });
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
//return [key, value.map(replaceUrl)];
|
//return [key, value.map(replaceUrl)];
|
||||||
value = value.map(fullUrl);
|
value = value.map(fullUrl);
|
||||||
@@ -390,3 +403,16 @@ export function registerSampleSource(key, bank, params) {
|
|||||||
registerSample(key, bank, params);
|
registerSample(key, bank, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractMidiNoteFromString(str) {
|
||||||
|
const regex = /_([a-gA-G])([#b])?([1-9])?\b/;
|
||||||
|
const match = str.match(regex);
|
||||||
|
if (match == null) {
|
||||||
|
return BASE_MIDI_NOTE;
|
||||||
|
}
|
||||||
|
const base = match[1].toUpperCase();
|
||||||
|
const accidental = match[2] ?? '';
|
||||||
|
const octave_str = match[3] ?? '';
|
||||||
|
const parsedVal = base + accidental + octave_str;
|
||||||
|
return noteToMidi(parsedVal);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1048,3 +1048,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const superdoughTrigger = (t, hap, ct, cps) => {
|
||||||
|
superdough(hap, t - ct, hap.duration / cps, cps);
|
||||||
|
};
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
|||||||
export const getAccidentalsOffset = (accidentals) => {
|
export const getAccidentalsOffset = (accidentals) => {
|
||||||
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} note
|
||||||
|
* @param {number} defaultOctave
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
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) {
|
||||||
@@ -29,6 +34,11 @@ export const noteToMidi = (note, defaultOctave = 3) => {
|
|||||||
const offset = getAccidentalsOffset(acc);
|
const offset = getAccidentalsOffset(acc);
|
||||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} n
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
export const midiToFreq = (n) => {
|
export const midiToFreq = (n) => {
|
||||||
return Math.pow(2, (n - 69) / 12) * 440;
|
return Math.pow(2, (n - 69) / 12) * 440;
|
||||||
};
|
};
|
||||||
@@ -38,7 +48,17 @@ export const freqToMidi = (freq) => {
|
|||||||
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
|
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const valueToMidi = (value, fallbackValue) => {
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} note
|
||||||
|
* @param {number} defaultOctave
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const noteToFreq = (note, defaultOctave = 3) => {
|
||||||
|
return midiToFreq(noteToMidi(note, defaultOctave));
|
||||||
|
};
|
||||||
|
function __valueToMidi(value) {
|
||||||
if (typeof value !== 'object') {
|
if (typeof value !== 'object') {
|
||||||
throw new Error('valueToMidi: expected object value');
|
throw new Error('valueToMidi: expected object value');
|
||||||
}
|
}
|
||||||
@@ -52,10 +72,15 @@ export const valueToMidi = (value, fallbackValue) => {
|
|||||||
if (typeof note === 'number') {
|
if (typeof note === 'number') {
|
||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
if (!fallbackValue) {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const valueToMidi = (value, fallbackValue) => {
|
||||||
|
const parsedValue = __valueToMidi(value) ?? fallbackValue;
|
||||||
|
if (parsedValue == null) {
|
||||||
throw new Error('valueToMidi: expected freq or note to be set');
|
throw new Error('valueToMidi: expected freq or note to be set');
|
||||||
}
|
}
|
||||||
return fallbackValue;
|
return parsedValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function nanFallback(value, fallback = 0, silent) {
|
export function nanFallback(value, fallback = 0, silent) {
|
||||||
@@ -84,15 +109,18 @@ export function secondsToCycle(t, cps) {
|
|||||||
// deduces relevant info for sample loading from hap.value and sample definition
|
// deduces relevant info for sample loading from hap.value and sample definition
|
||||||
// it encapsulates the core sampler logic into a pure and synchronous function
|
// it encapsulates the core sampler logic into a pure and synchronous function
|
||||||
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
||||||
|
export const BASE_MIDI_NOTE = 36;
|
||||||
|
|
||||||
export function getCommonSampleInfo(hapValue, bank) {
|
export function getCommonSampleInfo(hapValue, bank) {
|
||||||
const { s, n = 0 } = hapValue;
|
const { s, n = 0 } = hapValue;
|
||||||
let midi = valueToMidi(hapValue, 36);
|
const maybeMidiNote = __valueToMidi(hapValue);
|
||||||
let transpose = midi - 36; // C3 is middle C;
|
const midi = maybeMidiNote ?? BASE_MIDI_NOTE;
|
||||||
let url;
|
let transpose = midi - BASE_MIDI_NOTE; // C3 is middle C;
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
let samplemeta;
|
||||||
if (Array.isArray(bank)) {
|
if (Array.isArray(bank)) {
|
||||||
index = getSoundIndex(n, bank.length);
|
index = getSoundIndex(n, bank.length);
|
||||||
url = bank[index];
|
samplemeta = bank[index];
|
||||||
} else {
|
} else {
|
||||||
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
||||||
// object format will expect keys as notes
|
// object format will expect keys as notes
|
||||||
@@ -104,10 +132,14 @@ export function getCommonSampleInfo(hapValue, bank) {
|
|||||||
);
|
);
|
||||||
transpose = -midiDiff(closest); // semitones to repitch
|
transpose = -midiDiff(closest); // semitones to repitch
|
||||||
index = getSoundIndex(n, bank[closest].length);
|
index = getSoundIndex(n, bank[closest].length);
|
||||||
url = bank[closest][index];
|
samplemeta = bank[closest][index];
|
||||||
}
|
}
|
||||||
const label = `${s}:${index}`;
|
const label = `${s}:${index}`;
|
||||||
return { transpose, url, index, midi, label };
|
if (maybeMidiNote != null) {
|
||||||
|
transpose = transpose + (BASE_MIDI_NOTE - samplemeta.midi);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { transpose, index, midi, label, url: samplemeta.url };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Selects entries from `source` and renames them via `map`
|
/** Selects entries from `source` and renames them via `map`
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const soundMap = new Map();
|
|||||||
const loadedSounds = new Map();
|
const loadedSounds = new Map();
|
||||||
|
|
||||||
Pattern.prototype.supradough = function () {
|
Pattern.prototype.supradough = function () {
|
||||||
return this.onSchedule((hap, begin, cps) => {
|
return this.onTrigger((hap, __, cps, begin) => {
|
||||||
hap.value._begin = begin;
|
hap.value._begin = begin;
|
||||||
hap.value._duration = hap.duration / cps;
|
hap.value._duration = hap.duration / cps;
|
||||||
!doughWorklet && initDoughWorklet();
|
!doughWorklet && initDoughWorklet();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
superdough,
|
superdough,
|
||||||
getAudioContext,
|
getAudioContext,
|
||||||
setLogger,
|
setLogger,
|
||||||
rawdspScheduler,
|
rawdspTrigger,
|
||||||
registerWorklet,
|
registerWorklet,
|
||||||
setAudioContext,
|
setAudioContext,
|
||||||
initAudio,
|
initAudio,
|
||||||
@@ -33,8 +33,7 @@ const hap2value = (hap) => {
|
|||||||
|
|
||||||
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
|
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
|
||||||
// TODO: refactor output callbacks to eliminate deadline
|
// TODO: refactor output callbacks to eliminate deadline
|
||||||
export const webaudioOutput = (hap, t, cps) => {
|
export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => {
|
||||||
const hapDuration = hap.duration / cps;
|
|
||||||
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
|
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -175,7 +174,7 @@ export function webaudioRepl(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype.rawdsp = function () {
|
Pattern.prototype.rawdsp = function () {
|
||||||
return this.onSchedule(rawdspScheduler, 1);
|
return this.onTrigger(rawdspTrigger, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
function audioBufferToWav(buffer, opt) {
|
function audioBufferToWav(buffer, opt) {
|
||||||
@@ -272,23 +271,3 @@ function writeString(view, offset, string) {
|
|||||||
view.setUint8(offset + i, string.charCodeAt(i));
|
view.setUint8(offset + i, string.charCodeAt(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* make something happen on event time
|
|
||||||
* uses browser timeout which is innacurate for audio tasks
|
|
||||||
* @name onTriggerTime
|
|
||||||
* @tags external_io
|
|
||||||
* @memberof Pattern
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* s("bd!8").onTriggerTime((hap) => {console.log(hap)})
|
|
||||||
*/
|
|
||||||
Pattern.prototype.onTriggerTime = function (func) {
|
|
||||||
return this.onSchedule((hap, targetTime) => {
|
|
||||||
const diff = targetTime - getAudioContext().currentTime;
|
|
||||||
window.setTimeout(() => {
|
|
||||||
func(hap);
|
|
||||||
}, diff * 1000);
|
|
||||||
}, false);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -8522,19 +8522,6 @@ exports[`runs examples > example "often" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "onSchedule" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/2 | s:bd ]",
|
|
||||||
"[ 1/2 → 1/1 | s:sd ]",
|
|
||||||
"[ 1/1 → 3/2 | s:bd ]",
|
|
||||||
"[ 3/2 → 2/1 | s:sd ]",
|
|
||||||
"[ 2/1 → 5/2 | s:bd ]",
|
|
||||||
"[ 5/2 → 3/1 | s:sd ]",
|
|
||||||
"[ 3/1 → 7/2 | s:bd ]",
|
|
||||||
"[ 7/2 → 4/1 | s:sd ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "onTriggerTime" example index 0 1`] = `
|
exports[`runs examples > example "onTriggerTime" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | s:bd ]",
|
"[ 0/1 → 1/8 | s:bd ]",
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function MiniRepl({
|
|||||||
tunes,
|
tunes,
|
||||||
hideHeader = false,
|
hideHeader = false,
|
||||||
canvasHeight = 100,
|
canvasHeight = 100,
|
||||||
onSchedule,
|
onTrigger,
|
||||||
punchcard,
|
punchcard,
|
||||||
punchcardLabels = true,
|
punchcardLabels = true,
|
||||||
claviature,
|
claviature,
|
||||||
@@ -58,8 +58,8 @@ export function MiniRepl({
|
|||||||
drawTime,
|
drawTime,
|
||||||
drawContext,
|
drawContext,
|
||||||
editPattern: (pat, id) => {
|
editPattern: (pat, id) => {
|
||||||
if (onSchedule) {
|
if (onTrigger) {
|
||||||
pat = pat.onSchedule(onSchedule, false);
|
pat = pat.onTrigger(onTrigger, false);
|
||||||
}
|
}
|
||||||
if (claviature) {
|
if (claviature) {
|
||||||
pat = pat.onPaint((ctx, time, haps, drawTime) => {
|
pat = pat.onPaint((ctx, time, haps, drawTime) => {
|
||||||
|
|||||||
@@ -186,9 +186,9 @@ These functions are more low level, probably not needed by the live coder.
|
|||||||
|
|
||||||
# Other
|
# Other
|
||||||
|
|
||||||
## onSchedule
|
## onTrigger
|
||||||
|
|
||||||
<JsDoc client:idle name="Pattern#onSchedule" h={0} />
|
<JsDoc client:idle name="Pattern#onTrigger" h={0} />
|
||||||
|
|
||||||
## log
|
## log
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Besides a UI for playback control and meta information, the main part of the REP
|
|||||||
|
|
||||||
1. The user writes and updates code. Each update transpiles and evaluates it to create a `Pattern` instance
|
1. The user writes and updates code. Each update transpiles and evaluates it to create a `Pattern` instance
|
||||||
2. While the REPL is running, the `Scheduler` queries the active `Pattern` by a regular interval, generating `Events` (also known as `Haps` in Strudel) for the next time span.
|
2. While the REPL is running, the `Scheduler` queries the active `Pattern` by a regular interval, generating `Events` (also known as `Haps` in Strudel) for the next time span.
|
||||||
3. For each scheduling tick, all generated `Events` are triggered by calling their `onSchedule` method, which is set by the output.
|
3. For each scheduling tick, all generated `Events` are triggered by calling their `onTrigger` method, which is set by the output.
|
||||||
|
|
||||||
<img src="https://codeberg.org/uzu/strudel/raw/branch/talk/talk/public/strudelflow.png" width="600" />
|
<img src="https://codeberg.org/uzu/strudel/raw/branch/talk/talk/public/strudelflow.png" width="600" />
|
||||||
|
|
||||||
@@ -134,7 +134,10 @@ let minLatency = 0.1; // min time before a hap should trigger
|
|||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const haps = pattern.queryArc(time, time + interval);
|
const haps = pattern.queryArc(time, time + interval);
|
||||||
time += interval; // increment time
|
time += interval; // increment time
|
||||||
haps.forEach((hap) => onSchedule(hap, hap.whole.begin + minLatency));
|
haps.forEach((hap) => {
|
||||||
|
const deadline = hap.whole.begin - time + minLatency;
|
||||||
|
onTrigger(hap, deadline, duration);
|
||||||
|
});
|
||||||
}, interval * 1000); // query each "interval" seconds
|
}, interval * 1000); // query each "interval" seconds
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -178,19 +181,20 @@ This example creates the custom control parameters `x` and `y` which are then us
|
|||||||
|
|
||||||
### Outputs
|
### Outputs
|
||||||
|
|
||||||
Now that we know how the value of an hap is manipulated using control parameters, we can look at how outputs can use that value to generate anything. The scheduler above was calling the `onSchedule` function which is used to implement the output. A very simple version of the web audio output could look like this:
|
Now that we know how the value of an event is manipulated using control parameters, we can look at how outputs can use that value to generate anything. The scheduler above was calling the `onTrigger` function which is used to implement the output. A very simple version of the web audio output could look like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function onSchedule(hap, time, cps) {
|
function onTrigger(hap, deadline, duration) {
|
||||||
const { note } = hap.value;
|
const { note } = hap.value;
|
||||||
|
const time = getAudioContext().currentTime + deadline;
|
||||||
const o = getAudioContext().createOscillator();
|
const o = getAudioContext().createOscillator();
|
||||||
o.frequency.value = getFreq(note);
|
o.frequency.value = getFreq(note);
|
||||||
o.start(time);
|
o.start(time);
|
||||||
o.stop(time + hap.duration / cps);
|
o.stop(time + event.duration);
|
||||||
o.connect(getAudioContext().destination);
|
o.connect(getAudioContext().destination);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The above example will create an `OscillatorNode` for each hap, where the frequency is controlled by the `note` param. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects.
|
The above example will create an `OscillatorNode` for each event, where the frequency is controlled by the `note` param. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects.
|
||||||
|
|
||||||
I want to help, how do I contribute to the [Docs](/technical-manual/docs)?
|
I want to help, how do I contribute to the [Docs](/technical-manual/docs)?
|
||||||
|
|||||||
@@ -628,7 +628,7 @@ export const debugAudiograph = async (argOptions = {}) => {
|
|||||||
|
|
||||||
/*global all*/
|
/*global all*/
|
||||||
all((pat) =>
|
all((pat) =>
|
||||||
pat.onSchedule(async (hap, t, cps) => {
|
pat.onTrigger(async (hap, duration, cps, t) => {
|
||||||
hap_count++;
|
hap_count++;
|
||||||
const key = Object.entries(hap.value)
|
const key = Object.entries(hap.value)
|
||||||
.map((param) => param.join('/'))
|
.map((param) => param.join('/'))
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import {
|
|||||||
userPattern,
|
userPattern,
|
||||||
} from '../../../user_pattern_utils.mjs';
|
} from '../../../user_pattern_utils.mjs';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import { getMetadata } from '../../../metadata_parser.js';
|
|
||||||
import { useExamplePatterns } from '../../useExamplePatterns.jsx';
|
import { useExamplePatterns } from '../../useExamplePatterns.jsx';
|
||||||
import { parseJSON, isUdels } from '../../util.mjs';
|
|
||||||
import { useSettings } from '../../../settings.mjs';
|
import { useSettings } from '../../../settings.mjs';
|
||||||
import { ActionButton } from '../button/action-button.jsx';
|
import { ActionButton } from '../button/action-button.jsx';
|
||||||
import { Pagination } from '../pagination/Pagination.jsx';
|
import { Pagination } from '../pagination/Pagination.jsx';
|
||||||
@@ -20,8 +18,13 @@ import { useDebounce } from '../usedebounce.jsx';
|
|||||||
import cx from '@src/cx.mjs';
|
import cx from '@src/cx.mjs';
|
||||||
import { Textbox } from '@src/repl/components/panel/SettingsTab.jsx';
|
import { Textbox } from '@src/repl/components/panel/SettingsTab.jsx';
|
||||||
|
|
||||||
|
const PATTERN_SORT = {
|
||||||
|
"NEWEST": "most recent",
|
||||||
|
"A-Z": "A-Z",
|
||||||
|
}
|
||||||
|
|
||||||
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
||||||
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
|
const meta = pattern.meta
|
||||||
|
|
||||||
let title = meta.title;
|
let title = meta.title;
|
||||||
if (title == null) {
|
if (title == null) {
|
||||||
@@ -58,6 +61,10 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
|
|||||||
return (
|
return (
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
{Object.values(patterns)
|
{Object.values(patterns)
|
||||||
|
.sort((a, b) => {
|
||||||
|
return (b.meta.title ?? "").localeCompare(a.meta.title ?? "")
|
||||||
|
|
||||||
|
})
|
||||||
.reverse()
|
.reverse()
|
||||||
.map((pattern) => {
|
.map((pattern) => {
|
||||||
const id = pattern.id;
|
const id = pattern.id;
|
||||||
@@ -93,7 +100,7 @@ export function PatternsTab({ context }) {
|
|||||||
}
|
}
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(userPatterns).filter(([_key, pattern]) => {
|
Object.entries(userPatterns).filter(([_key, pattern]) => {
|
||||||
const meta = getMetadata(pattern.code);
|
const meta = pattern.meta;
|
||||||
|
|
||||||
// Search for specific meta keys
|
// Search for specific meta keys
|
||||||
const searchLowercaseTrimmed = search.trim().toLowerCase();
|
const searchLowercaseTrimmed = search.trim().toLowerCase();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { registerSampleSource } from '@strudel/webaudio';
|
import { extractMidiNoteFromString, registerSampleSource } from '@strudel/webaudio';
|
||||||
import { isAudioFile } from './files.mjs';
|
import { isAudioFile } from './files.mjs';
|
||||||
import { logger } from '@strudel/core';
|
import { logger } from '@strudel/core';
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
|
|||||||
Promise.all(
|
Promise.all(
|
||||||
[...soundFiles]
|
[...soundFiles]
|
||||||
.sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }))
|
.sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }))
|
||||||
.map((soundFile, i) => {
|
.map((soundFile) => {
|
||||||
const title = soundFile.title;
|
const title = soundFile.title;
|
||||||
if (!isAudioFile(title)) {
|
if (!isAudioFile(title)) {
|
||||||
return;
|
return;
|
||||||
@@ -58,25 +58,26 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
|
|||||||
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
|
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
|
||||||
const blob = soundFile.blob;
|
const blob = soundFile.blob;
|
||||||
|
|
||||||
return blobToDataUrl(blob).then((soundPath) => {
|
return blobToDataUrl(blob).then((path) => {
|
||||||
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
|
const sampleInfoMap = sounds.get(parentDirectory) ?? new Map();
|
||||||
|
const midi = extractMidiNoteFromString(title);
|
||||||
|
/** @type {import('@strudel/webaudio').SampleMetaData} */
|
||||||
|
const samplemetadata = { url: path, midi };
|
||||||
|
sampleInfoMap.set(title, samplemetadata);
|
||||||
|
|
||||||
titlePathMap.set(title, soundPath);
|
sounds.set(parentDirectory, sampleInfoMap);
|
||||||
|
|
||||||
sounds.set(parentDirectory, titlePathMap);
|
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
sounds.forEach((titlePathMap, key) => {
|
sounds.forEach((sampleInfoMap, key) => {
|
||||||
const value = Array.from(titlePathMap.keys())
|
const bank = Array.from(sampleInfoMap.keys())
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a.localeCompare(b);
|
return a.localeCompare(b);
|
||||||
})
|
})
|
||||||
.map((title) => titlePathMap.get(title));
|
.map((title) => sampleInfoMap.get(title));
|
||||||
|
registerSampleSource(key, bank, { prebake: false });
|
||||||
registerSampleSource(key, value, { prebake: false });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger('imported sounds registered!', 'success');
|
logger('imported sounds registered!', 'success');
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useStore } from '@nanostores/react';
|
|||||||
import { register } from '@strudel/core';
|
import { register } from '@strudel/core';
|
||||||
import { isUdels } from './repl/util.mjs';
|
import { isUdels } from './repl/util.mjs';
|
||||||
import { computed } from 'nanostores';
|
import { computed } from 'nanostores';
|
||||||
|
import { getMetadata } from './metadata_parser';
|
||||||
|
|
||||||
export const audioEngineTargets = {
|
export const audioEngineTargets = {
|
||||||
webaudio: 'webaudio',
|
webaudio: 'webaudio',
|
||||||
@@ -18,6 +19,10 @@ export const soundFilterType = {
|
|||||||
ALL: 'all',
|
ALL: 'all',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PATTERN_SORT = {
|
||||||
|
"NEWEST": "most recent",
|
||||||
|
"A-Z": "A-Z",
|
||||||
|
}
|
||||||
const initialPrebakeScript = `// Prebake script
|
const initialPrebakeScript = `// Prebake script
|
||||||
//
|
//
|
||||||
// This is code that is loaded before your pattern is run.
|
// This is code that is loaded before your pattern is run.
|
||||||
@@ -86,6 +91,7 @@ export const $settings = computed(settingsMap, (state) => {
|
|||||||
Object.keys(userPatterns).forEach((key) => {
|
Object.keys(userPatterns).forEach((key) => {
|
||||||
const data = userPatterns[key];
|
const data = userPatterns[key];
|
||||||
data.id = data.id ?? key;
|
data.id = data.id ?? key;
|
||||||
|
data.meta = getMetadata(data.code)
|
||||||
userPatterns[key] = data;
|
userPatterns[key] = data;
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -138,7 +144,7 @@ export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
|
|||||||
|
|
||||||
const patternSetting = (key) =>
|
const patternSetting = (key) =>
|
||||||
register(key, (value, pat) =>
|
register(key, (value, pat) =>
|
||||||
pat.onSchedule(() => {
|
pat.onTrigger(() => {
|
||||||
value = Array.isArray(value) ? value.join(' ') : value;
|
value = Array.isArray(value) ? value.join(' ') : value;
|
||||||
if (value !== settingsMap.get()[key]) {
|
if (value !== settingsMap.get()[key]) {
|
||||||
settingsMap.setKey(key, value);
|
settingsMap.setKey(key, value);
|
||||||
|
|||||||
Reference in New Issue
Block a user