This commit is contained in:
Jade (Rose) Rowland 2024-08-17 15:31:21 -04:00
parent 3945abd7e9
commit 9c899d5308
3 changed files with 85 additions and 62 deletions

View File

@ -5,8 +5,9 @@
function getTime() { function getTime() {
const precision = 10 ** 4; const precision = 10 ** 4;
const seconds = performance.now() / 1000; const seconds = performance.now() * .001;
return Math.round(seconds * precision) / precision; return seconds
// return Math.round(seconds * precision) / precision;
} }
let num_cycles_at_cps_change = 0; let num_cycles_at_cps_change = 0;
@ -41,10 +42,8 @@ const sendTick = (phase, duration, tick, time) => {
begin, begin,
end, end,
cps, cps,
tickdeadline, time: num_seconds_at_cps_change + num_seconds_since_cps_change + tickdeadline,
num_cycles_at_cps_change, num_cycles_at_cps_change,
num_seconds_at_cps_change,
num_seconds_since_cps_change,
cycle, cycle,
}); });
num_ticks_since_cps_change++; num_ticks_since_cps_change++;

View File

@ -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 { logger } from './logger.mjs';
import { ClockCollator, cycleToSeconds } from './util.mjs';
export class NeoCyclist { export class NeoCyclist {
constructor({ onTrigger, onToggle, getTime }) { constructor({ onTrigger, onToggle, getTime }) {
@ -13,65 +14,30 @@ export class NeoCyclist {
this.lastTick = 0; // absolute time when last tick (clock callback) happened this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.getTime = getTime; // get absolute time this.getTime = getTime; // get absolute time
this.time_at_last_tick_message = 0; this.time_at_last_tick_message = 0;
this.num_cycles_at_cps_change = 0;
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 // 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. // 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 // 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. // in order to schedule events consistently.
const setTimeReference = (num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline) => { this.collator = new ClockCollator({ getTargetClockTime: getTime });
const time_dif = getTime() - (num_seconds_at_cps_change + num_seconds_since_cps_change) + tickdeadline; this.onToggle = onToggle;
if (this.worker_time_dif == null) { this.latency = 0.1; // fixed trigger time offset
this.worker_time_dif = time_dif; this.cycle = 0;
} else { this.id = Math.round(Date.now() * Math.random());
const w = 1; //weight of new time diff; this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url));
const new_dif = this.worker.port.start();
Math.round(((this.worker_time_dif * weight + time_dif * w) / (weight + w)) * precision) / precision; this.channel = new BroadcastChannel('strudeltick');
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 tickCallback = (payload) => {
const { const { num_cycles_at_cps_change, cps, begin, end, cycle, time } = payload;
num_cycles_at_cps_change,
cps,
num_seconds_at_cps_change,
num_seconds_since_cps_change,
begin,
end,
tickdeadline,
cycle,
} = payload;
this.cps = cps; this.cps = cps;
this.cycle = cycle; this.cycle = cycle;
const currentTime = cycleToSeconds(num_cycles_at_cps_change + this.cycle, this.cps)
setTimeReference(num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline); console.log(time, currentTime, this.cycle)
processHaps(begin, end, currentTime, num_cycles_at_cps_change);
processHaps(begin, end, num_cycles_at_cps_change, num_seconds_at_cps_change);
this.time_at_last_tick_message = this.getTime(); this.time_at_last_tick_message = this.getTime();
}; };
const processHaps = (begin, end, num_cycles_at_cps_change, seconds_at_cps_change) => { const processHaps = (begin, end, currentTime, num_cycles_at_cps_change) => {
if (this.started === false) { if (this.started === false) {
return; return;
} }
@ -80,12 +46,10 @@ export class NeoCyclist {
haps.forEach((hap) => { haps.forEach((hap) => {
if (hap.hasOnset()) { if (hap.hasOnset()) {
const targetTime = const target = cycleToSeconds(hap.whole.begin - num_cycles_at_cps_change, this.cps)
(hap.whole.begin - num_cycles_at_cps_change) / this.cps + // const target = (hap.whole.begin - num_cycles_at_cps_change) / this.cps;
seconds_at_cps_change + const targetTime = this.collator.calculateTimestamp(currentTime, target) + this.latency;
this.latency + const duration = cycleToSeconds(hap.duration, this.cps)
this.worker_time_dif;
const duration = hap.duration / this.cps;
onTrigger?.(hap, 0, duration, this.cps, targetTime); onTrigger?.(hap, 0, duration, this.cps, targetTime);
} }
}); });
@ -129,8 +93,8 @@ export class NeoCyclist {
this.setStarted(true); this.setStarted(true);
} }
stop() { stop() {
this.worker_time_dif = null;
logger('[cyclist] stop'); logger('[cyclist] stop');
this.collator.reset()
this.setStarted(false); this.setStarted(false);
} }
setPattern(pat, autostart = false) { setPattern(pat, autostart = false) {

View File

@ -363,6 +363,66 @@ export function objectMap(obj, fn) {
} }
return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)])); 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;
};
}
calculateTimestamp(currentTime, targetTime) {
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;
}
}
const timestamp = this.offsetTime + targetTime;
return timestamp;
}
}
// Floating point versions, see Fraction for rational versions // Floating point versions, see Fraction for rational versions
// // greatest common divisor // // greatest common divisor