From af27364c457119e00302503a6f2d6bc1e017560d Mon Sep 17 00:00:00 2001 From: prezmop Date: Sat, 26 Apr 2025 17:47:27 +0900 Subject: [PATCH 01/10] add prepare() to superdough --- packages/superdough/sampler.mjs | 23 ++++++++++++++++------- packages/superdough/superdough.mjs | 17 +++++++++++++++-- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 18d1b7797..3ca4e9465 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -265,19 +265,28 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option processSampleMap( sampleMap, (key, bank) => - registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), { - type: 'sample', - samples: bank, - baseUrl, - prebake, - tag, - }), + registerSound( + key, + (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), + { + type: 'sample', + samples: bank, + baseUrl, + prebake, + tag, + }, + (hapValue) => onPrepareSample(hapValue, bank), + ), baseUrl, ); }; const cutGroups = []; +export async function onPrepareSample(hapValue, bank, resolveUrl) { + await getSampleBuffer(hapValue, bank, resolveUrl); +} + export async function onTriggerSample(t, value, onended, bank, resolveUrl) { let { s, diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index e936d38e1..bbc2f9fe1 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -23,9 +23,10 @@ export function setMaxPolyphony(polyphony) { } export const soundMap = map(); -export function registerSound(key, onTrigger, data = {}) { +export function registerSound(key, onTrigger, data = {}, onPrepare = (() => {})) { key = key.toLowerCase().replace(/\s+/g, '_'); - soundMap.setKey(key, { onTrigger, data }); + soundMap.setKey(key, { onTrigger, data, onPrepare }); + } function aliasBankMap(aliasMap) { @@ -721,3 +722,15 @@ export const superdough = async (value, t, hapDuration) => { export const superdoughTrigger = (t, hap, ct, cps) => { superdough(hap, t - ct, hap.duration / cps, cps); }; + +export const prepare = (value) => { + const { + onPrepare, + } = getSound(value.s); + if (onPrepare) { + if (value.bank && value.s) { + value.s = `${value.bank}_${value.s}`; + } + onPrepare(value); + } +} From 8674b61c2c506aefedd33191f91fc7101fe46188 Mon Sep 17 00:00:00 2001 From: prezmop Date: Sat, 26 Apr 2025 17:47:54 +0900 Subject: [PATCH 02/10] add prepare functionality to webaudio --- packages/webaudio/webaudio.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 44a683480..21a2bc826 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import * as strudel from '@strudel/core'; -import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough'; +import { superdough, prepare, getAudioContext, setLogger, doughTrigger } from 'superdough'; const { Pattern, logger, repl } = strudel; setLogger(logger); @@ -20,6 +20,9 @@ export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(h export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration); +export const webaudioPrepare = (hap) => + prepare(hap2value(hap)); + Pattern.prototype.webaudio = function () { return this.onTrigger(webaudioOutputTrigger); }; @@ -28,6 +31,7 @@ export function webaudioRepl(options = {}) { options = { getTime: () => getAudioContext().currentTime, defaultOutput: webaudioOutput, + defaultPrepare: webaudioPrepare, ...options, }; return repl(options); From eac45d06b34edabb60dfd450fd956e4cba74e19e Mon Sep 17 00:00:00 2001 From: prezmop Date: Sat, 26 Apr 2025 18:01:51 +0900 Subject: [PATCH 03/10] add prepare to cyclist --- packages/core/cyclist.mjs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index bd2db1223..5324b5e93 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -11,6 +11,7 @@ export class Cyclist { constructor({ interval, onTrigger, + onPrepare, onToggle, onError, getTime, @@ -18,6 +19,7 @@ export class Cyclist { setInterval, clearInterval, beforeStart, + prepareTime = 4, }) { this.started = false; this.beforeStart = beforeStart; @@ -26,6 +28,8 @@ export class Cyclist { this.lastTick = 0; // absolute time when last tick (clock callback) happened this.lastBegin = 0; // query begin of last tick this.lastEnd = 0; // query end of last tick + this.preparedUntil = 0; + this.prepareTime = prepareTime; this.getTime = getTime; // get absolute time this.num_cycles_at_cps_change = 0; this.seconds_at_cps_change; // clock phase when cps was changed @@ -85,6 +89,31 @@ export class Cyclist { setInterval, clearInterval, ); + + onPrepare? this.prepClock = createClock( + getTime, + (phase, duration, _, t) => { + try { + const start = Math.max(t, this.preparedUntil); + end = t + this.prepareTime; + this.preparedUntil = end; + + const haps = this.pattern.queryArc(start, end, { _cps: 1 }); + + haps.forEach((hap) => { + onPrepare?.(hap); + }) + } catch (e) { + logger(`[cyclist] error: ${e.message}`); + onError?.(e); + } + }, + 1, // duration of each cycle + 1, + 0, + setInterval, + clearInterval, + ) : null; } now() { if (!this.started) { @@ -106,16 +135,19 @@ export class Cyclist { } logger('[cyclist] start'); this.clock.start(); + this.prepClock?.start(); this.setStarted(true); } pause() { logger('[cyclist] pause'); this.clock.pause(); + this.prepClock?.pause(); this.setStarted(false); } stop() { logger('[cyclist] stop'); this.clock.stop(); + this.prepClock?.stop(); this.lastEnd = 0; this.setStarted(false); } @@ -130,6 +162,7 @@ export class Cyclist { return; } this.cps = cps; + this.preparedUntil = 0; this.num_ticks_since_cps_change = 0; } log(begin, end, haps) { From 3a49ba6eb1d1af2edb1b9f882fa67aa9003b3b81 Mon Sep 17 00:00:00 2001 From: prezmop Date: Sat, 26 Apr 2025 18:04:50 +0900 Subject: [PATCH 04/10] use prepare in repl --- packages/core/repl.mjs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index e703909ff..72be00b31 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -8,6 +8,7 @@ import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; export function repl({ defaultOutput, + defaultPrepare, onEvalError, beforeEval, beforeStart, @@ -47,6 +48,7 @@ export function repl({ const schedulerOptions = { onTrigger: getTrigger({ defaultOutput, getTime }), + onPrepare: getPrepare({ defaultPrepare }), getTime, onToggle: (started) => { updateState({ started }); @@ -238,3 +240,13 @@ export const getTrigger = logger(`[cyclist] error: ${err.message}`, 'error'); } }; + +export const getPrepare = + ({defaultPrepare}) => + async (hap) => { + try { + await defaultPrepare(hap); + } catch (err) { + logger(`[cyclist] error: ${err.message}`, 'error'); + } + } From e4099946cddef24f16c1d8ba3515dc2ec995a428 Mon Sep 17 00:00:00 2001 From: prezmop Date: Sat, 26 Apr 2025 18:05:16 +0900 Subject: [PATCH 05/10] use prepare in web --- website/src/repl/useReplContext.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index f0895aaa4..9348fa8c1 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -10,6 +10,7 @@ import { transpiler } from '@strudel/transpiler'; import { getAudioContextCurrentTime, webaudioOutput, + webaudioPrepare, resetGlobalEffects, resetLoadedSounds, initAudioOnFirstClick, @@ -59,6 +60,7 @@ export function useReplContext() { const { isSyncEnabled, audioEngineTarget } = useSettings(); const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc; const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput; + const defaultPrepare = shouldUseWebaudio ? webaudioPrepare : undefined; const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds; const init = useCallback(() => { @@ -67,6 +69,7 @@ export function useReplContext() { const editor = new StrudelMirror({ sync: isSyncEnabled, defaultOutput, + defaultPrepare, getTime, setInterval, clearInterval, From 88823d26b15c7ac90d2b55a03ee4f6d29c5ffb5d Mon Sep 17 00:00:00 2001 From: prezmop Date: Sat, 26 Apr 2025 18:08:07 +0900 Subject: [PATCH 06/10] formatting --- packages/core/cyclist.mjs | 46 ++++++++++++++++-------------- packages/core/repl.mjs | 4 +-- packages/superdough/superdough.mjs | 9 ++---- packages/webaudio/webaudio.mjs | 3 +- 4 files changed, 30 insertions(+), 32 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 5324b5e93..1447a3915 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -90,30 +90,32 @@ export class Cyclist { clearInterval, ); - onPrepare? this.prepClock = createClock( - getTime, - (phase, duration, _, t) => { - try { - const start = Math.max(t, this.preparedUntil); - end = t + this.prepareTime; - this.preparedUntil = end; + onPrepare + ? (this.prepClock = createClock( + getTime, + (phase, duration, _, t) => { + try { + const start = Math.max(t, this.preparedUntil); + end = t + this.prepareTime; + this.preparedUntil = end; - const haps = this.pattern.queryArc(start, end, { _cps: 1 }); + const haps = this.pattern.queryArc(start, end, { _cps: 1 }); - haps.forEach((hap) => { - onPrepare?.(hap); - }) - } catch (e) { - logger(`[cyclist] error: ${e.message}`); - onError?.(e); - } - }, - 1, // duration of each cycle - 1, - 0, - setInterval, - clearInterval, - ) : null; + haps.forEach((hap) => { + onPrepare?.(hap); + }); + } catch (e) { + logger(`[cyclist] error: ${e.message}`); + onError?.(e); + } + }, + 1, // duration of each cycle + 1, + 0, + setInterval, + clearInterval, + )) + : null; } now() { if (!this.started) { diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 72be00b31..b50451d0b 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -242,11 +242,11 @@ export const getTrigger = }; export const getPrepare = - ({defaultPrepare}) => + ({ defaultPrepare }) => async (hap) => { try { await defaultPrepare(hap); } catch (err) { logger(`[cyclist] error: ${err.message}`, 'error'); } - } + }; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index bbc2f9fe1..2a1377803 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -23,10 +23,9 @@ export function setMaxPolyphony(polyphony) { } export const soundMap = map(); -export function registerSound(key, onTrigger, data = {}, onPrepare = (() => {})) { +export function registerSound(key, onTrigger, data = {}, onPrepare = () => {}) { key = key.toLowerCase().replace(/\s+/g, '_'); soundMap.setKey(key, { onTrigger, data, onPrepare }); - } function aliasBankMap(aliasMap) { @@ -724,13 +723,11 @@ export const superdoughTrigger = (t, hap, ct, cps) => { }; export const prepare = (value) => { - const { - onPrepare, - } = getSound(value.s); + const { onPrepare } = getSound(value.s); if (onPrepare) { if (value.bank && value.s) { value.s = `${value.bank}_${value.s}`; } onPrepare(value); } -} +}; diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 21a2bc826..97dd89694 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -20,8 +20,7 @@ export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(h export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration); -export const webaudioPrepare = (hap) => - prepare(hap2value(hap)); +export const webaudioPrepare = (hap) => prepare(hap2value(hap)); Pattern.prototype.webaudio = function () { return this.onTrigger(webaudioOutputTrigger); From 60388e57985d4599946c2b82aa8f904a110bea51 Mon Sep 17 00:00:00 2001 From: prezmop Date: Sat, 26 Apr 2025 18:09:48 +0900 Subject: [PATCH 07/10] lint --- packages/core/cyclist.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 1447a3915..1a9ed8e74 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -96,7 +96,7 @@ export class Cyclist { (phase, duration, _, t) => { try { const start = Math.max(t, this.preparedUntil); - end = t + this.prepareTime; + const end = t + this.prepareTime; this.preparedUntil = end; const haps = this.pattern.queryArc(start, end, { _cps: 1 }); From be8b4d787355e7ab469b582c82b5687e77b7e4c5 Mon Sep 17 00:00:00 2001 From: prezmop <60825986+prezmop@users.noreply.github.com> Date: Sat, 24 May 2025 04:34:02 +0900 Subject: [PATCH 08/10] make prepare() async --- packages/superdough/superdough.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 2a1377803..91c1a4bbb 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -722,12 +722,12 @@ export const superdoughTrigger = (t, hap, ct, cps) => { superdough(hap, t - ct, hap.duration / cps, cps); }; -export const prepare = (value) => { +export const prepare = async (value) => { const { onPrepare } = getSound(value.s); if (onPrepare) { if (value.bank && value.s) { value.s = `${value.bank}_${value.s}`; } - onPrepare(value); + await onPrepare(value); } }; From 31a164a09f70cc02b57333a7824af6f08495c651 Mon Sep 17 00:00:00 2001 From: prezmop <60825986+prezmop@users.noreply.github.com> Date: Sat, 24 May 2025 05:37:57 +0900 Subject: [PATCH 09/10] document prepare() in the README file --- packages/superdough/README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/superdough/README.md b/packages/superdough/README.md index 0c6e4f14c..89b811b93 100644 --- a/packages/superdough/README.md +++ b/packages/superdough/README.md @@ -92,6 +92,29 @@ superdough({ s: 'bd', delay: 0.5 }, 0, 1); - `deadline`: seconds until the sound should play (0 = immediate) - `duration`: seconds the sound should last. optional for one shot samples, required for synth sounds +### prepare() + +Informs superdough that a sound will be needed in the future. +If the sound is a sample that is not loaded yet, it will be fetched. +Otherwise does nothing. +`value` has a syntax identical to the one used `superdough()`. + +```js +prepare({ s: 'bd', delay: 0.5 }); + +// some time later + +superdough({ s: 'bd', delay: 0.5 }, 0, 1); +``` + +Can be awaited to ensure that a given sound is ready to play. + +```js +const sound = { s: 'hh' }; +await prepare(sound); +superdough(sound, 0, 1); +``` + ### registerSynthSounds() Loads the default waveforms `sawtooth`, `square`, `triangle` and `sine`. Use them like this: From 685937911c5a4cac6e3c19dc76ce4c609fdf4a51 Mon Sep 17 00:00:00 2001 From: prezmop <60825986+prezmop@users.noreply.github.com> Date: Sat, 24 May 2025 05:55:04 +0900 Subject: [PATCH 10/10] correct the signnature of prepare() in README --- packages/superdough/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/README.md b/packages/superdough/README.md index 89b811b93..bb9787eca 100644 --- a/packages/superdough/README.md +++ b/packages/superdough/README.md @@ -92,7 +92,7 @@ superdough({ s: 'bd', delay: 0.5 }, 0, 1); - `deadline`: seconds until the sound should play (0 = immediate) - `duration`: seconds the sound should last. optional for one shot samples, required for synth sounds -### prepare() +### prepare(value) Informs superdough that a sound will be needed in the future. If the sound is a sample that is not loaded yet, it will be fetched.