The audio graph
The Web Audio API models audio processing as a directed graph
of connected nodes. Every graph has at least one source and ends at a
single destination — usually the user's speakers or headphones,
represented by ctx.destination. Between the source and the
destination you can insert any number of processing nodes: gain (volume),
equalisation, convolution reverb, compression, and more.
AudioContext
The entire API lives inside an AudioContext. You create one
instance per page and reuse it. All nodes — sources, processors, and the
destination — belong to a specific context and can only be connected within
the same context.
One important constraint: browsers suspend the AudioContext by
default until a user gesture (a click, tap, or keypress) has
occurred. This is a deliberate anti-autoplay policy. You must either
create the context inside a click handler, or call
ctx.resume() inside one. Attempting to play audio while the
context is suspended produces silence.
Typical graph structure
A simple playback-with-volume graph looks like this:
(<audio> element)
(volume)
(speakers)
.connect() call.
.connect() returns the destination node, which lets you chain
calls: src.connect(gain).connect(ctx.destination) reads naturally
left to right. Once nodes are connected, audio flows through them
automatically — you do not need to push data manually.
Browser autoplay policy: Chrome, Firefox, and Safari all
block AudioContext from running until the user has interacted
with the page. If you create the context eagerly (outside a click handler),
its state will be "suspended". Call
ctx.resume() inside your click handler to unblock it.
Connecting a media element and controlling gain
ctx.createMediaElementSource(element) takes an existing
<audio> or <video> element and wraps
it as a Web Audio source node. Once you do this, the element's audio output
is rerouted through your graph — it no longer goes directly to the
speakers until it reaches ctx.destination.
Important: call createMediaElementSource only once
If you call createMediaElementSource() on the same element
twice, the browser throws an error: "HTMLMediaElement already connected
previously to a different MediaElementSourceNode". Guard against this
with a flag so re-clicking your "Start" button is safe.
Live demo
Press Play — the AudioContext is created and the audio graph is assembled
on your first click. Drag the volume slider to set
gain.gain.value in real time; the native audio volume control
never enters the picture.
GainNode vs element.volume: Setting
gain.gain.value via the Web Audio API and setting
audio.volume directly are independent controls — their
effects multiply. If the element volume is 0.5 and the gain
node is 0.5, you hear 25% of the original level. When using
Web Audio, leave audio.volume at its default (1)
and control everything through the graph.
Analyser node and frequency visualizer
An AnalyserNode is a read-only tap — it does not change the
audio signal, it just exposes real-time frequency and time-domain data.
You insert it anywhere in the graph (typically after the source), then
read its data in a requestAnimationFrame loop to drive a
canvas visualizer.
Adding the analyser to the graph
Because createMediaElementSource() can only be called once,
the analyser must be set up at the same time as the gain node. The
analyser taps off the source in parallel with the gain path:
Drawing frequency bars on a canvas
analyser.getByteFrequencyData(dataArray) fills a
Uint8Array with values from 0–255, one per frequency bin.
You call it every frame inside a requestAnimationFrame
callback and draw the bars yourself.
Always respect prefers-reduced-motion: if the user has
requested reduced motion, skip the animation loop and draw a single
static snapshot instead.
Live visualizer demo
This demo uses its own <audio> element, separate from
the gain demo above. It has to: createMediaElementSource()
can be called only once per element — calling it again on the same element
throws an error — so each Web Audio demo needs its own
<audio>. Press "Start Visualizer", then play the audio
to see the frequency bars update.
prefers-reduced-motion: Some users configure
their OS to minimise on-screen movement (vestibular disorders, epilepsy,
preference). Calling matchMedia('(prefers-reduced-motion: reduce)')
lets your code respect that preference. For a visualizer the right response
is to draw a single static snapshot rather than a live animation loop — the
information is still there, just not moving.
Key AnalyserNode properties
| Property / Method | What it does |
|---|---|
fftSize |
FFT window size — power of 2, 32–32768. Larger = more frequency resolution, more CPU. |
frequencyBinCount |
Read-only. Always fftSize / 2. This is the length of your data array. |
getByteFrequencyData(array) |
Fills array with frequency magnitude values 0–255 (unsigned byte). |
getFloatFrequencyData(array) |
Same, but values are in decibels (float). More precise, same concept. |
getByteTimeDomainData(array) |
Fills array with waveform samples — useful for an oscilloscope-style display. |
smoothingTimeConstant |
0–1. Higher = smoother animation (values decay slowly). Default is 0.8. |
When to use the Web Audio API
The Web Audio API is powerful but has a non-trivial learning curve. Before reaching for it, ask whether a simpler tool does the job:
- Plain playback — use
<audio controls> - Custom controls — use the JS media element API (Tutorial 06)
- Volume/seeking only —
audio.volumeandaudio.currentTimeare enough - Visualizer — Web Audio API (this tutorial)
- Real-time effects — equaliser, reverb, compression — Web Audio API
- Browser games — spatial audio, synthesized sound effects — Web Audio API
- Music production apps — DAW-style sequencers — Web Audio API
For a complete, styled frequency visualizer wired up to a custom player interface, see Example 03: Audio Visualizer.
Further reading: The Web Audio API specification is maintained by the W3C Audio Working Group. MDN's Web Audio API guide is the most accessible starting point, with detailed explanations of every node type and several interactive examples.