From d7fb9fe992cf87895c19202726ccffff009805fc Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 19 Aug 2026 18:07:55 +0200 Subject: [PATCH] add onSchedule with simpler args to replace onTrigger --- packages/core/cyclist.mjs | 9 +- packages/core/neocyclist.mjs | 5 +- packages/core/pattern.mjs | 53 ++++++------ packages/core/repl.mjs | 25 +++--- packages/core/schedulerState.mjs | 10 +-- packages/core/speak.mjs | 2 +- packages/core/test/pattern.test.mjs | 4 +- packages/csound/index.mjs | 8 +- packages/desktopbridge/midibridge.mjs | 4 +- packages/desktopbridge/oscbridge.mjs | 8 +- packages/desktopbridge/package.json | 1 + packages/dough/dough.mjs | 4 +- packages/midi/midi.mjs | 13 ++- packages/mqtt/mqtt.mjs | 61 +++++++------- packages/osc/osc.mjs | 4 +- packages/osc/superdirtoutput.js | 9 +- packages/serial/serial.mjs | 83 +++++++++---------- packages/soundfonts/sfumato.mjs | 2 +- packages/superdough/dspworklet.mjs | 5 +- packages/superdough/superdough.mjs | 4 - packages/webaudio/supradough.mjs | 2 +- packages/webaudio/webaudio.mjs | 27 +++++- test/__snapshots__/examples.test.mjs.snap | 13 +++ website/src/docs/MiniRepl.jsx | 6 +- .../src/pages/technical-manual/internals.mdx | 4 +- website/src/pages/technical-manual/repl.mdx | 16 ++-- website/src/repl/audiograph.mjs | 2 +- website/src/settings.mjs | 2 +- 28 files changed, 200 insertions(+), 186 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 59e410c53..19d9dc4a9 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -10,7 +10,7 @@ import { errorLogger, logger } from './logger.mjs'; export class Cyclist { constructor({ interval, - onTrigger, + onSchedule, onToggle, onError, getTime, @@ -63,12 +63,7 @@ export class Cyclist { if (hap.hasOnset()) { const targetTime = (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency; - 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); + onSchedule?.(hap, targetTime, this.cps); if (hap.value.cps !== undefined && this.cps != hap.value.cps) { this.cps = hap.value.cps; this.num_ticks_since_cps_change = 0; diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 3e412074d..61b2e54bd 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -8,7 +8,7 @@ import { logger } from './logger.mjs'; import { ClockCollator, cycleToSeconds } from './util.mjs'; export class NeoCyclist { - constructor({ onTrigger, onToggle, getTime }) { + constructor({ onSchedule, onToggle, getTime }) { this.started = false; this.cps = 0.5; this.getTime = getTime; // get absolute time @@ -43,8 +43,7 @@ export class NeoCyclist { if (hap.hasOnset()) { const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps); const targetTime = timeUntilTrigger + currentTime + this.latency; - const duration = cycleToSeconds(hap.duration, this.cps); - onTrigger?.(hap, 0, duration, this.cps, targetTime); + onSchedule?.(hap, targetTime, this.cps); } }); }; diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 0f5083901..16bd7be5b 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -27,6 +27,7 @@ import { import drawLine from './drawLine.mjs'; import { errorLogger, logger } from './logger.mjs'; import { strudelScope } from './evaluate.mjs'; +import { getTime } from './schedulerState.mjs'; let stringParser; @@ -872,23 +873,41 @@ export class Pattern { ////////////////////////////////////////////////////////////////////// // 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) => hap.setContext({ ...hap.context, - onTrigger: (...args) => { + onSchedule: (scheduleHap, t, cps) => { // run previously set trigger, if it exists - hap.context.onTrigger?.(...args); - onTrigger(...args); + hap.context.onSchedule?.(scheduleHap, t, cps); + onSchedule(scheduleHap, t, cps); }, - // if dominantTrigger is set to true, the default output (webaudio) will be disabled + // if dominantScheduler is set to true, the default output (webaudio) will be disabled // 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 - dominantTrigger: hap.context.dominantTrigger || dominant, + dominantScheduler: hap.context.dominantScheduler || 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). * @tags visualization @@ -898,7 +917,7 @@ export class Pattern { * s("bd sd").log() */ log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) { - return this.onTrigger((...args) => { + return this.onSchedule((...args) => { logger(func(...args), undefined, getData(...args)); }, false); } @@ -3635,26 +3654,6 @@ export const slice = register( 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. * @name splice diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index ff2734967..321dcfc64 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -7,7 +7,7 @@ import { setIsStarted, setPattern as exposeSchedulerPattern, setTime, - setTriggerFunc, + setSchedulerFunc, } from './schedulerState.mjs'; import { evalScope } from './evaluate.mjs'; import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; @@ -55,7 +55,7 @@ export function repl({ }; const schedulerOptions = { - onTrigger: getTrigger({ defaultOutput, getTime }), + onSchedule: getScheduler({ defaultOutput }), getTime, onToggle: (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 const scheduler = sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions); - setTriggerFunc(schedulerOptions.onTrigger); + setSchedulerFunc(schedulerOptions.onSchedule); setCpsFunc(() => scheduler.cps); let pPatterns = {}; let anonymousIndex = 0; @@ -560,20 +560,17 @@ export function repl({ return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state }; } -export const getTrigger = - ({ getTime, defaultOutput }) => - 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 +export const getScheduler = + ({ defaultOutput }) => + async (hap, t, cps) => { try { - if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t); + if (!hap.context.onSchedule || !hap.context.dominantScheduler) { + await defaultOutput(hap, t, cps); } - if (hap.context.onTrigger) { - // call signature of output / onTrigger is different... - await hap.context.onTrigger(hap, getTime(), cps, t); + if (hap.context.onSchedule) { + await hap.context.onSchedule(hap, t, cps); } } catch (err) { - errorLogger(err, 'getTrigger'); + errorLogger(err, 'getScheduler'); } }; diff --git a/packages/core/schedulerState.mjs b/packages/core/schedulerState.mjs index 14b4c9054..4d7221ec6 100644 --- a/packages/core/schedulerState.mjs +++ b/packages/core/schedulerState.mjs @@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th let time; let cpsFunc; let pattern; -let triggerFunc; +let schedulerFunc; let isStarted; export function getTime() { if (!time) { @@ -36,12 +36,12 @@ export function getPattern() { return pattern; } -export function setTriggerFunc(func) { - triggerFunc = func; +export function setSchedulerFunc(func) { + schedulerFunc = func; } -export function getTriggerFunc() { - return triggerFunc; +export function getSchedulerFunc() { + return schedulerFunc; } export function setIsStarted(val) { diff --git a/packages/core/speak.mjs b/packages/core/speak.mjs index e0f60184e..dca182dec 100644 --- a/packages/core/speak.mjs +++ b/packages/core/speak.mjs @@ -32,7 +32,7 @@ function triggerSpeech(words, lang, voice) { } export const speak = register('speak', function (lang, voice, pat) { - return pat.onTrigger((hap) => { + return pat.onSchedule((hap) => { triggerSpeech(hap.value, lang, voice); }); }); diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 4eb18c647..a204c6e0c 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1363,7 +1363,7 @@ describe('Pattern', () => { // Force a trigger haps.forEach((hap) => { - hap.context?.onTrigger?.(hap); + hap.context?.onSchedule?.(hap); }); expect(mockConsoleLog).toHaveBeenCalledWith( @@ -1381,7 +1381,7 @@ describe('Pattern', () => { // Force a trigger haps.forEach((hap) => { - hap.context?.onTrigger?.(hap); + hap.context?.onSchedule?.(hap); }); expect(mockConsoleLog).toHaveBeenCalledWith( diff --git a/packages/csound/index.mjs b/packages/csound/index.mjs index e459f3873..c070aa293 100644 --- a/packages/csound/index.mjs +++ b/packages/csound/index.mjs @@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => { instrument = instrument || 'triangle'; 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) - return pat.onTrigger((hap, currentTime, _cps, targetTime) => { + return pat.onSchedule((hap, targetTime) => { if (!_csound) { logger('[csound] not loaded yet', 'warning'); return; @@ -36,9 +36,10 @@ export const csound = register('csound', (instrument, pat) => { const controls = Object.entries({ ...hap.value, freq }) .flat() .join('/'); + // TODO: find out how to send a precise ctx based time // http://www.csounds.com/manual/html/i.html - const timeOffset = targetTime - currentTime; // latency ? + const timeOffset = targetTime - getAudioContext().currentTime; // latency ? //const timeOffset = time_deprecate - getAudioContext().currentTime const params = [ `"${instrument}"`, // p1: instrument name @@ -144,7 +145,7 @@ export const csoundm = register('csoundm', (instrument, pat) => { p1 = `"${instrument}"`; } init(); // not async to support csound inside other patterns + to be able to call pattern methods after it - return pat.onTrigger((hap, currentTime, _cps, targetTime) => { + return pat.onSchedule((hap, targetTime) => { if (!_csound) { logger('[csound] not loaded yet', 'warning'); return; @@ -152,6 +153,7 @@ export const csoundm = register('csoundm', (instrument, pat) => { if (typeof hap.value !== 'object') { throw new Error('csound only support objects as hap values'); } + const currentTime = getAudioContext().currentTime; // Time in seconds counting from now. const p2 = targetTime - currentTime; const p3 = hap.duration.valueOf() + 0; diff --git a/packages/desktopbridge/midibridge.mjs b/packages/desktopbridge/midibridge.mjs index 2436d8ad7..26c4154f9 100644 --- a/packages/desktopbridge/midibridge.mjs +++ b/packages/desktopbridge/midibridge.mjs @@ -1,3 +1,4 @@ +import { getAudioContext } from '@strudel/webaudio/index.mjs'; import { Invoke } from './utils.mjs'; import { Pattern, getEventOffsetMs, noteToMidi } from '@strudel/core'; @@ -6,7 +7,8 @@ const OFF_MESSAGE = 0x80; const CC_MESSAGE = 0xb0; Pattern.prototype.midi = function (output) { - return this.onTrigger((hap, currentTime, cps, targetTime) => { + return this.onSchedule((hap, targetTime, cps) => { + const currentTime = getAudioContext().currentTime; 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 const latencyMs = 34; diff --git a/packages/desktopbridge/oscbridge.mjs b/packages/desktopbridge/oscbridge.mjs index 2568c78e5..cebe3583f 100644 --- a/packages/desktopbridge/oscbridge.mjs +++ b/packages/desktopbridge/oscbridge.mjs @@ -1,12 +1,14 @@ import { Pattern, ClockCollator } from '@strudel/core'; -import { parseControlsFromHap } from 'node_modules/@strudel/osc/osc.mjs'; +import { parseControlsFromHap } from '@strudel/osc/osc.mjs'; import { Invoke } from './utils.mjs'; +import { getAudioContext } from '@strudel/webaudio/index.mjs'; const collator = new ClockCollator({}); -export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) { +export async function oscSchedulerTauri(hap, targetTime, cps = 1) { const controls = parseControlsFromHap(hap, cps); const params = []; + const currentTime = getAudioContext().currentTime; const timestamp = collator.calculateTimestamp(currentTime, targetTime); Object.keys(controls).forEach((key) => { @@ -32,5 +34,5 @@ export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) { }); } Pattern.prototype.osc = function () { - return this.onTrigger(oscTriggerTauri); + return this.onSchedule(oscSchedulerTauri); }; diff --git a/packages/desktopbridge/package.json b/packages/desktopbridge/package.json index a7352d6b0..32bf4483a 100644 --- a/packages/desktopbridge/package.json +++ b/packages/desktopbridge/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "@strudel/core": "workspace:*", + "@strudel/webaudio": "workspace:*", "@tauri-apps/api": "^2.2.0" }, "homepage": "https://codeberg.org/uzu/strudel#readme", diff --git a/packages/dough/dough.mjs b/packages/dough/dough.mjs index 908138bae..9d9a2e4eb 100644 --- a/packages/dough/dough.mjs +++ b/packages/dough/dough.mjs @@ -37,7 +37,7 @@ export function initDough() { return D.ready; } -export async function doughTrigger(hap, _currentTime, cps = 1, targetTime) { +export async function doughScheduler(hap, targetTime, cps = 1) { const offset = D.context_offset?.[0]; if (!offset) { return; // not ready @@ -71,5 +71,5 @@ export async function doughTrigger(hap, _currentTime, cps = 1, targetTime) { export const dough = register('dough', (pat) => { initDough(); ensureMinimalOutput(); - return pat.onTrigger(doughTrigger); + return pat.onSchedule(doughScheduler); }); diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 68a0d744f..a04dac275 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -13,7 +13,7 @@ import { getIsStarted, getPattern, getTime, - getTriggerFunc, + getSchedulerFunc, isPattern, logger, ref, @@ -342,7 +342,7 @@ Pattern.prototype.midi = function (midiport, options = {}) { ensureMinimalOutput(); - return this.sortHapsByPart().onTrigger((hap, _currentTime, cps, targetTime) => { + return this.sortHapsByPart().onSchedule((hap, targetTime, cps) => { if (!WebMidi.enabled) { logger('Midi not enabled'); return; @@ -570,8 +570,8 @@ const kListeners = {}; function _triggerKeyboard(input, cps, now, latencyCycles) { const pattern = getPattern(); - const trigger = getTriggerFunc(); - if (!pattern || !trigger) { + const schedule = getSchedulerFunc(); + if (!pattern || !schedule) { return false; } const t = now + latencyCycles; @@ -588,8 +588,7 @@ function _triggerKeyboard(input, cps, now, latencyCycles) { return; } const t = ctxNow + (hap.whole.begin - now) / cps; - const duration = hap.duration / cps; - trigger(hap, t - ctxNow, duration, cps, t); + schedule(hap, t, cps); }); return true; @@ -624,7 +623,7 @@ export async function midikeys(input) { noteoff ||= noteon && velocity === 0; // handle devices which may use velocity = 0 to signal noteoff const key = `${input}_${note}`; const cps = getCps() ?? 0.5; - const triggerAvailable = !!(getPattern() && getTriggerFunc()); + const triggerAvailable = !!(getPattern() && getSchedulerFunc()); const latencySeconds = triggerAvailable ? 0.01 : 0.06; // avoid missing notes due to cyclist / trigger latency const now = getTime(); const t = now + latencySeconds * cps; diff --git a/packages/mqtt/mqtt.mjs b/packages/mqtt/mqtt.mjs index c0a958cfd..333a3428a 100644 --- a/packages/mqtt/mqtt.mjs +++ b/packages/mqtt/mqtt.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern, isPattern, createParams } from '@strudel/core'; +import { Pattern, getTime, createParams } from '@strudel/core'; import Paho from 'paho-mqtt'; const connections = {}; @@ -80,42 +80,39 @@ Pattern.prototype.mqtt = function ( } cx.connect(props); } - return this.withHap((hap) => { - const onTrigger = (hap, currentTime, cps, targetTime) => { - let msg_topic = topic; - if (!cx || !cx.isConnected()) { - return; - } - let message = ''; - if (typeof hap.value === 'object') { - let value = hap.value; + return this.onSchedule((hap, targetTime, cps) => { + let msg_topic = topic; + if (!cx || !cx.isConnected()) { + return; + } + let message = ''; + if (typeof hap.value === 'object') { + let value = hap.value; - // Try to take topic from pattern if it's not set - if (typeof msg_topic === 'undefined' && 'topic' in value) { - msg_topic = value.topic; - if (Array.isArray(msg_topic)) { - msg_topic = msg_topic.join('/'); - } - msg_topic = '/' + msg_topic; + // Try to take topic from pattern if it's not set + if (typeof msg_topic === 'undefined' && 'topic' in value) { + msg_topic = value.topic; + if (Array.isArray(msg_topic)) { + msg_topic = msg_topic.join('/'); } - if (add_meta) { - const duration = hap.duration.div(cps); - value = { ...value, duration: duration.valueOf(), cps: cps }; - } - message = JSON.stringify(value); - } else { - message = hap.value; + msg_topic = '/' + msg_topic; } - message = new Paho.Message(message); - message.destinationName = msg_topic; + if (add_meta) { + const duration = hap.duration.div(cps); + value = { ...value, duration: duration.valueOf(), cps: cps }; + } + message = JSON.stringify(value); + } else { + message = hap.value; + } + message = new Paho.Message(message); + message.destinationName = msg_topic; - const offset = (targetTime - currentTime + latency) * 1000; + const offset = (targetTime - getTime() + latency) * 1000; - window.setTimeout(function () { - cx.send(message); - }, offset); - }; - return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true }); + window.setTimeout(function () { + cx.send(message); + }, offset); }); }; diff --git a/packages/osc/osc.mjs b/packages/osc/osc.mjs index 2c0619b4a..0741552de 100644 --- a/packages/osc/osc.mjs +++ b/packages/osc/osc.mjs @@ -55,7 +55,7 @@ export function parseControlsFromHap(hap, cps) { return controls; } -export async function oscTrigger(hap, currentTime, cps = 1, targetTime) { +export async function oscScheduler(hap, targetTime, cps = 1) { const ws = await connect(); const timeMs = getClockBridge().getPerformanceTime(targetTime); @@ -90,5 +90,5 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) { */ export const osc = register('osc', (pat) => { ensureMinimalOutput(); - return pat.onTrigger(oscTrigger); + return pat.onSchedule(oscScheduler); }); diff --git a/packages/osc/superdirtoutput.js b/packages/osc/superdirtoutput.js index a317af8f1..20cc7d95c 100644 --- a/packages/osc/superdirtoutput.js +++ b/packages/osc/superdirtoutput.js @@ -1,10 +1,7 @@ /* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs'; import { isTauri } from '../desktopbridge/utils.mjs'; */ -import { oscTrigger } from './osc.mjs'; +import { oscScheduler } from './osc.mjs'; -const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger; +const scheduler = /* isTauri() ? oscTriggerTauri : */ oscScheduler; -export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => { - const currentTime = performance.now() / 1000; - return trigger(hap, currentTime, cps, targetTime); -}; +export const superdirtOutput = (hap, targetTime, cps) => scheduler(hap, targetTime, cps); diff --git a/packages/serial/serial.mjs b/packages/serial/serial.mjs index e007b24b9..30d39830c 100644 --- a/packages/serial/serial.mjs +++ b/packages/serial/serial.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern, isPattern } from '@strudel/core'; +import { Pattern, getTime } from '@strudel/core'; var writeMessagers = {}; var choosing = false; @@ -64,53 +64,50 @@ function crc16(data) { } Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids = false, name = 'default') { - return this.withHap((hap) => { - if (!(name in writeMessagers)) { - getWriter(name, br); - } - const onTrigger = (hap, currentTime, _cps, targetTime) => { - var message = ''; - var chk = 0; - if (typeof hap.value === 'object') { - if ('action' in hap.value) { - var action = hap.value['action']; + if (!(name in writeMessagers)) { + getWriter(name, br); + } + return this.onSchedule((hap, targetTime) => { + var message = ''; + var chk = 0; + if (typeof hap.value === 'object') { + if ('action' in hap.value) { + var action = hap.value['action']; + if (singlecharids) { + action = action.charAt(0); + } + message += action + '('; + var first = true; + for (var [key, val] of Object.entries(hap.value)) { + if (key === 'action') { + continue; + } + if (first) { + first = false; + } else { + message += ','; + } if (singlecharids) { - action = action.charAt(0); - } - message += action + '('; - var first = true; - for (var [key, val] of Object.entries(hap.value)) { - if (key === 'action') { - continue; - } - if (first) { - first = false; - } else { - message += ','; - } - if (singlecharids) { - key = key.charAt(0); - } - message += key + ':' + val; - } - message += ')'; - if (sendcrc) { - chk = crc16(message); - } - } else { - for (const [key, val] of Object.entries(hap.value)) { - message += `${key}:${val}`; + key = key.charAt(0); } + message += key + ':' + val; + } + message += ')'; + if (sendcrc) { + chk = crc16(message); } } else { - message = hap.value; + for (const [key, val] of Object.entries(hap.value)) { + message += `${key}:${val}`; + } } - const offset = (targetTime - currentTime + latency) * 1000; + } else { + message = hap.value; + } + const offset = (targetTime - getTime() + latency) * 1000; - window.setTimeout(function () { - writeMessagers[name](message, chk); - }, offset); - }; - return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true }); + window.setTimeout(function () { + writeMessagers[name](message, chk); + }, offset); }); }; diff --git a/packages/soundfonts/sfumato.mjs b/packages/soundfonts/sfumato.mjs index 4d5e7ef45..a8aa299c1 100644 --- a/packages/soundfonts/sfumato.mjs +++ b/packages/soundfonts/sfumato.mjs @@ -3,7 +3,7 @@ import { getAudioContext, registerSound } from '@strudel/webaudio'; import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato'; Pattern.prototype.soundfont = function (sf, n = 0) { - return this.onTrigger((h, ct, cps, targetTime) => { + return this.onSchedule((h, targetTime) => { const ctx = getAudioContext(); const note = getPlayableNoteValue(h); const preset = sf.presets[n % sf.presets.length]; diff --git a/packages/superdough/dspworklet.mjs b/packages/superdough/dspworklet.mjs index ed6d0c7b3..d37b67cbf 100644 --- a/packages/superdough/dspworklet.mjs +++ b/packages/superdough/dspworklet.mjs @@ -74,6 +74,7 @@ export const rawdsp = async (code) => { worklet.node.connect(ac.destination); }; -export function rawdspTrigger(hap, currentTime, cps, targetTime) { - window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps }); +export function rawdspScheduler(hap, time, cps) { + const currentTime = getAudioContext().currentTime; + window.postMessage({ time, dough: hap.value, currentTime, duration: hap.duration, cps }); } diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 98d1fb67b..3209e7098 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -1048,7 +1048,3 @@ 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); -}; diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs index d55957ced..83458c54e 100644 --- a/packages/webaudio/supradough.mjs +++ b/packages/webaudio/supradough.mjs @@ -20,7 +20,7 @@ const soundMap = new Map(); const loadedSounds = new Map(); Pattern.prototype.supradough = function () { - return this.onTrigger((hap, __, cps, begin) => { + return this.onSchedule((hap, begin, cps) => { hap.value._begin = begin; hap.value._duration = hap.duration / cps; !doughWorklet && initDoughWorklet(); diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 4f7256fe7..120b97535 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -9,7 +9,7 @@ import { superdough, getAudioContext, setLogger, - rawdspTrigger, + rawdspScheduler, registerWorklet, setAudioContext, initAudio, @@ -33,7 +33,8 @@ const hap2value = (hap) => { // uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004 // TODO: refactor output callbacks to eliminate deadline -export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => { +export const webaudioOutput = (hap, t, cps) => { + const hapDuration = hap.duration / cps; return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf()); }; @@ -174,7 +175,7 @@ export function webaudioRepl(options = {}) { } Pattern.prototype.rawdsp = function () { - return this.onTrigger(rawdspTrigger, 1); + return this.onSchedule(rawdspScheduler, 1); }; function audioBufferToWav(buffer, opt) { @@ -271,3 +272,23 @@ function writeString(view, offset, string) { 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); +}; diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index ccb91377f..f174206ce 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -8522,6 +8522,19 @@ 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`] = ` [ "[ 0/1 → 1/8 | s:bd ]", diff --git a/website/src/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx index 09ebb7692..e29d08b24 100644 --- a/website/src/docs/MiniRepl.jsx +++ b/website/src/docs/MiniRepl.jsx @@ -22,7 +22,7 @@ export function MiniRepl({ tunes, hideHeader = false, canvasHeight = 100, - onTrigger, + onSchedule, punchcard, punchcardLabels = true, claviature, @@ -58,8 +58,8 @@ export function MiniRepl({ drawTime, drawContext, editPattern: (pat, id) => { - if (onTrigger) { - pat = pat.onTrigger(onTrigger, false); + if (onSchedule) { + pat = pat.onSchedule(onSchedule, false); } if (claviature) { pat = pat.onPaint((ctx, time, haps, drawTime) => { diff --git a/website/src/pages/technical-manual/internals.mdx b/website/src/pages/technical-manual/internals.mdx index 2ec74641a..f3fc1122a 100644 --- a/website/src/pages/technical-manual/internals.mdx +++ b/website/src/pages/technical-manual/internals.mdx @@ -186,9 +186,9 @@ These functions are more low level, probably not needed by the live coder. # Other -## onTrigger +## onSchedule - + ## log diff --git a/website/src/pages/technical-manual/repl.mdx b/website/src/pages/technical-manual/repl.mdx index d8a22cf55..4135848ed 100644 --- a/website/src/pages/technical-manual/repl.mdx +++ b/website/src/pages/technical-manual/repl.mdx @@ -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 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 `onTrigger` method, which is set by the output. +3. For each scheduling tick, all generated `Events` are triggered by calling their `onSchedule` method, which is set by the output. @@ -134,10 +134,7 @@ let minLatency = 0.1; // min time before a hap should trigger setInterval(() => { const haps = pattern.queryArc(time, time + interval); time += interval; // increment time - haps.forEach((hap) => { - const deadline = hap.whole.begin - time + minLatency; - onTrigger(hap, deadline, duration); - }); + haps.forEach((hap) => onSchedule(hap, hap.whole.begin + minLatency)); }, interval * 1000); // query each "interval" seconds ``` @@ -181,20 +178,19 @@ This example creates the custom control parameters `x` and `y` which are then us ### Outputs -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: +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: ```js -function onTrigger(hap, deadline, duration) { +function onSchedule(hap, time, cps) { const { note } = hap.value; - const time = getAudioContext().currentTime + deadline; const o = getAudioContext().createOscillator(); o.frequency.value = getFreq(note); o.start(time); - o.stop(time + event.duration); + o.stop(time + hap.duration / cps); o.connect(getAudioContext().destination); } ``` -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. +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. I want to help, how do I contribute to the [Docs](/technical-manual/docs)? diff --git a/website/src/repl/audiograph.mjs b/website/src/repl/audiograph.mjs index 2876388a8..46bee8664 100644 --- a/website/src/repl/audiograph.mjs +++ b/website/src/repl/audiograph.mjs @@ -628,7 +628,7 @@ export const debugAudiograph = async (argOptions = {}) => { /*global all*/ all((pat) => - pat.onTrigger(async (hap, duration, cps, t) => { + pat.onSchedule(async (hap, t, cps) => { hap_count++; const key = Object.entries(hap.value) .map((param) => param.join('/')) diff --git a/website/src/settings.mjs b/website/src/settings.mjs index d2f57afc9..cd883f81f 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -138,7 +138,7 @@ export const setIsZen = (active) => settingsMap.setKey('isZen', !!active); const patternSetting = (key) => register(key, (value, pat) => - pat.onTrigger(() => { + pat.onSchedule(() => { value = Array.isArray(value) ? value.join(' ') : value; if (value !== settingsMap.get()[key]) { settingsMap.setKey(key, value);