The audio tag's limits
For years, the only way to break the silence of the web was to rely on Flash or another plugin for audio playback. Although the HTML5 <audio> element removed the plugin requirement, it is a poor fit for games and interactive applications that need precise scheduling, dynamic effects, or mixing of multiple sound sources.
The Web Audio API is a high-level JavaScript API for synthesizing and processing audio in web applications. Its feature set covers the needs of modern game audio engines, along with the mixing, processing, and filtering tasks found in desktop audio production tools. The AudioContext is at the core of the API: it defines a routing graph that connects one or more audio source nodes through intermediate processing nodes (AudioNodes) to a destination. A single AudioContext instance supports even complex graphs, so an application typically needs only one. The following snippet creates one, but older WebKit-based browsers use the prefixed webkitAudioContext.
var context;
window.addEventListener('load', init, false);
function init() {
try {
context = new AudioContext();
}
catch(e) {
alert('Web Audio API is not supported in this browser');
}
}
Loading and decoding samples
The API uses an AudioBuffer to hold short- to medium-length sounds. The standard technique fetches sound files with XMLHttpRequest, which supports binary data in the commonly used formats (WAV, MP3, AAC, OGG, and others), though browser format support varies.
var dogBarkingBuffer = null;
var context = new AudioContext();
function loadDogSound(url) {
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
// Decode asynchronously
request.onload = function() {
context.decodeAudioData(request.response, function(buffer) {
dogBarkingBuffer = buffer;
}, onError);
}
request.send();
}
Because the audio file data is binary, set the request's responseType to 'arraybuffer'. Once the data arrives, it can either be stored in that raw form for later use or decoded immediately with the AudioContext's $decodeAudioData() method. The method converts the ArrayBuffer to PCM audio data asynchronously to avoid blocking the JavaScript thread. When decoding completes, it returns the data as an AudioBuffer in a callback.
With the buffer in hand, you can play the sound the instant it's ready, such as the moment the user clicks or presses a key. The snippet below shows such a playSound() function; note that it calls the noteOn() method with a start time, which makes precise playback scheduling trivial.
var context = new AudioContext();
function playSound(buffer) {
var source = context.createBufferSource(); // creates a sound source
source.buffer = buffer; // tell the source which sound to play
source.connect(context.destination); // connect the source to the context's destination (the speakers)
source.noteOn(0); // play the source now
}
A generic buffer loader
Hardcoding a project to one sound effect isn't practical. A BufferLoader class—part of the API's wider ecosystem, but not itself a web standard—is a better way to manage the bundle of sounds that a game or audio application will use. Its use is straightforward:
window.onload = init;
var context;
var bufferLoader;
function init() {
context = new AudioContext();
bufferLoader = new BufferLoader(
context,
[
'../sounds/hyper-reality/br-jam-loop.wav',
'../sounds/hyper-reality/laughter.wav',
],
finishedLoading
);
bufferLoader.load();
}
function finishedLoading(bufferList) {
// Create two sources and play them both together.
var source1 = context.createBufferSource();
var source2 = context.createBufferSource();
source1.buffer = bufferList[0];
source2.buffer = bufferList[1];
source1.connect(context.destination);
source2.connect(context.destination);
source1.noteOn(0);
source2.noteOn(0);
}
This creates a pair of AudioBuffers, and both are splayed simultaneously as soon as they are loaded.
Precise timing
For a simple demonstration of precise audio scheduling with the API, take the standard drum pattern of alternating kick and snare every quarter note, with the hihat every eighth note in 4/4 time.
With the three samples preloaded, a function for the pattern is short, even if it loops only once instead of indefinitely:
for (var bar = 0; bar < 2; bar++) {
var time = startTime + bar * 8 * eighthNoteTime;
// Play the bass (kick) drum on beats 1, 5
playSound(kick, time);
playSound(kick, time + 4 * eighthNoteTime);
// Play the snare drum on beats 3, 7
playSound(snare, time + 2 * eighthNoteTime);
playSound(snare, time + 6 * eighthNoteTime);
// Play the hi-hat every eighth note.
for (var i = 0; i < 8; ++i) {
playSound(hihat, time + i * eighthNoteTime);
}
}
The pattern relies on the playSound helper, which takes a buffer and first play time:
function playSound(buffer, time) {
var source = context.createBufferSource();
source.buffer = buffer;
source.connect(context.destination);
source.noteOn(time);
}
Routing for volume and crossfades
Perhaps the most basic effect is a volume control. This is handled by routing the source to its destination through an AudioGainNode:

