JavaScript Web Audio API
Learn the JavaScript Web Audio API: build an audio graph from AudioContext and AudioNodes, load and play sounds, control volume, and generate tones.
The Web Audio API lets you generate, load, process, and play sound directly in the browser — no plugins required. This guide walks you through its core building blocks: how to create an audio context, load and play sound files, control volume, generate tones with an oscillator, and work around the browser autoplay policy. Each example is small and self-contained so you can drop it into a page and hear the result.
Introduction to Web Audio API
The Web Audio API is a JavaScript API for processing and synthesizing audio in web applications. Unlike a plain <audio> element — which just plays a file from start to finish — the Web Audio API gives you a graph of connected nodes that route, mix, and transform sound in real time. That makes it the right tool for games, music apps, synthesizers, and audio visualizers.
Thinking in audio graphs
Every sound in the Web Audio API flows through a chain of nodes, from a source to a destination:
source → (effects) → destination
(file, (gain, (your
oscillator) filter) speakers)The three concepts you will meet first:
- AudioContext — the container that owns the whole graph and the audio clock. You create one per page and reuse it.
- AudioNode — a modular unit of processing. Source nodes produce sound (a buffer or an oscillator), effect nodes transform it (gain, filter, panner), and the destination node sends it to the speakers.
- AudioBuffer — decoded, in-memory audio data, ideal for short sounds (effects, samples) that you trigger repeatedly with tight timing.
You build a graph by calling node.connect(otherNode). Audio always ends at audioContext.destination.
Getting Started with Web Audio API
To do anything with audio, you first create an AudioContext, which serves as the container for your audio graph.
Creating an AudioContext
const audioContext = new (window.AudioContext || window.webkitAudioContext)();This initializes a new AudioContext. The webkit fallback covers older Safari versions; modern browsers support the unprefixed AudioContext directly.
Autoplay policy gotcha. Browsers create the context in a
"suspended"state and refuse to play sound until the user interacts with the page (a click or key press). Always resume it inside an event handler before playing:button.addEventListener("click", async () => { if (audioContext.state === "suspended") { await audioContext.resume(); } // ...now it is safe to play });If audio is silent with no errors in the console, an un-resumed context is the most common cause.
Loading and Playing Audio
To play a sound file, you load it into an AudioBuffer, then connect a source node to the context for playback.
Loading Audio Files
This uses the Fetch API and async/await to download the file and decode it:
async function loadAudioFile(url, audioContext) {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
return audioBuffer;
}The function fetches the file, reads it as a raw ArrayBuffer, then hands that to decodeAudioData, which decompresses formats like MP3, WAV, or OGG into a ready-to-play AudioBuffer. Decoding happens off the main thread and returns a promise, so await it.
Playing the Loaded Audio
function playAudio(audioBuffer, audioContext) {
const sourceNode = audioContext.createBufferSource();
sourceNode.buffer = audioBuffer;
sourceNode.connect(audioContext.destination);
sourceNode.start();
}This creates a BufferSourceNode, points it at the decoded buffer, connects it to the speakers (audioContext.destination), and starts playback.
A source node is single-use. An
AudioBufferSourceNodecan only be started once — after it stops you must create a new one to play the same buffer again. TheAudioBufferitself is reusable, so keep the buffer and recreate the cheap source node each time:// ❌ Throws after the first play: "cannot call start more than once" // sourceNode.start(); // sourceNode.start(); // ✅ One fresh source node per playback function playOnce(buffer, ctx) { const src = ctx.createBufferSource(); src.buffer = buffer; src.connect(ctx.destination); src.start(); }
Manipulating Audio
The Web Audio API shines when you insert effect nodes between the source and the destination. The most common is the gain node for volume control.
Creating a Gain Node
function createGainNode(audioContext, volume) {
const gainNode = audioContext.createGain();
gainNode.gain.value = volume; // 1 = full volume, 0 = silent, 0.5 = half
return gainNode;
}A GainNode multiplies the signal by its gain value. The gain property is an AudioParam, which means you can also schedule smooth changes over time (for fades) instead of setting a value abruptly:
// Fade from silent to full volume over 2 seconds, avoiding clicks/pops
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(1, audioContext.currentTime + 2);Connecting Nodes for Audio Effects
function applyEffects(audioBuffer, volume, audioContext) {
const sourceNode = audioContext.createBufferSource();
sourceNode.buffer = audioBuffer;
const gainNode = createGainNode(audioContext, volume);
// source → gain → destination
sourceNode.connect(gainNode);
gainNode.connect(audioContext.destination);
sourceNode.start();
}This builds a three-node graph: the source feeds the gain node, and the gain node feeds the destination. Insert more effect nodes the same way — for example a BiquadFilterNode for tone shaping:
const filter = audioContext.createBiquadFilter();
filter.type = "lowpass"; // let low frequencies through, cut highs
filter.frequency.value = 800; // cutoff in Hz
sourceNode.connect(filter);
filter.connect(gainNode);
gainNode.connect(audioContext.destination);Order matters: the signal flows in the direction of your connect calls.
A Runnable Example: a Tone Generator
For a self-contained demo that needs no audio file, here's a tone generator built from an oscillator. It also handles the autoplay policy by resuming the context on the first click.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Simple Tone Generator</title>
</head>
<body>
<h1>Simple Tone Generator</h1>
<button onclick="playTone()">Play Tone</button>
<button onclick="stopTone()">Stop Tone</button>
<script>
let audioContext;
let oscillator;
async function playTone() {
if (!audioContext) {
audioContext = new (window.AudioContext ||
window.webkitAudioContext)();
}
// Satisfy the autoplay policy: resume on user gesture
if (audioContext.state === "suspended") {
await audioContext.resume();
}
if (!oscillator) {
oscillator = audioContext.createOscillator();
oscillator.type = "sine"; // Sine wave — other values are 'square', 'sawtooth', 'triangle'
oscillator.frequency.setValueAtTime(440, audioContext.currentTime); // A4 note, 440 Hz
oscillator.connect(audioContext.destination);
oscillator.start();
}
}
function stopTone() {
if (oscillator) {
oscillator.stop();
oscillator.disconnect();
oscillator = null; // Reset the oscillator to allow a new one to be created next time
}
}
</script>
</body>
</html>How it works:
- Oscillator node — generates a pure sine wave at 440 Hz (the A4 note used for tuning). Other
typevalues are"square","sawtooth", and"triangle", each with a different timbre. - Control functions —
playToneresumes the context and starts the oscillator;stopTonestops it, disconnects it, and resets the variable so a fresh oscillator can be created next time. - Why reset
oscillator = null? Like a buffer source, an oscillator is single-use — once stopped it cannot be restarted, so you build a new one for the next tone.
When to Use the Web Audio API
| Use case | Reach for… |
|---|---|
| Play a single background track or podcast | A plain <audio> element — simpler, less code |
| Sound effects in a game with precise timing | Web Audio API + AudioBuffer |
| Volume control, fades, filters, panning | Web Audio API gain/filter/panner nodes |
| Synthesizers and generated tones | Web Audio API OscillatorNode |
| Real-time waveform/frequency visualizers | Web Audio API AnalyserNode |
If you only need start/stop playback, the <audio> element is enough. Choose the Web Audio API the moment you need processing, timing, or synthesis.
Conclusion
The Web Audio API turns the browser into a programmable mixing console: you wire up a graph of nodes from a source, through effects, to the speakers, and control every step in real time. Keep these takeaways in mind:
- Create one
AudioContextand resume it on a user gesture before playing. - Reuse
AudioBuffers, but create a fresh source/oscillator node for each playback. - Build effects by chaining nodes with
connect(), always ending ataudioContext.destination.
From here, explore related browser capabilities such as the Web MIDI API for connecting musical instruments, or the Web Animations API to sync visuals with your audio.