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' &&