Skip to content

Commit

Permalink
Merge branch 'tidalcycles:main' into patternuary
Browse files Browse the repository at this point in the history
  • Loading branch information
edcrub authored Aug 21, 2024
2 parents 0e609e6 + d86ff0a commit 0dc2de7
Show file tree
Hide file tree
Showing 10 changed files with 387 additions and 369 deletions.
15 changes: 4 additions & 11 deletions packages/core/clockworker.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
// import createClock from './zyklus.mjs';

function getTime() {
const precision = 10 ** 4;
const seconds = performance.now() / 1000;
return Math.round(seconds * precision) / precision;
const seconds = performance.now() * 0.001;
return seconds;
// return Math.round(seconds * precision) / precision;
}

let num_cycles_at_cps_change = 0;
Expand All @@ -24,27 +24,20 @@ const sendMessage = (type, payload) => {

const sendTick = (phase, duration, tick, time) => {
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;

const tickdeadline = phase - time;
const lastTick = time + tickdeadline;
const num_cycles_since_cps_change = num_seconds_since_cps_change * cps;

const begin = num_cycles_at_cps_change + num_cycles_since_cps_change;
const secondsSinceLastTick = time - lastTick - duration;

const eventLength = duration * cps;
const end = begin + eventLength;

const cycle = begin + secondsSinceLastTick * cps;

sendMessage('tick', {
begin,
end,
cps,
tickdeadline,
num_cycles_at_cps_change,
num_seconds_at_cps_change,
num_seconds_since_cps_change,
time,
cycle,
});
num_ticks_since_cps_change++;
Expand Down
70 changes: 15 additions & 55 deletions packages/core/neocyclist.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/

import { logger } from './logger.mjs';
import { ClockCollator, cycleToSeconds } from './util.mjs';

export class NeoCyclist {
constructor({ onTrigger, onToggle, getTime }) {
Expand All @@ -13,79 +14,38 @@ export class NeoCyclist {
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.getTime = getTime; // get absolute time
this.time_at_last_tick_message = 0;

this.num_cycles_at_cps_change = 0;
// the clock of the worker and the audio context clock can drift apart over time
// aditionally, the message time of the worker pinging the callback to process haps can be inconsistent.
// we need to keep a rolling average of the time difference between the worker clock and audio context clock
// in order to schedule events consistently.
this.collator = new ClockCollator({ getTargetClockTime: getTime });
this.onToggle = onToggle;
this.latency = 0.1; // fixed trigger time offset
this.cycle = 0;
this.id = Math.round(Date.now() * Math.random());
this.worker_time_dif;
this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url));
this.worker.port.start();

this.channel = new BroadcastChannel('strudeltick');
let weight = 0; // the amount of weight that is applied to the current average when averaging a new time dif
const maxWeight = 20;
const precision = 10 ** 3; //round off time diff to prevent accumulating outliers

// the clock of the worker and the audio context clock can drift apart over time
// aditionally, the message time of the worker pinging the callback to process haps can be inconsistent.
// we need to keep a rolling weighted average of the time difference between the worker clock and audio context clock
// in order to schedule events consistently.
const setTimeReference = (num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline) => {
const time_dif = getTime() - (num_seconds_at_cps_change + num_seconds_since_cps_change) + tickdeadline;
if (this.worker_time_dif == null) {
this.worker_time_dif = time_dif;
} else {
const w = 1; //weight of new time diff;
const new_dif =
Math.round(((this.worker_time_dif * weight + time_dif * w) / (weight + w)) * precision) / precision;

if (new_dif != this.worker_time_dif) {
// reset the weight so the clock recovers faster from an audio context freeze/dropout if it happens
weight = 4;
}
this.worker_time_dif = new_dif;
}
weight = Math.min(weight + 1, maxWeight);
};

const tickCallback = (payload) => {
const {
num_cycles_at_cps_change,
cps,
num_seconds_at_cps_change,
num_seconds_since_cps_change,
begin,
end,
tickdeadline,
cycle,
} = payload;
const { cps, begin, end, cycle, time } = payload;
this.cps = cps;
this.cycle = cycle;

setTimeReference(num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline);

processHaps(begin, end, num_cycles_at_cps_change, num_seconds_at_cps_change);

this.time_at_last_tick_message = this.getTime();
const currentTime = this.collator.calculateOffset(time) + time;
processHaps(begin, end, currentTime);
this.time_at_last_tick_message = currentTime;
};

const processHaps = (begin, end, num_cycles_at_cps_change, seconds_at_cps_change) => {
const processHaps = (begin, end, currentTime) => {
if (this.started === false) {
return;
}

const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });

haps.forEach((hap) => {
if (hap.hasOnset()) {
const targetTime =
(hap.whole.begin - num_cycles_at_cps_change) / this.cps +
seconds_at_cps_change +
this.latency +
this.worker_time_dif;
const duration = hap.duration / this.cps;
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
const targetTime = timeUntilTrigger + currentTime + this.latency;
const duration = cycleToSeconds(hap.duration, this.cps);
onTrigger?.(hap, 0, duration, this.cps, targetTime);
}
});
Expand Down Expand Up @@ -129,8 +89,8 @@ export class NeoCyclist {
this.setStarted(true);
}
stop() {
this.worker_time_dif = null;
logger('[cyclist] stop');
this.collator.reset();
this.setStarted(false);
}
setPattern(pat, autostart = false) {
Expand Down
62 changes: 62 additions & 0 deletions packages/core/util.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,68 @@ export function objectMap(obj, fn) {
}
return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)]));
}
export function cycleToSeconds(cycle, cps) {
return cycle / cps;
}

