From 9c899d5308a536bf6fd0aa00f121cb9a0c67ded9 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 17 Aug 2024 15:31:21 -0400 Subject: [PATCH 1/5] testing --- packages/core/clockworker.js | 9 ++--- packages/core/neocyclist.mjs | 78 ++++++++++-------------------------- packages/core/util.mjs | 60 +++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 62 deletions(-) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index 4c33de29..9edb4b0f 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -5,8 +5,9 @@ function getTime() { const precision = 10 ** 4; - const seconds = performance.now() / 1000; - return Math.round(seconds * precision) / precision; + const seconds = performance.now() * .001; + return seconds + // return Math.round(seconds * precision) / precision; } let num_cycles_at_cps_change = 0; @@ -41,10 +42,8 @@ const sendTick = (phase, duration, tick, time) => { begin, end, cps, - tickdeadline, + time: num_seconds_at_cps_change + num_seconds_since_cps_change + tickdeadline, num_cycles_at_cps_change, - num_seconds_at_cps_change, - num_seconds_since_cps_change, cycle, }); num_ticks_since_cps_change++; diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 4600d0ef..41c50af0 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -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 }) { @@ -13,65 +14,30 @@ 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; - 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); - }; - + 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 = new SharedWorker(new URL('./clockworker.js', import.meta.url)); + this.worker.port.start(); + this.channel = new BroadcastChannel('strudeltick'); 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 { num_cycles_at_cps_change, 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); - + const currentTime = cycleToSeconds(num_cycles_at_cps_change + this.cycle, this.cps) + console.log(time, currentTime, this.cycle) + processHaps(begin, end, currentTime, num_cycles_at_cps_change); 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) { return; } @@ -80,12 +46,10 @@ export class NeoCyclist { 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 target = cycleToSeconds(hap.whole.begin - num_cycles_at_cps_change, this.cps) + // const target = (hap.whole.begin - num_cycles_at_cps_change) / this.cps; + const targetTime = this.collator.calculateTimestamp(currentTime, target) + this.latency; + const duration = cycleToSeconds(hap.duration, this.cps) onTrigger?.(hap, 0, duration, this.cps, targetTime); } }); @@ -129,8 +93,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) { diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 798870f0..08bcdbce 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -363,6 +363,66 @@ 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; + }; + } + + 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 // // greatest common divisor From 73ee84a1fbc16b75baac056e18d824185ab68747 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 17 Aug 2024 17:11:51 -0400 Subject: [PATCH 2/5] working --- packages/core/clockworker.js | 9 ++------- packages/core/neocyclist.mjs | 20 ++++++++------------ 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index 9edb4b0f..8b2c1eb3 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -4,7 +4,7 @@ // import createClock from './zyklus.mjs'; function getTime() { - const precision = 10 ** 4; + const seconds = performance.now() * .001; return seconds // return Math.round(seconds * precision) / precision; @@ -25,25 +25,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, - time: num_seconds_at_cps_change + num_seconds_since_cps_change + tickdeadline, - num_cycles_at_cps_change, + time, cycle, }); num_ticks_since_cps_change++; diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 41c50af0..8d5fadd2 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -27,29 +27,25 @@ export class NeoCyclist { this.worker.port.start(); this.channel = new BroadcastChannel('strudeltick'); const tickCallback = (payload) => { - const { num_cycles_at_cps_change, cps, begin, end, cycle, time } = payload; + const { cps, begin, end, cycle, time } = payload; this.cps = cps; this.cycle = cycle; - const currentTime = cycleToSeconds(num_cycles_at_cps_change + this.cycle, this.cps) - console.log(time, currentTime, this.cycle) - processHaps(begin, end, currentTime, num_cycles_at_cps_change); + processHaps(begin, end, time); this.time_at_last_tick_message = this.getTime(); - }; - - const processHaps = (begin, end, currentTime, num_cycles_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 target = cycleToSeconds(hap.whole.begin - num_cycles_at_cps_change, this.cps) - // const target = (hap.whole.begin - num_cycles_at_cps_change) / this.cps; + const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps); + const target = timeUntilTrigger + currentTime; const targetTime = this.collator.calculateTimestamp(currentTime, target) + this.latency; - const duration = cycleToSeconds(hap.duration, this.cps) + const duration = cycleToSeconds(hap.duration, this.cps); onTrigger?.(hap, 0, duration, this.cps, targetTime); } }); @@ -94,7 +90,7 @@ export class NeoCyclist { } stop() { logger('[cyclist] stop'); - this.collator.reset() + this.collator.reset(); this.setStarted(false); } setPattern(pat, autostart = false) { From 117f7fa4b58ef9fb598cd7034c096cf4e43b27b5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 17 Aug 2024 17:26:46 -0400 Subject: [PATCH 3/5] cleaning up --- packages/core/neocyclist.mjs | 10 +++++----- packages/core/util.mjs | 10 ++++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 8d5fadd2..fe4cb0e2 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -16,7 +16,7 @@ export class NeoCyclist { this.time_at_last_tick_message = 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 weighted average of the time difference between the worker clock and audio context clock + // 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; @@ -30,8 +30,9 @@ export class NeoCyclist { const { cps, begin, end, cycle, time } = payload; this.cps = cps; this.cycle = cycle; - processHaps(begin, end, time); - 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, currentTime) => { @@ -43,8 +44,7 @@ export class NeoCyclist { haps.forEach((hap) => { if (hap.hasOnset()) { const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps); - const target = timeUntilTrigger + currentTime; - const targetTime = this.collator.calculateTimestamp(currentTime, target) + this.latency; + const targetTime = timeUntilTrigger + currentTime + this.latency const duration = cycleToSeconds(hap.duration, this.cps); onTrigger?.(hap, 0, duration, this.cps, targetTime); } diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 08bcdbce..f41128bc 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -390,8 +390,7 @@ export class ClockCollator { this.timeAtPrevOffsetSample = null; }; } - - calculateTimestamp(currentTime, targetTime) { + calculateOffset(currentTime) { const targetClockTime = this.getTargetClockTime(); const diffBetweenTimeSamples = targetClockTime - this.timeAtPrevOffsetSample; const newOffsetTime = targetClockTime - currentTime; @@ -419,8 +418,11 @@ export class ClockCollator { } } - const timestamp = this.offsetTime + targetTime; - return timestamp; + return this.offsetTime; + } + + calculateTimestamp(currentTime, targetTime) { + return this.calculateOffset(currentTime) + targetTime } } From 4eae366bee955a80320502b98bdaf53b8092bb6b Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 18 Aug 2024 12:05:03 -0400 Subject: [PATCH 4/5] cleaning up --- packages/core/util.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/util.mjs b/packages/core/util.mjs index f41128bc..52296078 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -415,6 +415,7 @@ export class ClockCollator { //when the clock offsets surpass the delta, set the new reference time if (Math.abs(rollingOffsetTime - this.offsetTime) > this.offsetDelta) { this.offsetTime = rollingOffsetTime; + } } From 2c9ea03fdea42ae070e13b38f33f2baf65834e61 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 18 Aug 2024 12:08:40 -0400 Subject: [PATCH 5/5] prettier --- packages/core/clockworker.js | 5 ++--- packages/core/neocyclist.mjs | 2 +- packages/core/util.mjs | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/core/clockworker.js b/packages/core/clockworker.js index 8b2c1eb3..bcaf2872 100644 --- a/packages/core/clockworker.js +++ b/packages/core/clockworker.js @@ -4,9 +4,8 @@ // import createClock from './zyklus.mjs'; function getTime() { - - const seconds = performance.now() * .001; - return seconds + const seconds = performance.now() * 0.001; + return seconds; // return Math.round(seconds * precision) / precision; } diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index fe4cb0e2..ad22cf00 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -44,7 +44,7 @@ export class NeoCyclist { haps.forEach((hap) => { if (hap.hasOnset()) { const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps); - const targetTime = timeUntilTrigger + currentTime + this.latency + const targetTime = timeUntilTrigger + currentTime + this.latency; const duration = cycleToSeconds(hap.duration, this.cps); onTrigger?.(hap, 0, duration, this.cps, targetTime); } diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 52296078..66f8d0db 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -415,7 +415,6 @@ export class ClockCollator { //when the clock offsets surpass the delta, set the new reference time if (Math.abs(rollingOffsetTime - this.offsetTime) > this.offsetDelta) { this.offsetTime = rollingOffsetTime; - } } @@ -423,7 +422,7 @@ export class ClockCollator { } calculateTimestamp(currentTime, targetTime) { - return this.calculateOffset(currentTime) + targetTime + return this.calculateOffset(currentTime) + targetTime; } }