Low Latency Web Audio Doesn’t Have To Be Hard

In the beginning of developing my web-based metronome app (https://metpro34.com), I quickly realized that typical approaches to playing audio with code would simply not work.

The use-case of a metronome may not sound that exciting, but it requires extreme accuracy and consistency, otherwise it is unusable.

Let’s take a look at what I tried, and what it takes to get low latency audio on the web.

The Naive Approach

If you want to see how this turns out, paste this code into your console; it creates a beep function and runs it 20 times on an interval of 500ms, which is 120 bpm (beats per minute), a very medium tempo for music.

Credit to https://www.rgagnon.com/jsdetails/js-0024.html for the pure JS beep audio, super cool!

After running this a few times, I was getting an average delay of 501.4 ms between each beep.

Though most of the beeps seem reasonably consistent, every so often a beep will be extremely late. Do you know why?

Contrary to popular belief, setInterval and setTimeout do not tell the JS interpreter to run the callback after an exact number of milliseconds. What they actually do is tell it to run the callback after at least some number of milliseconds, whenever resources are available for execution.

This means performance may vary quite drastically from machine to machine, and depending on what other tasks are running. This is unacceptable for the metronome use-case.

After some initial testing, this clearly will not ever be accurate enough for a musician to use as a metronome on its own. Let’s try and fix it up!

The (Slightly) Better Naive Approach

The next idea that might come to mind is adjusting for this delay by keeping track of the system time, and changing the delay to adjust for lapses.

Let’s try out this code, which accounts for latency from each beep, and see what the average ends up being.

After running this a few times, I was getting an average delay of 501.1 ms between each beep, an improvement of 0.3 ms per beep on the whole.

Yet, there are still too many beeps that are way off mark, and even a single out-of-time beep is considered unusable by a musician of any caliber.

After these efforts with setTimeout, it’s not looking too good. We need lower level audio control to get pristine accuracy and consistency.

We need…

Web AudioContext

“The AudioContext interface represents an audio-processing graph built from audio modules linked together, each represented by an AudioNode. An audio context controls both the creation of the nodes it contains and the execution of the audio processing, or decoding.”

The AudioContext object gives the programmer a highly flexible and powerful grasp on the audio engine of the browser.

It allows the programmer to easily understand and develop their application audio as a directed graph data structure.

This means it is easy to attach a new node to the graph by connecting it to another node already in the graph, or by directly attaching it to the final output node: audioContext.destination

This can be super useful when you need many streams of audio, or need to mix streams or set volumes / EQ on a node-by-node basis.

Copy and paste this code into your console to experience an extremely accurate AudioContext metronome!

Running this code consistently gives me an average delay of ~499.75 ms between beeps, with zero noticeable hiccups in rhythm… Now that’s a good metronome!

Using AudioContext for your next web application may be overkill, depending on your needs.

There are certainly many ways to play and configure your application audio playback, with loads of various libraries available, as well as the built-in Javascript functionality.

However, when extremely low latency audio is a must, or you want maximum control over every part of your application audio, AudioContext is a great bet!

An old-fashioned metronome