diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 9b522f05..7ffca4a5 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -10,28 +10,35 @@ import { logger } from './logger.mjs'; export class Cyclist { constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) { this.started = false; - this.cps = 1; // TODO - this.phase = 0; - this.getTime = getTime; + this.cps = 1; + 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.getTime = getTime; // get absolute time this.onToggle = onToggle; - this.latency = latency; + this.latency = latency; // fixed trigger time offset const round = (x) => Math.round(x * 1000) / 1000; this.clock = createClock( getTime, + // called slightly before each cycle (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 + const time = getTime(); + const begin = this.lastEnd; + this.lastBegin = begin; + const end = round(begin + duration * this.cps); + this.lastEnd = end; + const haps = this.pattern.queryArc(begin, end); + const tickdeadline = phase - time; // time left till phase begins + this.lastTick = time + tickdeadline; + haps.forEach((hap) => { if (hap.part.begin.equals(hap.whole.begin)) { - const deadline = hap.whole.begin + this.origin - time + latency; - const duration = hap.duration * 1; + const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency; + const duration = hap.duration / this.cps; onTrigger?.(hap, deadline, duration); } }); @@ -39,15 +46,13 @@ export class Cyclist { logger(`[cyclist] error: ${e.message}`); onError?.(e); } - }, // called slightly before each cycle + }, interval, // duration of each cycle ); } - getPhase() { - return this.getTime() - this.origin - this.latency; - } now() { - return this.getTime() - this.origin + this.clock.minLatency; + const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration; + return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; } setStarted(v) { this.started = v; diff --git a/packages/core/zyklus.mjs b/packages/core/zyklus.mjs index e66d8e2e..3d25b054 100644 --- a/packages/core/zyklus.mjs +++ b/packages/core/zyklus.mjs @@ -44,6 +44,6 @@ function createClock( }; const getPhase = () => phase; // setCallback - return { setDuration, start, stop, pause, duration, getPhase, minLatency }; + return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency }; } export default createClock; diff --git a/packages/react/src/hooks/useHighlighting.mjs b/packages/react/src/hooks/useHighlighting.mjs index 6e336586..4ce2a936 100644 --- a/packages/react/src/hooks/useHighlighting.mjs +++ b/packages/react/src/hooks/useHighlighting.mjs @@ -1,5 +1,6 @@ import { useEffect, useRef } from 'react'; import { setHighlights } from '../components/CodeMirror6'; +const round = (x) => Math.round(x * 1000) / 1000; function useHighlighting({ view, pattern, active, getTime }) { const highlights = useRef([]); @@ -14,7 +15,7 @@ function useHighlighting({ view, pattern, active, getTime }) { // force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away // see https://github.com/tidalcycles/strudel/issues/108 const begin = Math.max(lastEnd.current ?? audioTime, audioTime - 1 / 10, -0.01); // negative time seems buggy - const span = [begin, audioTime + 1 / 60]; + const span = [round(begin), round(audioTime + 1 / 60)]; lastEnd.current = span[1]; highlights.current = highlights.current.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset()); diff --git a/website/src/repl/Footer.jsx b/website/src/repl/Footer.jsx index 07bdd386..6a9ea876 100644 --- a/website/src/repl/Footer.jsx +++ b/website/src/repl/Footer.jsx @@ -92,7 +92,7 @@ export function Footer({ context }) { {activeFooter === 'console' && } {activeFooter === 'samples' && } {activeFooter === 'reference' && } - {activeFooter === 'settings' && } + {activeFooter === 'settings' && } )} @@ -284,10 +284,28 @@ const fontFamilyOptions = { PressStart: 'PressStart2P', }; -function SettingsTab() { +function SettingsTab({ scheduler }) { const { theme, keybindings, fontSize, fontFamily } = useSettings(); return (
+ +
+ + +
+
settingsMap.setKey('theme', theme)} /> diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index d73adcae..ce21f1c1 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -242,6 +242,7 @@ export function Repl({ embedded = false }) { } }; const context = { + scheduler, embedded, started, pending, @@ -273,7 +274,10 @@ export function Repl({ embedded = false }) { fontSize={fontSize} fontFamily={fontFamily} onChange={handleChangeCode} - onViewChanged={setView} + onViewChanged={(v) => { + setView(v); + // window.editorView = v; + }} onSelectionChange={handleSelectionChange} />