The graph is set up by connecting the source to the gain node, and that node to the destination:
// Create a gain node.
var gainNode = context.createGainNode();
// Connect the source to the gain node.
source.connect(gainNode);
// Connect the gain node to the destination.
gainNode.connect(context.destination);
After this, changing the output volume is a matter of assigning a new value to gainNode.gain.value:
// Reduce the volume.
gainNode.gain.value = 0.5;
The same pattern scales up to a typical DJ-style crossfader that pans between two samples:
In that case, create two AudioGainNodes, connecting each sound source through its own node as this helper shows:
function createSource(buffer) {
var source = context.createBufferSource();
// Create a gain node.
var gainNode = context.createGainNode();
source.buffer = buffer;
// Turn on looping.
source.loop = true;
// Connect source to gain.
source.connect(gainNode);
// Connect gain to destination.
gainNode.connect(context.destination);
return {
source: source,
gainNode: gainNode
};
}
A linear range of gain values is conceptually simple, but it causes the overall volume to dip between the two samples. An equal-power curve—non-linear gain curves that intersect at a higher amplitude—spreads the sounds more evenly, which is what most users will hear as natural.
Playlist style crossfades are another common use-case: fade the outgoing track down while bringing the new one up, without an abrupt transition. Although an idle caller might consider setTimeout to drive this, it's far too imprecise. The Web Audio API's AudioParam interface is better suited to schedule future values for parameters such as gain. Given a playlist, fade, schedule both volume changes a little before the current track ends:
function playHelper(bufferNow, bufferLater) {
var playNow = createSource(bufferNow);
var source = playNow.source;
var gainNode = playNow.gainNode;
var duration = bufferNow.duration;
var currTime = context.currentTime;
// Fade the playNow track in.
gainNode.gain.linearRampToValueAtTime(0, currTime);
gainNode.gain.linearRampToValueAtTime(1, currTime + ctx.FADE_TIME);
// Play the playNow track.
source.noteOn(0);
// At the end of the track, fade it out.
gainNode.gain.linearRampToValueAtTime(1, currTime + duration-ctx.FADE_TIME);
gainNode.gain.linearRampToValueAtTime(0, currTime + duration);
// Schedule a recursive track change with the tracks swapped.
var recurse = arguments.callee;
ctx.timer = setTimeout(function() {
recurse(bufferLater, bufferNow);
}, (duration - ctx.FADE_TIME) - 1000);
}
The RampToValue family of methods—including linearRampToValueAtTime and exponentialRampToValueAtTime—gradually transitions a parameter. There are also built-in curves and the ability to provide your own curve via setValueCurveAtTime.
Filtering a sound

Rather than applying all processing inline, the Web Audio API walks a signal through a series of processing nodes between the source and the destination. One of the more common processors is the BiquadFilterNode, which acts as a type of low-order filter. These filters are standard building blocks for graphic equalizers and other common frequency-based effects. Supported types include:
- Low pass filter
- High pass filter
- Band pass filter
- Low shelf filter
- High shelf filter
- Peaking filter
- Notch filter
- All pass filter
Every filter type exposes parameters for gain, the frequency at which to apply the effect, and a unitless quality factor (Q) that shapes the filter's response curve. A low-pass filter discards frequencies above its cutoff. Because human hearing perceives pitch logarithmically (A4 is 440 Hz, so A5 is 880 Hz), you may find it easier to adjust filter frequency on a logarithmic scale, especially for a more musical feel. Refer to a FilterSample.changeFrequency implementation for details.
The full graph for a low-pass filter is built by creating a source and connecting it to the filter node, then to the destination:
// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
source.connect(filter);
filter.connect(context.destination);
// Create and specify parameters for the low-pass filter.
filter.type = 0; // Low-pass filter. See BiquadFilterNode docs
filter.frequency.value = 440; // Set cutoff to 440 HZ
// Playback the sound.
source.noteOn(0);
Graphs are dynamic: you can disconnect an AudioNode by calling node.disconnect(outputNumber). Use that to tear down a filter connection set up as above:
// Disconnect the source and filter.
source.disconnect(0);
filter.disconnect(0);
// Connect the source directly.
source.connect(context.destination);
This touches on the basics—loading, playing, routing through gain and filter stages, and scheduled parameter tweaks—enough to start building with the Web Audio API. Current versions of the well-known applications AudioJedit, a sound splicing tool online, and workspace ToneCraft, a 3D sound sequencer, along with experimental works like Plink are good places to look for further inspiration.



