implement cps + add baasic tempo control ui

This commit is contained in:
Felix Roos 2023-02-28 13:00:35 +01:00
parent cfe369d9fe
commit ead5942ef0
5 changed files with 49 additions and 21 deletions

View file

@ -10,28 +10,35 @@ 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);
} }
}); });
@ -39,15 +46,13 @@ export class Cyclist {
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

@ -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

@ -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>