Two Clocks: Why Timing in Browser Audio Feels Loose
Short answer: The audio side of a browser is precise. You can schedule a sound to begin on an exact sample. The JavaScript side is not: the moment your code runs can be late by tens of milliseconds. So timing depends on which of the two clocks decided "now". Schedule ahead on the audio clock when you can. When you can't, keep the looseness small and make it sound deliberate.
What I had wrong
I assumed browser audio was simply imprecise. It isn't.
The article that put me right is A Tale of Two Clocks. The audio clock, it says, is designed to line things up at the level of a single sample. JavaScript timers are the weak part: they can be thrown off by tens of milliseconds by layout, rendering and memory clean-up.
The usual answer: look ahead
If you know what is coming, as in a drum pattern, you don't play notes "now". A timer wakes up often, looks a short way into the future and books the next few notes on the audio clock. The timer can be late. The notes won't be.
function scheduler() {
while (nextNoteTime < audioCtx.currentTime + 0.1) { // look 100 ms ahead
playNoteAt(nextNoteTime); // exact, on the audio clock
nextNoteTime += secondsPerBeat;
}
}
setInterval(scheduler, 25);
Those two numbers, a 25 millisecond wake-up and a 100 millisecond window, are the starting point the article suggests.
Why I can't fully use it
In my app nothing is known in advance. A sound happens when two shapes touch, and I only find that out when the screen is redrawn, about sixty times a second. So a sound can start up to one frame late, and two sounds that should be together can land a hair apart.
I do two things about it. Short sounds are nudged onto the pulse of the drone, which I describe in The Drone Is the Clock. And each one gets a very small random offset on purpose, so that sounds landing together don't stack into one hard edge. A little looseness that was chosen sounds like a player. Looseness that just happens sounds like a fault.
When the tab is in the background
Browsers stop redrawing a tab you are not looking at. For me that would stop the shapes, and with them the sound. So when the tab is hidden, a plain timer takes over and keeps the shapes moving. Timers are slowed down in background tabs but they do keep firing. For background sound, that is the case that matters most: nobody keeps the tab in front.
Sources
- A Tale of Two Clocks, web.dev: the audio clock, JavaScript timers and the look-ahead pattern
Try it: start Ocean, then switch to another tab and keep working. It carries on. It is free and starts in one click.
Open Ocean, free