From 7596aea4912e21095ab3b4bb7b5f70533c8990d8 Mon Sep 17 00:00:00 2001 From: Jose Luis Date: Sat, 21 Mar 2026 18:08:10 +0100 Subject: [PATCH] fix: derive master clock ticks from AudioContext.currentTime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/engine/audioEngine.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/engine/audioEngine.js b/src/engine/audioEngine.js index 0239e25..b38d97a 100644 --- a/src/engine/audioEngine.js +++ b/src/engine/audioEngine.js @@ -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();