fix: derive master clock ticks from AudioContext.currentTime

The _masterTicks++ counter fell behind when Tone.Clock callbacks were
delayed by GC pauses, UI interactions, or tab throttling. The counter
never recovered, causing cumulative drift between sequencers.

Now ticks are derived from the callback's time parameter (which comes
from AudioContext.currentTime — hardware clock, always precise):
  ticks = Math.round((time - startTime) * MASTER_TICK_RATE)

If a callback is delayed by 50ms, the time is still correct and ticks
jump ahead to the right value. No accumulation, no drift, self-healing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-21 18:08:10 +01:00
parent 7d3a19ec35
commit 7596aea491

View File

@@ -18,13 +18,8 @@ const keyboardState = { frequency: 440, gate: false };
// Using integers avoids floating-point drift entirely.
export const MASTER_TICK_RATE = 480; // Hz — must be high enough for fastest BPM
let _masterClock = null;
let _masterTicks = 0;
const _tickListeners = new Map(); // id → callback(audioTime, ticks)
export function getMasterTicks() {
return _masterTicks;
}
export function subscribeTick(id, callback) {
_tickListeners.set(id, callback);
}
@@ -35,11 +30,16 @@ export function unsubscribeTick(id) {
function startMasterClock() {
if (_masterClock) return;
_masterTicks = 0;
let _startTime = 0;
let _started = false;
_masterClock = new Tone.Clock((time) => {
_masterTicks++;
if (!_started) { _startTime = time; _started = true; }
// Derive ticks from precise AudioContext.currentTime, not a counter.
// Counters fall behind when callbacks are delayed (GC, UI, tab throttle).
// The time parameter is always accurate regardless of callback jitter.
const ticks = Math.round((time - _startTime) * MASTER_TICK_RATE);
for (const cb of _tickListeners.values()) {
cb(time, _masterTicks);
cb(time, ticks);
}
}, MASTER_TICK_RATE);
_masterClock.start();