A suspended AudioContext holds currentTime still, so any clock derived from it stops
Fact
AudioContext.currentTime advances only while the context's state is running. A context constructed before any user gesture starts suspended under the autoplay policy, and a browser may suspend a running one again when the page is backgrounded or the output device changes.
While suspended it does not throw, reset, or report anything unusual: it holds whatever finite, non-zero value it had reached. Anything clocked on it — playback position, progress, a whole simulation — therefore stops dead while rendering carries on at full frame rate.
resume() returns a promise that settles only once the context is actually allowed to run, and a resume() that is not reached synchronously from a user gesture handler may never settle at all.
Why it matters
The frozen value looks exactly like a working clock. It is finite, it is not zero, and reading it twice inside one frame legitimately returns the same number even on a healthy context, so the obvious sanity checks pass.
The symptom is "everything draws, nothing moves", which points at the frame loop or at the update function — neither of which is at fault. It also hides during development, because interacting with the page to test it is itself the gesture that resumes the context.
How to apply
Treat
state === 'running'as a precondition of the clock, not just of the sound. Either gate the start of anything time-driven on it, or carry the time forward on a wall clock while suspended, anchored so it cannot jump:if (ctx && ctx.state === 'running') { anchorAudio = ctx.currentTime; anchorPerf = performance.now() / 1000; return anchorAudio; } return anchorAudio + performance.now() / 1000 - anchorPerf;- Call
resume()from inside the gesture handler, not from a timer or a continuation after anawait. - Listen for the context's
statechangeevent rather than pollingstate. - When nothing is moving, separate the two candidates before debugging either: log
ctx.stateand arequestAnimationFramecounter. A hidden surface delivering no frames and a frozen audio clock present identically.
Related: [[audiocontext-currenttime-is-not-audible-time]], [[hidden-surface-delivers-no-frames]].