Merge pull request #493 from tidalcycles/cps

implement cps in scheduler
This commit is contained in:
Felix Roos 2023-03-01 09:27:26 +01:00 committed by GitHub
commit 3cfb0f4c76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 71 additions and 34 deletions

View file

@ -10,44 +10,49 @@ import { logger } from './logger.mjs';
export class Cyclist { export class Cyclist {
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) { constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
this.started = false; this.started = false;
this.cps = 1; // TODO this.cps = 1;
this.phase = 0; this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.getTime = getTime; 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.onToggle = onToggle;
this.latency = latency; this.latency = latency; // fixed trigger time offset
const round = (x) => Math.round(x * 1000) / 1000; const round = (x) => Math.round(x * 1000) / 1000;
this.clock = createClock( this.clock = createClock(
getTime, getTime,
// called slightly before each cycle
(phase, duration, tick) => { (phase, duration, tick) => {
if (tick === 0) { if (tick === 0) {
this.origin = phase; this.origin = phase;
} }
const begin = round(phase - this.origin);
this.phase = begin - latency;
const end = round(begin + duration);
const time = getTime();
try { 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) => { haps.forEach((hap) => {
if (hap.part.begin.equals(hap.whole.begin)) { if (hap.part.begin.equals(hap.whole.begin)) {
const deadline = hap.whole.begin + this.origin - time + latency; const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
const duration = hap.duration * 1; const duration = hap.duration / this.cps;
onTrigger?.(hap, deadline, duration); onTrigger?.(hap, deadline, duration, this.cps);
} }
}); });
} catch (e) { } catch (e) {
logger(`[cyclist] error: ${e.message}`); logger(`[cyclist] error: ${e.message}`);
onError?.(e); onError?.(e);
} }
}, // called slightly before each cycle },
interval, // duration of each cycle interval, // duration of each cycle
); );
} }
getPhase() {
return this.getTime() - this.origin - this.latency;
}
now() { 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) { setStarted(v) {
this.started = v; this.started = v;

View file

@ -6,12 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import { isPattern } from './index.mjs'; import { isPattern } from './index.mjs';
let scoped = false;
export const evalScope = async (...args) => { export const evalScope = async (...args) => {
if (scoped) {
console.warn('evalScope was called more than once.');
}
scoped = true;
const results = await Promise.allSettled(args); const results = await Promise.allSettled(args);
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value); const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
results.forEach((result, i) => { results.forEach((result, i) => {
@ -42,9 +37,6 @@ function safeEval(str, options = {}) {
} }
export const evaluate = async (code, transpiler) => { export const evaluate = async (code, transpiler) => {
if (!scoped) {
await evalScope(); // at least scope Pattern.prototype.boostrap
}
if (transpiler) { if (transpiler) {
code = transpiler(code); // transform syntactically correct js code to semantically usable code code = transpiler(code); // transform syntactically correct js code to semantically usable code
} }

View file

@ -2,6 +2,7 @@ import { Cyclist } from './cyclist.mjs';
import { evaluate as _evaluate } from './evaluate.mjs'; import { evaluate as _evaluate } from './evaluate.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
import { setTime } from './time.mjs'; import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
export function repl({ export function repl({
interval, interval,
@ -17,13 +18,12 @@ export function repl({
}) { }) {
const scheduler = new Cyclist({ const scheduler = new Cyclist({
interval, interval,
onTrigger: async (hap, deadline, duration) => { onTrigger: async (hap, deadline, duration, cps) => {
try { try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) { if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration); await defaultOutput(hap, deadline, duration);
} }
if (hap.context.onTrigger) { if (hap.context.onTrigger) {
const cps = 1;
// call signature of output / onTrigger is different... // call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps); await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
} }
@ -42,6 +42,17 @@ export function repl({
} }
try { try {
beforeEval?.({ code }); 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); let { pattern } = await _evaluate(code, transpiler);
logger(`[eval] code updated`); logger(`[eval] code updated`);
@ -58,5 +69,10 @@ export function repl({
const stop = () => scheduler.stop(); const stop = () => scheduler.stop();
const start = () => scheduler.start(); const start = () => scheduler.start();
const pause = () => scheduler.pause(); 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 };
} }

View file

@ -44,6 +44,6 @@ function createClock(
}; };
const getPhase = () => phase; const getPhase = () => phase;
// setCallback // setCallback
return { setDuration, start, stop, pause, duration, getPhase, minLatency }; return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
} }
export default createClock; export default createClock;

View file

@ -1,5 +1,6 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { setHighlights } from '../components/CodeMirror6'; import { setHighlights } from '../components/CodeMirror6';
const round = (x) => Math.round(x * 1000) / 1000;
function useHighlighting({ view, pattern, active, getTime }) { function useHighlighting({ view, pattern, active, getTime }) {
const highlights = useRef([]); 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 // 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 // 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 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]; lastEnd.current = span[1];
highlights.current = highlights.current.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active 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()); const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());

View file

@ -32,7 +32,7 @@ function useStrudel({
const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]); const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
// TODO: make sure this hook reruns when scheduler.started changes // 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({ repl({
interval, interval,
@ -153,6 +153,7 @@ function useStrudel({
stop, stop,
pause, pause,
togglePlay, togglePlay,
setCps,
}; };
} }

View file

@ -92,7 +92,7 @@ export function Footer({ context }) {
{activeFooter === 'console' && <ConsoleTab log={log} />} {activeFooter === 'console' && <ConsoleTab log={log} />}
{activeFooter === 'samples' && <SamplesTab />} {activeFooter === 'samples' && <SamplesTab />}
{activeFooter === 'reference' && <Reference />} {activeFooter === 'reference' && <Reference />}
{activeFooter === 'settings' && <SettingsTab />} {activeFooter === 'settings' && <SettingsTab scheduler={context.scheduler} />}
</div> </div>
)} )}
</footer> </footer>
@ -284,10 +284,28 @@ const fontFamilyOptions = {
PressStart: 'PressStart2P', PressStart: 'PressStart2P',
}; };
function SettingsTab() { function SettingsTab({ scheduler }) {
const { theme, keybindings, fontSize, fontFamily } = useSettings(); const { theme, keybindings, fontSize, fontFamily } = useSettings();
return ( return (
<div className="text-foreground p-4 space-y-4"> <div className="text-foreground p-4 space-y-4">
{/* <FormItem label="Tempo">
<div className="space-x-4">
<button
onClick={() => {
scheduler.setCps(scheduler.cps - 0.1);
}}
>
slower
</button>
<button
onClick={() => {
scheduler.setCps(scheduler.cps + 0.1);
}}
>
faster
</button>
</div>
</FormItem> */}
<FormItem label="Theme"> <FormItem label="Theme">
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} /> <SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
</FormItem> </FormItem>

View file

@ -242,6 +242,7 @@ export function Repl({ embedded = false }) {
} }
}; };
const context = { const context = {
scheduler,
embedded, embedded,
started, started,
pending, pending,
@ -273,7 +274,10 @@ export function Repl({ embedded = false }) {
fontSize={fontSize} fontSize={fontSize}
fontFamily={fontFamily} fontFamily={fontFamily}
onChange={handleChangeCode} onChange={handleChangeCode}
onViewChanged={setView} onViewChanged={(v) => {
setView(v);
// window.editorView = v;
}}
onSelectionChange={handleSelectionChange} onSelectionChange={handleSelectionChange}
/> />
</section> </section>