// utility for averaging two clocks together to account for drift
export class ClockCollator {
constructor({
getTargetClockTime = () => Date.now() * 0.001,
weight = 16,
offsetDelta = 0.005,
checkAfterTime = 2,
resetAfterTime = 8,
}) {
this.offsetTime;
this.timeAtPrevOffsetSample;
this.prevOffsetTimes = [];
this.getTargetClockTime = getTargetClockTime;
this.weight = weight;
this.offsetDelta = offsetDelta;
this.checkAfterTime = checkAfterTime;
this.resetAfterTime = resetAfterTime;
this.reset = () => {
this.prevOffsetTimes = [];
this.offsetTime = null;
this.timeAtPrevOffsetSample = null;
};
}
calculateOffset(currentTime) {
const targetClockTime = this.getTargetClockTime();
const diffBetweenTimeSamples = targetClockTime - this.timeAtPrevOffsetSample;
const newOffsetTime = targetClockTime - currentTime;
// recalcuate the diff from scratch if the clock has been paused for some time.
if (diffBetweenTimeSamples > this.resetAfterTime) {
this.reset();
}

if (this.offsetTime == null) {
this.offsetTime = newOffsetTime;
}
this.prevOffsetTimes.push(newOffsetTime);
if (this.prevOffsetTimes.length > this.weight) {
this.prevOffsetTimes.shift();
}

// after X time has passed, the average of the previous weight offset times is calculated and used as a stable reference
// for calculating the timestamp
if (this.timeAtPrevOffsetSample == null || diffBetweenTimeSamples > this.checkAfterTime) {
this.timeAtPrevOffsetSample = targetClockTime;
const rollingOffsetTime = averageArray(this.prevOffsetTimes);
//when the clock offsets surpass the delta, set the new reference time
if (Math.abs(rollingOffsetTime - this.offsetTime) > this.offsetDelta) {
this.offsetTime = rollingOffsetTime;
}
}

return this.offsetTime;
}

calculateTimestamp(currentTime, targetTime) {
return this.calculateOffset(currentTime) + targetTime;
}
}

// Floating point versions, see Fraction for rational versions
// // greatest common divisor
Expand Down
30 changes: 11 additions & 19 deletions website/src/components/Udels/UdelsEditor.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { ReplContext } from '@src/repl/util.mjs';

import Loader from '@src/repl/components/Loader';
import { Panel } from '@src/repl/components/panel/Panel';
import { Code } from '@src/repl/components/Code';
Expand All @@ -8,27 +6,21 @@ import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage'

// type Props = {
// context: replcontext,
// containerRef: React.MutableRefObject<HTMLElement | null>,
// editorRef: React.MutableRefObject<HTMLElement | null>,
// error: Error
// init: () => void
// }

export default function UdelsEditor(Props) {
const { context, containerRef, editorRef, error, init } = Props;
const { pending, started, handleTogglePlay } = context;
const { context } = Props;
const { containerRef, editorRef, error, init, pending, started, handleTogglePlay } = context;

return (
<ReplContext.Provider value={context}>
<div className={'h-full flex w-full flex-col relative'}>
<Loader active={pending} />
{/* <Header context={context} /> */}
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
<div className="grow flex relative overflow-hidden">
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
</div>
<UserFacingErrorMessage error={error} />
<Panel context={context} />
<div className={'h-full flex w-full flex-col relative'}>
<Loader active={pending} />
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
<div className="grow flex relative overflow-hidden">
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
</div>
</ReplContext.Provider>
<UserFacingErrorMessage error={error} />
<Panel context={context} />
</div>
);
}
Loading

0 comments on commit 0dc2de7

Please sign in to comment.