Compare commits

...

11 Commits

Author SHA1 Message Date
prezmop d92d93e37d Merge branch 'tidalcycles:main' into prepare 2025-06-01 12:07:24 +00:00
prezmop 685937911c correct the signnature of prepare() in README 2025-05-24 05:55:04 +09:00
prezmop 31a164a09f document prepare() in the README file 2025-05-24 05:40:01 +09:00
prezmop be8b4d7873 make prepare() async 2025-05-24 04:34:02 +09:00
prezmop 60388e5798 lint 2025-04-26 18:09:48 +09:00
prezmop 88823d26b1 formatting 2025-04-26 18:08:07 +09:00
prezmop e4099946cd use prepare in web 2025-04-26 18:05:16 +09:00
prezmop 3a49ba6eb1 use prepare in repl 2025-04-26 18:04:50 +09:00
prezmop eac45d06b3 add prepare to cyclist 2025-04-26 18:03:44 +09:00
prezmop 8674b61c2c add prepare functionality to webaudio 2025-04-26 17:47:54 +09:00
prezmop af27364c45 add prepare() to superdough 2025-04-26 17:47:27 +09:00
7 changed files with 105 additions and 10 deletions
+35
View File
@@ -11,6 +11,7 @@ export class Cyclist {
constructor({ constructor({
interval, interval,
onTrigger, onTrigger,
onPrepare,
onToggle, onToggle,
onError, onError,
getTime, getTime,
@@ -18,6 +19,7 @@ export class Cyclist {
setInterval, setInterval,
clearInterval, clearInterval,
beforeStart, beforeStart,
prepareTime = 4,
}) { }) {
this.started = false; this.started = false;
this.beforeStart = beforeStart; this.beforeStart = beforeStart;
@@ -26,6 +28,8 @@ export class Cyclist {
this.lastTick = 0; // absolute time when last tick (clock callback) happened this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.lastBegin = 0; // query begin of last tick this.lastBegin = 0; // query begin of last tick
this.lastEnd = 0; // query end of last tick this.lastEnd = 0; // query end of last tick
this.preparedUntil = 0;
this.prepareTime = prepareTime;
this.getTime = getTime; // get absolute time this.getTime = getTime; // get absolute time
this.num_cycles_at_cps_change = 0; this.num_cycles_at_cps_change = 0;
this.seconds_at_cps_change; // clock phase when cps was changed this.seconds_at_cps_change; // clock phase when cps was changed
@@ -85,6 +89,33 @@ export class Cyclist {
setInterval, setInterval,
clearInterval, clearInterval,
); );
onPrepare
? (this.prepClock = createClock(
getTime,
(phase, duration, _, t) => {
try {
const start = Math.max(t, this.preparedUntil);
const 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() { now() {
if (!this.started) { if (!this.started) {
@@ -106,16 +137,19 @@ export class Cyclist {
} }
logger('[cyclist] start'); logger('[cyclist] start');
this.clock.start(); this.clock.start();
this.prepClock?.start();
this.setStarted(true); this.setStarted(true);
} }
pause() { pause() {
logger('[cyclist] pause'); logger('[cyclist] pause');
this.clock.pause(); this.clock.pause();
this.prepClock?.pause();
this.setStarted(false); this.setStarted(false);
} }
stop() { stop() {
logger('[cyclist] stop'); logger('[cyclist] stop');
this.clock.stop(); this.clock.stop();
this.prepClock?.stop();
this.lastEnd = 0; this.lastEnd = 0;
this.setStarted(false); this.setStarted(false);
} }
@@ -130,6 +164,7 @@ export class Cyclist {
return; return;
} }
this.cps = cps; this.cps = cps;
this.preparedUntil = 0;
this.num_ticks_since_cps_change = 0; this.num_ticks_since_cps_change = 0;
} }
log(begin, end, haps) { log(begin, end, haps) {
+12
View File
@@ -8,6 +8,7 @@ import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
export function repl({ export function repl({
defaultOutput, defaultOutput,
defaultPrepare,
onEvalError, onEvalError,
beforeEval, beforeEval,
beforeStart, beforeStart,
@@ -47,6 +48,7 @@ export function repl({
const schedulerOptions = { const schedulerOptions = {
onTrigger: getTrigger({ defaultOutput, getTime }), onTrigger: getTrigger({ defaultOutput, getTime }),
onPrepare: getPrepare({ defaultPrepare }),
getTime, getTime,
onToggle: (started) => { onToggle: (started) => {
updateState({ started }); updateState({ started });
@@ -238,3 +240,13 @@ export const getTrigger =
logger(`[cyclist] error: ${err.message}`, 'error'); logger(`[cyclist] error: ${err.message}`, 'error');
} }
}; };
export const getPrepare =
({ defaultPrepare }) =>
async (hap) => {
try {
await defaultPrepare(hap);
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
}
};
+23
View File
@@ -92,6 +92,29 @@ superdough({ s: 'bd', delay: 0.5 }, 0, 1);
- `deadline`: seconds until the sound should play (0 = immediate) - `deadline`: seconds until the sound should play (0 = immediate)
- `duration`: seconds the sound should last. optional for one shot samples, required for synth sounds - `duration`: seconds the sound should last. optional for one shot samples, required for synth sounds
### 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.
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() ### registerSynthSounds()
Loads the default waveforms `sawtooth`, `square`, `triangle` and `sine`. Use them like this: Loads the default waveforms `sawtooth`, `square`, `triangle` and `sine`. Use them like this:
+16 -7
View File
@@ -265,19 +265,28 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
processSampleMap( processSampleMap(
sampleMap, sampleMap,
(key, bank) => (key, bank) =>
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), { registerSound(
type: 'sample', key,
samples: bank, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank),
baseUrl, {
prebake, type: 'sample',
tag, samples: bank,
}), baseUrl,
prebake,
tag,
},
(hapValue) => onPrepareSample(hapValue, bank),
),
baseUrl, baseUrl,
); );
}; };
const cutGroups = []; const cutGroups = [];
export async function onPrepareSample(hapValue, bank, resolveUrl) {
await getSampleBuffer(hapValue, bank, resolveUrl);
}
export async function onTriggerSample(t, value, onended, bank, resolveUrl) { export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
let { let {
s, s,
+12 -2
View File
@@ -30,9 +30,9 @@ export function setMultiChannelOrbits(bool) {
export const soundMap = map(); export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) { export function registerSound(key, onTrigger, data = {}, onPrepare = () => {}) {
key = key.toLowerCase().replace(/\s+/g, '_'); key = key.toLowerCase().replace(/\s+/g, '_');
soundMap.setKey(key, { onTrigger, data }); soundMap.setKey(key, { onTrigger, data, onPrepare });
} }
let gainCurveFunc = (val) => val; let gainCurveFunc = (val) => val;
@@ -757,3 +757,13 @@ export const superdough = async (value, t, hapDuration, cps) => {
export const superdoughTrigger = (t, hap, ct, cps) => { export const superdoughTrigger = (t, hap, ct, cps) => {
superdough(hap, t - ct, hap.duration / cps, cps); superdough(hap, t - ct, hap.duration / cps, cps);
}; };
export const prepare = async (value) => {
const { onPrepare } = getSound(value.s);
if (onPrepare) {
if (value.bank && value.s) {
value.s = `${value.bank}_${value.s}`;
}
await onPrepare(value);
}
};
+4 -1
View File
@@ -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 * 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; const { Pattern, logger, repl } = strudel;
setLogger(logger); setLogger(logger);
@@ -20,6 +20,8 @@ export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(h
export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => export const webaudioOutput = (hap, deadline, hapDuration, cps, t) =>
superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration); superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration);
export const webaudioPrepare = (hap) => prepare(hap2value(hap));
Pattern.prototype.webaudio = function () { Pattern.prototype.webaudio = function () {
return this.onTrigger(webaudioOutputTrigger); return this.onTrigger(webaudioOutputTrigger);
}; };
@@ -28,6 +30,7 @@ export function webaudioRepl(options = {}) {
options = { options = {
getTime: () => getAudioContext().currentTime, getTime: () => getAudioContext().currentTime,
defaultOutput: webaudioOutput, defaultOutput: webaudioOutput,
defaultPrepare: webaudioPrepare,
...options, ...options,
}; };
return repl(options); return repl(options);
+3
View File
@@ -10,6 +10,7 @@ import { transpiler } from '@strudel/transpiler';
import { import {
getAudioContextCurrentTime, getAudioContextCurrentTime,
webaudioOutput, webaudioOutput,
webaudioPrepare,
resetGlobalEffects, resetGlobalEffects,
resetLoadedSounds, resetLoadedSounds,
initAudioOnFirstClick, initAudioOnFirstClick,
@@ -63,6 +64,7 @@ export function useReplContext() {
const { isSyncEnabled, audioEngineTarget } = useSettings(); const { isSyncEnabled, audioEngineTarget } = useSettings();
const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc; const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc;
const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput; const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput;
const defaultPrepare = shouldUseWebaudio ? webaudioPrepare : undefined;
const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds; const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds;
const init = useCallback(() => { const init = useCallback(() => {
@@ -71,6 +73,7 @@ export function useReplContext() {
const editor = new StrudelMirror({ const editor = new StrudelMirror({
sync: isSyncEnabled, sync: isSyncEnabled,
defaultOutput, defaultOutput,
defaultPrepare,
getTime, getTime,
setInterval, setInterval,
clearInterval, clearInterval,