add onSchedule with simpler args to replace onTrigger

This commit is contained in:
Felix Roos
2026-08-19 18:07:55 +02:00
parent 37d524e639
commit d7fb9fe992
28 changed files with 200 additions and 186 deletions
+2 -7
View File
@@ -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;
+2 -3
View File
@@ -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);
}
});
};
+26 -27
View File
@@ -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
+11 -14
View File
@@ -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');
}
};
+5 -5
View File
@@ -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) {
+1 -1
View File
@@ -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);
});
});
+2 -2
View File
@@ -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(
+5 -3
View File
@@ -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;
+3 -1
View File
@@ -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;
+5 -3
View File
@@ -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);
};
+1
View File
@@ -23,6 +23,7 @@
},
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/webaudio": "workspace:*",
"@tauri-apps/api": "^2.2.0"
},
"homepage": "https://codeberg.org/uzu/strudel#readme",
+2 -2
View File
@@ -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);
});
+6 -7
View File
@@ -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;
+29 -32
View File
@@ -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/>.
*/
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);
});
};
+2 -2
View File
@@ -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);
});
+3 -6
View File
@@ -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);
+40 -43
View File
@@ -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/>.
*/
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);
});
};
+1 -1
View File
@@ -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];
+3 -2
View File
@@ -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 });
}
-4
View File
@@ -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);
};
+1 -1
View File
@@ -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();
+24 -3
View File
@@ -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);
};
+13
View File
@@ -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 ]",
+3 -3
View File
@@ -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) => {
@@ -186,9 +186,9 @@ These functions are more low level, probably not needed by the live coder.
# Other
## onTrigger
## onSchedule
<JsDoc client:idle name="Pattern#onTrigger" h={0} />
<JsDoc client:idle name="Pattern#onSchedule" h={0} />
## log
+6 -10
View File
@@ -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.
<img src="https://codeberg.org/uzu/strudel/raw/branch/talk/talk/public/strudelflow.png" width="600" />
@@ -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)?
+1 -1
View File
@@ -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('/'))
+1 -1
View File
@@ -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);