Compare commits

..

10 Commits

38 changed files with 422 additions and 194 deletions
+2 -7
View File
@@ -10,7 +10,7 @@ import { errorLogger, logger } from './logger.mjs';
export class Cyclist { export class Cyclist {
constructor({ constructor({
interval, interval,
onTrigger, onSchedule,
onToggle, onToggle,
onError, onError,
getTime, getTime,
@@ -63,12 +63,7 @@ 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;
const duration = hap.duration / this.cps; onSchedule?.(hap, targetTime, 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;
+2 -3
View File
@@ -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({ onTrigger, onToggle, getTime }) { constructor({ onSchedule, 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,8 +43,7 @@ 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;
const duration = cycleToSeconds(hap.duration, this.cps); onSchedule?.(hap, targetTime, this.cps);
onTrigger?.(hap, 0, duration, this.cps, targetTime);
} }
}); });
}; };
+26 -27
View File
@@ -27,6 +27,7 @@ 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;
@@ -872,23 +873,41 @@ 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,
onTrigger: (...args) => { onSchedule: (scheduleHap, t, cps) => {
// run previously set trigger, if it exists // run previously set trigger, if it exists
hap.context.onTrigger?.(...args); hap.context.onSchedule?.(scheduleHap, t, cps);
onTrigger(...args); 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! // 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
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). * Writes the content of the current event to the console (visible in the side menu).
* @tags visualization * @tags visualization
@@ -898,7 +917,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.onTrigger((...args) => { return this.onSchedule((...args) => {
logger(func(...args), undefined, getData(...args)); logger(func(...args), undefined, getData(...args));
}, false); }, false);
} }
@@ -3635,26 +3654,6 @@ 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
+11 -14
View File
@@ -7,7 +7,7 @@ import {
setIsStarted, setIsStarted,
setPattern as exposeSchedulerPattern, setPattern as exposeSchedulerPattern,
setTime, setTime,
setTriggerFunc, setSchedulerFunc,
} 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 = {
onTrigger: getTrigger({ defaultOutput, getTime }), onSchedule: getScheduler({ defaultOutput }),
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);
setTriggerFunc(schedulerOptions.onTrigger); setSchedulerFunc(schedulerOptions.onSchedule);
setCpsFunc(() => scheduler.cps); setCpsFunc(() => scheduler.cps);
let pPatterns = {}; let pPatterns = {};
let anonymousIndex = 0; let anonymousIndex = 0;
@@ -560,20 +560,17 @@ 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 getTrigger = export const getScheduler =
({ getTime, defaultOutput }) => ({ defaultOutput }) =>
async (hap, deadline, duration, cps, t) => { async (hap, t, cps) => {
// ^ 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.onTrigger || !hap.context.dominantTrigger) { if (!hap.context.onSchedule || !hap.context.dominantScheduler) {
await defaultOutput(hap, deadline, duration, cps, t); await defaultOutput(hap, t, cps);
} }
if (hap.context.onTrigger) { if (hap.context.onSchedule) {
// call signature of output / onTrigger is different... await hap.context.onSchedule(hap, t, cps);
await hap.context.onTrigger(hap, getTime(), cps, t);
} }
} catch (err) { } 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 time;
let cpsFunc; let cpsFunc;
let pattern; let pattern;
let triggerFunc; let schedulerFunc;
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 setTriggerFunc(func) { export function setSchedulerFunc(func) {
triggerFunc = func; schedulerFunc = func;
} }
export function getTriggerFunc() { export function getSchedulerFunc() {
return triggerFunc; return schedulerFunc;
} }
export function setIsStarted(val) { 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) { export const speak = register('speak', function (lang, voice, pat) {
return pat.onTrigger((hap) => { return pat.onSchedule((hap) => {
triggerSpeech(hap.value, lang, voice); triggerSpeech(hap.value, lang, voice);
}); });
}); });
+2 -2
View File
@@ -1363,7 +1363,7 @@ describe('Pattern', () => {
// Force a trigger // Force a trigger
haps.forEach((hap) => { haps.forEach((hap) => {
hap.context?.onTrigger?.(hap); hap.context?.onSchedule?.(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?.onTrigger?.(hap); hap.context?.onSchedule?.(hap);
}); });
expect(mockConsoleLog).toHaveBeenCalledWith( expect(mockConsoleLog).toHaveBeenCalledWith(
+5 -3
View File
@@ -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.onTrigger((hap, currentTime, _cps, targetTime) => { return pat.onSchedule((hap, targetTime) => {
if (!_csound) { if (!_csound) {
logger('[csound] not loaded yet', 'warning'); logger('[csound] not loaded yet', 'warning');
return; return;
@@ -36,9 +36,10 @@ 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 - currentTime; // latency ? const timeOffset = targetTime - getAudioContext().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
@@ -144,7 +145,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.onTrigger((hap, currentTime, _cps, targetTime) => { return pat.onSchedule((hap, targetTime) => {
if (!_csound) { if (!_csound) {
logger('[csound] not loaded yet', 'warning'); logger('[csound] not loaded yet', 'warning');
return; return;
@@ -152,6 +153,7 @@ 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;
+3 -1
View File
@@ -1,3 +1,4 @@
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';
@@ -6,7 +7,8 @@ const OFF_MESSAGE = 0x80;
const CC_MESSAGE = 0xb0; const CC_MESSAGE = 0xb0;
Pattern.prototype.midi = function (output) { 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; 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;
+5 -3
View File
@@ -1,12 +1,14 @@
import { Pattern, ClockCollator } from '@strudel/core'; 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 { Invoke } from './utils.mjs';
import { getAudioContext } from '@strudel/webaudio/index.mjs';
const collator = new ClockCollator({}); 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 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) => {
@@ -32,5 +34,5 @@ export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) {
}); });
} }
Pattern.prototype.osc = function () { Pattern.prototype.osc = function () {
return this.onTrigger(oscTriggerTauri); return this.onSchedule(oscSchedulerTauri);
}; };
+1
View File
@@ -23,6 +23,7 @@
}, },
"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",
+75
View File
@@ -0,0 +1,75 @@
/*
dough.mjs
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/osc/osc.mjs>
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 { register, noteToMidi } from '@strudel/core';
import { Dough, doughsamples } from 'dough-synth';
import { getAudioContext, ensureMinimalOutput } from '@strudel/webaudio';
import wasm from 'dough-synth/dough.wasm?url';
import workletCode from 'dough-synth/dough.js?raw';
Object.assign(globalThis, { doughsamples });
let D;
/**
*
* initializes dough ahead of time. you don't need to use this, but if you do, you can await it, so dough is ready before the pattern starts.
*
* @name initDough
* @tags external_io
* @memberof Pattern
* @example
* await initDough()
* $: chord("<Dm9 Dm11 Dm7>").offset("<-1 0 1 0>").voicing()
* .phaser(3).fm(1.4).fmh(1.01)
* .dough()
*/
export function initDough() {
if (!D || D.audioContext !== getAudioContext()) {
D = new Dough({
wasm,
workletCode,
audioContext: getAudioContext(),
});
}
return D.ready;
}
export async function doughScheduler(hap, targetTime, cps = 1) {
const offset = D.context_offset?.[0];
if (!offset) {
return; // not ready
}
hap.ensureObjectValue();
const event = {
dough: 'play',
...hap.value,
time: targetTime - offset,
duration: hap.duration / cps,
};
if (typeof event.note === 'string') {
event.note = noteToMidi(event.note);
}
D.evaluate(event);
}
/**
*
* Uses dough as the audio engine. more info at https://dough.strudel.cc
*
* @name dough
* @tags external_io
* @memberof Pattern
* @returns Pattern
* @example
* $: chord("<Dm9 Dm11 Dm7>").offset("<-1 0 1 0>").voicing()
* .phaser(3).fm(1.4).fmh(1.01)
* .dough()
*/
export const dough = register('dough', (pat) => {
initDough();
ensureMinimalOutput();
return pat.onSchedule(doughScheduler);
});
+42
View File
@@ -0,0 +1,42 @@
{
"name": "@strudel/dough",
"version": "1.3.2",
"description": "dough synth integration for strudel",
"main": "dough.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/webaudio": "workspace:*",
"dough-synth": "0.2.4"
},
"devDependencies": {
"vite": "^6.0.11"
},
"engines": {
"node": ">=18.0.0"
}
}
+6 -7
View File
@@ -13,7 +13,7 @@ import {
getIsStarted, getIsStarted,
getPattern, getPattern,
getTime, getTime,
getTriggerFunc, getSchedulerFunc,
isPattern, isPattern,
logger, logger,
ref, ref,
@@ -342,7 +342,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
ensureMinimalOutput(); ensureMinimalOutput();
return this.sortHapsByPart().onTrigger((hap, _currentTime, cps, targetTime) => { return this.sortHapsByPart().onSchedule((hap, targetTime, cps) => {
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 trigger = getTriggerFunc(); const schedule = getSchedulerFunc();
if (!pattern || !trigger) { if (!pattern || !schedule) {
return false; return false;
} }
const t = now + latencyCycles; const t = now + latencyCycles;
@@ -588,8 +588,7 @@ function _triggerKeyboard(input, cps, now, latencyCycles) {
return; return;
} }
const t = ctxNow + (hap.whole.begin - now) / cps; const t = ctxNow + (hap.whole.begin - now) / cps;
const duration = hap.duration / cps; schedule(hap, t, cps);
trigger(hap, t - ctxNow, duration, cps, t);
}); });
return true; 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 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() && getTriggerFunc()); const triggerAvailable = !!(getPattern() && getSchedulerFunc());
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;
+3 -6
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/>. 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'; import Paho from 'paho-mqtt';
const connections = {}; const connections = {};
@@ -80,8 +80,7 @@ Pattern.prototype.mqtt = function (
} }
cx.connect(props); cx.connect(props);
} }
return this.withHap((hap) => { return this.onSchedule((hap, targetTime, cps) => {
const onTrigger = (hap, currentTime, cps, targetTime) => {
let msg_topic = topic; let msg_topic = topic;
if (!cx || !cx.isConnected()) { if (!cx || !cx.isConnected()) {
return; return;
@@ -109,13 +108,11 @@ 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 - currentTime + latency) * 1000; const offset = (targetTime - getTime() + latency) * 1000;
window.setTimeout(function () { window.setTimeout(function () {
cx.send(message); cx.send(message);
}, offset); }, offset);
};
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
}); });
}; };
+2 -2
View File
@@ -55,7 +55,7 @@ export function parseControlsFromHap(hap, cps) {
return controls; return controls;
} }
export async function oscTrigger(hap, currentTime, cps = 1, targetTime) { export async function oscScheduler(hap, targetTime, cps = 1) {
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 oscTrigger(hap, currentTime, cps = 1, targetTime) {
*/ */
export const osc = register('osc', (pat) => { export const osc = register('osc', (pat) => {
ensureMinimalOutput(); ensureMinimalOutput();
return pat.onTrigger(oscTrigger); return pat.onSchedule(oscScheduler);
}); });
+3 -6
View File
@@ -1,10 +1,7 @@
/* 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 { 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) => { export const superdirtOutput = (hap, targetTime, cps) => scheduler(hap, targetTime, cps);
const currentTime = performance.now() / 1000;
return trigger(hap, currentTime, cps, targetTime);
};
+3 -6
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/>. 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 writeMessagers = {};
var choosing = false; var choosing = false;
@@ -64,11 +64,10 @@ 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);
} }
const onTrigger = (hap, currentTime, _cps, targetTime) => { return this.onSchedule((hap, targetTime) => {
var message = ''; var message = '';
var chk = 0; var chk = 0;
if (typeof hap.value === 'object') { if (typeof hap.value === 'object') {
@@ -105,12 +104,10 @@ Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids
} else { } else {
message = hap.value; message = hap.value;
} }
const offset = (targetTime - currentTime + latency) * 1000; const offset = (targetTime - getTime() + 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 });
}); });
}; };
+1 -1
View File
@@ -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.onTrigger((h, ct, cps, targetTime) => { return this.onSchedule((h, 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];
+4 -3
View File
@@ -67,13 +67,14 @@ if (typeof window !== 'undefined') {
}); });
} }
export const dough = async (code) => { export const rawdsp = async (code) => {
const ac = getAudioContext(); const ac = getAudioContext();
stop(); stop();
worklet = await dspWorklet(ac, code); worklet = await dspWorklet(ac, code);
worklet.node.connect(ac.destination); worklet.node.connect(ac.destination);
}; };
export function doughTrigger(hap, currentTime, cps, targetTime) { export function rawdspScheduler(hap, time, cps) {
window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, 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);
};
+3 -3
View File
@@ -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.onTrigger((hap, __, cps, begin) => { return this.onSchedule((hap, begin, cps) => {
hap.value._begin = begin; hap.value._begin = begin;
hap.value._duration = hap.duration / cps; hap.value._duration = hap.duration / cps;
!doughWorklet && initDoughWorklet(); !doughWorklet && initDoughWorklet();
@@ -114,11 +114,11 @@ async function fetchSample(url) {
return { channels, sampleRate: buffer.sampleRate }; return { channels, sampleRate: buffer.sampleRate };
} }
export async function doughsamples(sampleMap, baseUrl) { export async function supradoughsamples(sampleMap, baseUrl) {
if (typeof sampleMap === 'string') { if (typeof sampleMap === 'string') {
const [json, base] = await fetchSampleMap(sampleMap); const [json, base] = await fetchSampleMap(sampleMap);
// console.log('json', json, 'base', base); // console.log('json', json, 'base', base);
return doughsamples(json, base); return supradoughsamples(json, base);
} }
Object.entries(sampleMap).map(async ([key, urls]) => { Object.entries(sampleMap).map(async ([key, urls]) => {
if (key !== '_base') { if (key !== '_base') {
+25 -4
View File
@@ -9,7 +9,7 @@ import {
superdough, superdough,
getAudioContext, getAudioContext,
setLogger, setLogger,
doughTrigger, rawdspScheduler,
registerWorklet, registerWorklet,
setAudioContext, setAudioContext,
initAudio, initAudio,
@@ -33,7 +33,8 @@ 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, _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()); return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
}; };
@@ -173,8 +174,8 @@ export function webaudioRepl(options = {}) {
return repl(options); return repl(options);
} }
Pattern.prototype.dough = function () { Pattern.prototype.rawdsp = function () {
return this.onTrigger(doughTrigger, 1); return this.onSchedule(rawdspScheduler, 1);
}; };
function audioBufferToWav(buffer, opt) { function audioBufferToWav(buffer, opt) {
@@ -271,3 +272,23 @@ 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);
};
+37 -1
View File
@@ -303,6 +303,22 @@ importers:
specifier: ^2.2.0 specifier: ^2.2.0
version: 2.2.0 version: 2.2.0
packages/dough:
dependencies:
'@strudel/core':
specifier: workspace:*
version: link:../core
'@strudel/webaudio':
specifier: workspace:*
version: link:../webaudio
dough-synth:
specifier: 0.2.4
version: 0.2.4
devDependencies:
vite:
specifier: ^6.0.11
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
packages/draw: packages/draw:
dependencies: dependencies:
'@strudel/core': '@strudel/core':
@@ -811,6 +827,9 @@ importers:
'@strudel/desktopbridge': '@strudel/desktopbridge':
specifier: workspace:* specifier: workspace:*
version: link:../packages/desktopbridge version: link:../packages/desktopbridge
'@strudel/dough':
specifier: workspace:*
version: link:../packages/dough
'@strudel/draw': '@strudel/draw':
specifier: workspace:* specifier: workspace:*
version: link:../packages/draw version: link:../packages/draw
@@ -2069,6 +2088,7 @@ packages:
'@lerna/create@8.1.9': '@lerna/create@8.1.9':
resolution: {integrity: sha512-DPnl5lPX4v49eVxEbJnAizrpMdMTBz1qykZrAbBul9rfgk531v8oAt+Pm6O/rpAleRombNM7FJb5rYGzBJatOQ==} resolution: {integrity: sha512-DPnl5lPX4v49eVxEbJnAizrpMdMTBz1qykZrAbBul9rfgk531v8oAt+Pm6O/rpAleRombNM7FJb5rYGzBJatOQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
deprecated: This package is an implementation detail of Lerna and is no longer published separately.
'@lezer/common@1.2.3': '@lezer/common@1.2.3':
resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
@@ -3068,6 +3088,7 @@ packages:
'@ungap/structured-clone@1.3.0': '@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
'@vite-pwa/astro@0.5.0': '@vite-pwa/astro@0.5.0':
resolution: {integrity: sha512-Yd3Pug/c1EUQJXWvzYh6eTtoqzmSKcdCqWCcNquZeaD13tLWpBb2FIPJ4HMULVY6+GfxMvrT+OBuMrbHQCvftw==} resolution: {integrity: sha512-Yd3Pug/c1EUQJXWvzYh6eTtoqzmSKcdCqWCcNquZeaD13tLWpBb2FIPJ4HMULVY6+GfxMvrT+OBuMrbHQCvftw==}
@@ -3719,6 +3740,7 @@ packages:
conventional-changelog-core@5.0.1: conventional-changelog-core@5.0.1:
resolution: {integrity: sha512-Rvi5pH+LvgsqGwZPZ3Cq/tz4ty7mjijhr3qR4m9IBXNbxGGYgTVVO+duXzz9aArmHxFtwZ+LRkrNIMDQzgoY4A==} resolution: {integrity: sha512-Rvi5pH+LvgsqGwZPZ3Cq/tz4ty7mjijhr3qR4m9IBXNbxGGYgTVVO+duXzz9aArmHxFtwZ+LRkrNIMDQzgoY4A==}
engines: {node: '>=14'} engines: {node: '>=14'}
deprecated: Deprecated and no longer maintained. Please use conventional-changelog instead.
conventional-changelog-preset-loader@3.0.0: conventional-changelog-preset-loader@3.0.0:
resolution: {integrity: sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA==} resolution: {integrity: sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA==}
@@ -4050,6 +4072,10 @@ packages:
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
dough-synth@0.2.4:
resolution: {integrity: sha512-qMnMKVj1B2ivUgobNCwqJqNzZHOMN+5339OmCOk07zIDEK7iCYdufa+ZDE2BEgmKrWDyhGS7oTNFjHtTwzEv9w==}
hasBin: true
dset@3.1.4: dset@3.1.4:
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -4582,6 +4608,7 @@ packages:
git-raw-commits@3.0.0: git-raw-commits@3.0.0:
resolution: {integrity: sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw==} resolution: {integrity: sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw==}
engines: {node: '>=14'} engines: {node: '>=14'}
deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.
hasBin: true hasBin: true
git-remote-origin-url@2.0.0: git-remote-origin-url@2.0.0:
@@ -4591,6 +4618,7 @@ packages:
git-semver-tags@5.0.1: git-semver-tags@5.0.1:
resolution: {integrity: sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA==} resolution: {integrity: sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA==}
engines: {node: '>=14'} engines: {node: '>=14'}
deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.
hasBin: true hasBin: true
git-up@7.0.0: git-up@7.0.0:
@@ -4618,15 +4646,17 @@ packages:
glob@10.4.5: glob@10.4.5:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true hasBin: true
glob@7.2.3: glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
glob@9.3.5: glob@9.3.5:
resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
globalize@0.1.1: globalize@0.1.1:
resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==} resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==}
@@ -6409,6 +6439,7 @@ packages:
prebuild-install@7.1.1: prebuild-install@7.1.1:
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
engines: {node: '>=10'} engines: {node: '>=10'}
deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
hasBin: true hasBin: true
precinct@12.1.2: precinct@12.1.2:
@@ -7233,6 +7264,7 @@ packages:
tar@6.2.1: tar@6.2.1:
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
engines: {node: '>=10'} engines: {node: '>=10'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
temp-dir@1.0.0: temp-dir@1.0.0:
resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==} resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==}
@@ -7360,6 +7392,7 @@ packages:
tsconfck@3.1.4: tsconfck@3.1.4:
resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==} resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==}
engines: {node: ^18 || >=20} engines: {node: ^18 || >=20}
deprecated: unmaintained
hasBin: true hasBin: true
peerDependencies: peerDependencies:
typescript: ^5.0.0 typescript: ^5.0.0
@@ -7639,6 +7672,7 @@ packages:
uuid@10.0.0: uuid@10.0.0:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
hasBin: true hasBin: true
validate-npm-package-license@3.0.4: validate-npm-package-license@3.0.4:
@@ -11760,6 +11794,8 @@ snapshots:
dotenv@16.4.7: {} dotenv@16.4.7: {}
dough-synth@0.2.4: {}
dset@3.1.4: {} dset@3.1.4: {}
dunder-proto@1.0.1: dunder-proto@1.0.1:
+61
View File
@@ -3422,6 +3422,30 @@ exports[`runs examples > example "djf" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "dough" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:F3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 0/1 → 1/1 | note:C4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 0/1 → 1/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 0/1 → 1/1 | note:E4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 0/1 → 1/1 | note:A4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 1/1 → 2/1 | note:A3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 1/1 → 2/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 1/1 → 2/1 | note:G4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 1/1 → 2/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 2/1 → 3/1 | note:A4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 2/1 → 3/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 2/1 → 3/1 | note:D5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 2/1 → 3/1 | note:F5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 2/1 → 3/1 | note:C6 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 3/1 → 4/1 | note:A3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 3/1 → 4/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 3/1 → 4/1 | note:E4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 3/1 → 4/1 | note:F4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 3/1 → 4/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
]
`;
exports[`runs examples > example "drawLine" example index 0 1`] = `[]`; exports[`runs examples > example "drawLine" example index 0 1`] = `[]`;
exports[`runs examples > example "drive" example index 0 1`] = ` exports[`runs examples > example "drive" example index 0 1`] = `
@@ -5930,6 +5954,30 @@ exports[`runs examples > example "inhabit" example index 1 1`] = `
] ]
`; `;
exports[`runs examples > example "initDough" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:F3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 0/1 → 1/1 | note:C4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 0/1 → 1/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 0/1 → 1/1 | note:E4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 0/1 → 1/1 | note:A4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 1/1 → 2/1 | note:A3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 1/1 → 2/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 1/1 → 2/1 | note:G4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 1/1 → 2/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 2/1 → 3/1 | note:A4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 2/1 → 3/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 2/1 → 3/1 | note:D5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 2/1 → 3/1 | note:F5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 2/1 → 3/1 | note:C6 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 3/1 → 4/1 | note:A3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 3/1 → 4/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 3/1 → 4/1 | note:E4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 3/1 → 4/1 | note:F4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
"[ 3/1 → 4/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
]
`;
exports[`runs examples > example "inside" example index 0 1`] = ` exports[`runs examples > example "inside" example index 0 1`] = `
[ [
"[ 0/1 → 1/8 | note:D3 ]", "[ 0/1 → 1/8 | note:D3 ]",
@@ -8474,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`] = ` exports[`runs examples > example "onTriggerTime" example index 0 1`] = `
[ [
"[ 0/1 → 1/8 | s:bd ]", "[ 0/1 → 1/8 | s:bd ]",
+2
View File
@@ -99,6 +99,7 @@ const toneHelpersMocked = {
'_spectrum', '_spectrum',
'markcss', 'markcss',
'p', 'p',
'dough',
].forEach((mock) => { ].forEach((mock) => {
strudel.Pattern.prototype[mock] = function () { strudel.Pattern.prototype[mock] = function () {
return this; return this;
@@ -174,6 +175,7 @@ evalScope(
getDuration, getDuration,
setcps: id, setcps: id,
setcpm: id, setcpm: id,
initDough: id,
Clock: {}, // whatever Clock: {}, // whatever
}, },
); );
+2 -2
View File
@@ -597,8 +597,8 @@
], ],
"/packages/superdough/dspworklet.mjs": [ "/packages/superdough/dspworklet.mjs": [
"dspWorklet", "dspWorklet",
"dough", "rawdsp",
"doughTrigger" "rawdspTrigger"
], ],
"/packages/superdough/index.mjs": [], "/packages/superdough/index.mjs": [],
"/packages/tonal/tonleiter.mjs": [ "/packages/tonal/tonleiter.mjs": [
+9
View File
@@ -145,4 +145,13 @@ export default defineConfig({
// external: ['fraction.js'], // https://github.com/infusion/Fraction.js/issues/51 // external: ['fraction.js'], // https://github.com/infusion/Fraction.js/issues/51
}, },
}, },
server: {
// these are needed for dough, which uses shared memory
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements
// this is only for the dev server, so the server strudel runs on, also needs those
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'credentialless',
},
},
}); });
+1
View File
@@ -41,6 +41,7 @@
"@strudel/motion": "workspace:*", "@strudel/motion": "workspace:*",
"@strudel/mqtt": "workspace:*", "@strudel/mqtt": "workspace:*",
"@strudel/osc": "workspace:*", "@strudel/osc": "workspace:*",
"@strudel/dough": "workspace:*",
"@strudel/serial": "workspace:*", "@strudel/serial": "workspace:*",
"@strudel/soundfonts": "workspace:*", "@strudel/soundfonts": "workspace:*",
"@strudel/tidal": "workspace:*", "@strudel/tidal": "workspace:*",
+1 -1
View File
@@ -24,7 +24,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
<base href={BASE_URL} /> <base href={BASE_URL} />
<!-- Scrollable a11y code helper --> <!-- Scrollable accessibility code helper -->
<script src={`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script> <script src={`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
<script src="/src/pwa.ts"></script> <script src="/src/pwa.ts"></script>
+1 -1
View File
@@ -23,7 +23,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
<base href={BASE_URL} /> <base href={BASE_URL} />
<!-- Scrollable a11y code helper --> <!-- Scrollable accessibility code helper -->
<script src={`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script> <script src={`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
<script src="/src/pwa.ts"></script> <script src="/src/pwa.ts"></script>
+1 -1
View File
@@ -16,7 +16,7 @@ const imageAlt = frontmatter.image?.alt ?? OPEN_GRAPH.image.alt;
<!-- Page Metadata --> <!-- Page Metadata -->
<link rel="canonical" href={canonicalUrl} /> <link rel="canonical" href={canonicalUrl} />
<!-- OpenGraph Tags --> <!-- og Tags -->
<meta property="og:title" content={formattedContentTitle} /> <meta property="og:title" content={formattedContentTitle} />
<meta property="og:type" content="article" /> <meta property="og:type" content="article" />
<meta property="og:url" content={canonicalUrl} /> <meta property="og:url" content={canonicalUrl} />
+3 -3
View File
@@ -22,7 +22,7 @@ export function MiniRepl({
tunes, tunes,
hideHeader = false, hideHeader = false,
canvasHeight = 100, canvasHeight = 100,
onTrigger, onSchedule,
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 (onTrigger) { if (onSchedule) {
pat = pat.onTrigger(onTrigger, false); pat = pat.onSchedule(onSchedule, 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
## onTrigger ## onSchedule
<JsDoc client:idle name="Pattern#onTrigger" h={0} /> <JsDoc client:idle name="Pattern#onSchedule" h={0} />
## log ## 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 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 `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" /> <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(() => { 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) => { haps.forEach((hap) => onSchedule(hap, hap.whole.begin + minLatency));
const deadline = hap.whole.begin - time + minLatency;
onTrigger(hap, deadline, duration);
});
}, interval * 1000); // query each "interval" seconds }, 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 ### 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 ```js
function onTrigger(hap, deadline, duration) { function onSchedule(hap, time, cps) {
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 + event.duration); o.stop(time + hap.duration / cps);
o.connect(getAudioContext().destination); 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)? 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*/ /*global all*/
all((pat) => all((pat) =>
pat.onTrigger(async (hap, duration, cps, t) => { pat.onSchedule(async (hap, t, cps) => {
hap_count++; hap_count++;
const key = Object.entries(hap.value) const key = Object.entries(hap.value)
.map((param) => param.join('/')) .map((param) => param.join('/'))
+1
View File
@@ -85,6 +85,7 @@ export function loadModules() {
import('@strudel/motion'), import('@strudel/motion'),
import('@strudel/mqtt'), import('@strudel/mqtt'),
import('@strudel/mondo'), import('@strudel/mondo'),
import('@strudel/dough'),
]; ];
if (isTauri()) { if (isTauri()) {
modules = modules.concat([ modules = modules.concat([
+1 -1
View File
@@ -138,7 +138,7 @@ export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
const patternSetting = (key) => const patternSetting = (key) =>
register(key, (value, pat) => register(key, (value, pat) =>
pat.onTrigger(() => { pat.onSchedule(() => {
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);