diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 9b522f05..6e8171ea 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -10,44 +10,49 @@ 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; - onTrigger?.(hap, deadline, duration); + const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency; + const duration = hap.duration / this.cps; + onTrigger?.(hap, deadline, duration, this.cps); } }); } catch (e) { 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/evaluate.mjs b/packages/core/evaluate.mjs index 3379824e..1d7be8e4 100644 --- a/packages/core/evaluate.mjs +++ b/packages/core/evaluate.mjs @@ -6,12 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import { isPattern } from './index.mjs'; -let scoped = false; export const evalScope = async (...args) => { - if (scoped) { - console.warn('evalScope was called more than once.'); - } - scoped = true; const results = await Promise.allSettled(args); const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value); results.forEach((result, i) => { @@ -42,9 +37,6 @@ function safeEval(str, options = {}) { } export const evaluate = async (code, transpiler) => { - if (!scoped) { - await evalScope(); // at least scope Pattern.prototype.boostrap - } if (transpiler) { code = transpiler(code); // transform syntactically correct js code to semantically usable code } diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 045f2102..b5d8cb92 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -2,6 +2,7 @@ import { Cyclist } from './cyclist.mjs'; import { evaluate as _evaluate } from './evaluate.mjs'; import { logger } from './logger.mjs'; import { setTime } from './time.mjs'; +import { evalScope } from './evaluate.mjs'; export function repl({ interval, @@ -17,13 +18,12 @@ export function repl({ }) { const scheduler = new Cyclist({ interval, - onTrigger: async (hap, deadline, duration) => { + onTrigger: async (hap, deadline, duration, cps) => { try { if (!hap.context.onTrigger || !hap.context.dominantTrigger) { await defaultOutput(hap, deadline, duration); } if (hap.context.onTrigger) { - const cps = 1; // call signature of output / onTrigger is different... await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps); } @@ -42,6 +42,17 @@ export function repl({ } try { beforeEval?.({ code }); + scheduler.setCps(1); // reset cps in case the code does not contain a setCps call + // problem: when the code does contain a setCps after an awaited promise, + // the cps will be 1 until the promise resolves + // example: + /* + await new Promise(resolve => setTimeout(resolve,1000)) + setCps(.5) + note("c a f e") + */ + // to make sure the setCps inside the code is called immediately, + // it has to be placed first let { pattern } = await _evaluate(code, transpiler); logger(`[eval] code updated`); @@ -58,5 +69,10 @@ export function repl({ const stop = () => scheduler.stop(); const start = () => scheduler.start(); const pause = () => scheduler.pause(); - return { scheduler, evaluate, start, stop, pause }; + const setCps = (cps) => scheduler.setCps(cps); + evalScope({ + setCps, + setcps: setCps, + }); + return { scheduler, evaluate, start, stop, pause, setCps }; } 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/packages/react/src/hooks/useStrudel.mjs b/packages/react/src/hooks/useStrudel.mjs index b0b1062c..0900ebbf 100644 --- a/packages/react/src/hooks/useStrudel.mjs +++ b/packages/react/src/hooks/useStrudel.mjs @@ -32,7 +32,7 @@ function useStrudel({ const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]); // TODO: make sure this hook reruns when scheduler.started changes - const { scheduler, evaluate, start, stop, pause } = useMemo( + const { scheduler, evaluate, start, stop, pause, setCps } = useMemo( () => repl({ interval, @@ -153,6 +153,7 @@ function useStrudel({ stop, pause, togglePlay, + setCps, }; } diff --git a/website/src/repl/Footer.jsx b/website/src/repl/Footer.jsx index 07bdd386..08ab967e 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} />