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
Initially, using either
setTimeout seems like a reasonable idea to try. These functions allows the programmer to specify a number of milliseconds to wait before running some function, which in this case, should play a sound.
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,
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
We quickly learned a plain old
setTimeout will not be able to be accurate consistently due to delays in waiting for resources from the browser.
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.
The AudioContext interface represents an audio-processing graph built from audio modules linked together, each…
AudioContextinterface 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.”
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:
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
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!
AudioContext for your next web application may be overkill, depending on your needs.
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!