diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs new file mode 100644 index 00000000..c79cb2ae --- /dev/null +++ b/packages/core/cyclist.mjs @@ -0,0 +1,79 @@ +/* +cyclist.mjs - +Copyright (C) 2022 Strudel contributors - see +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 . +*/ + +// import { ClockWorker } from './clockworker.mjs'; +import createClock from './zyklus.mjs'; + +export class Cyclist { + worker; + pattern; + started = false; + cps = 1; // TODO + getTime; + phase = 0; + constructor({ interval, onTrigger, onError, getTime, latency = 0.1 }) { + this.getTime = getTime; + const round = (x) => Math.round(x * 1000) / 1000; + this.clock = createClock( + getTime, + (phase, duration, tick) => { + if (tick === 0) { + this.origin = phase; + } + const begin = round(phase - this.origin); + this.phase = begin - latency; + const end = round(begin + duration); + const time = getTime(); + try { + const haps = this.pattern.queryArc(begin, end); // get Haps + // console.log('haps', haps.map((hap) => hap.value.n).join(' ')); + haps.forEach((hap) => { + // console.log('hap', hap.value.n, hap.part.begin); + if (hap.part.begin.equals(hap.whole.begin)) { + const deadline = hap.whole.begin + this.origin - time + latency; + const duration = hap.duration * 1; + onTrigger?.(hap, deadline, duration); + } + }); + } catch (e) { + console.warn('scheduler error', e); + onError?.(e); + } + }, // called slightly before each cycle + interval, // duration of each cycle + ); + } + getPhase() { + return this.phase; + } + start() { + if (!this.pattern) { + throw new Error('Scheduler: no pattern set! call .setPattern first.'); + } + this.clock.start(); + this.started = true; + } + pause() { + this.clock.stop(); + delete this.origin; + this.started = false; + } + stop() { + delete this.origin; + this.clock.stop(); + this.started = false; + } + setPattern(pat) { + this.pattern = pat; + } + setCps(cps = 1) { + this.cps = cps; + } + log(begin, end, haps) { + const onsets = haps.filter((h) => h.hasOnset()); + console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`); + } +} diff --git a/packages/core/zyklus.mjs b/packages/core/zyklus.mjs new file mode 100644 index 00000000..31a9533f --- /dev/null +++ b/packages/core/zyklus.mjs @@ -0,0 +1,48 @@ +// will move to https://github.com/felixroos/zyklus +// TODO: started flag + +function createClock( + getTime, + callback, // called slightly before each cycle + duration = 0.05, // duration of each cycle + interval = 0.1, // interval between callbacks + overlap = 0.1, // overlap between callbacks +) { + let tick = 0; // counts callbacks + let phase = 0; // next callback time + let precision = 10 ** 4; // used to round phase + let minLatency = 0.01; + const setDuration = (setter) => (duration = setter(duration)); + overlap = overlap || interval / 2; + const onTick = () => { + const t = getTime(); + const lookahead = t + interval + overlap; // the time window for this tick + if (phase === 0) { + phase = t + minLatency; + } + // callback as long as we're inside the lookahead + while (phase < lookahead) { + phase = Math.round(phase * precision) / precision; + phase >= t && callback(phase, duration, tick); + phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? + phase += duration; // increment phase by duration + tick++; + } + }; + let intervalID; + const start = () => { + onTick(); + intervalID = setInterval(onTick, interval * 1000); + }; + const clear = () => clearInterval(intervalID); + const pause = () => clear(); + const stop = () => { + tick = 0; + phase = 0; + clear(); + }; + const getPhase = () => phase; + // setCallback + return { setDuration, start, stop, pause, duration, getPhase }; +} +export default createClock; diff --git a/packages/react/src/hooks/useStrudel.mjs b/packages/react/src/hooks/useStrudel.mjs index a6653f24..250f3b51 100644 --- a/packages/react/src/hooks/useStrudel.mjs +++ b/packages/react/src/hooks/useStrudel.mjs @@ -1,6 +1,7 @@ -import { Scheduler } from '@strudel.cycles/core'; +// import { Scheduler } from '@strudel.cycles/core'; import { useRef, useCallback, useEffect, useMemo, useState } from 'react'; import { evaluate as _evaluate } from '@strudel.cycles/eval'; +import { Cyclist } from '@strudel.cycles/core/cyclist.mjs'; function useStrudel({ defaultOutput, interval, getTime, code, evalOnMount = false }) { // scheduler @@ -11,7 +12,8 @@ function useStrudel({ defaultOutput, interval, getTime, code, evalOnMount = fals const isDirty = code !== activeCode; // TODO: how / when to remove schedulerError? const scheduler = useMemo( - () => new Scheduler({ interval, onTrigger: defaultOutput, onError: setSchedulerError, getTime }), + // () => new Scheduler({ interval, onTrigger: defaultOutput, onError: setSchedulerError, getTime }), + () => new Cyclist({ interval, onTrigger: defaultOutput, onError: setSchedulerError, getTime }), [defaultOutput, interval], ); const evaluate = useCallback(async () => {