Audio Visualizer

Drawing live frequency bars with the Web Audio API

Background

This example builds directly on Tutorial 08: Audio Processing with the Web Audio API, which introduced the AudioContext, node graph model, and the AnalyserNode. Here, all of those pieces are combined into a self-contained visualizer: a <canvas> element that draws live frequency bars while the audio plays, and stops drawing when playback pauses or ends.

Browser autoplay policy requires audio to be started by an explicit user gesture — a button click. The AudioContext is therefore created (or resumed) only on click, never on page load. The MediaElementSource is connected to the graph exactly once, guarded by a flag so the same audio element is not re-routed on subsequent clicks.

Live demo

Click Play & Visualize to start the audio and watch the frequency bars animate. Click Pause to stop both. If your system preferences have reduced motion enabled, a single static bar snapshot will be drawn instead of an animation loop.

Ready

The Web Audio graph

The key to the visualizer is the node graph: audio flows from a MediaElementSource through an AnalyserNode and on to the speakers (ctx.destination). The analyser sits in the middle and can inspect the signal without affecting it.

/* Create the AudioContext lazily (must follow a user gesture) */ var audioCtx = null; var analyser = null; var connected = false; function setupGraph() { if (!audioCtx) { audioCtx = new AudioContext(); } audioCtx.resume(); if (!connected) { var audio = document.getElementById('viz-audio'); var src = audioCtx.createMediaElementSource(audio); analyser = audioCtx.createAnalyser(); analyser.fftSize = 256; /* 128 frequency bins */ src.connect(analyser); analyser.connect(audioCtx.destination); connected = true; /* call once — guard the flag */ } }

Call createMediaElementSource only once. Calling it a second time on the same <audio> element throws an InvalidStateError. The connected flag ensures the graph is built exactly once, no matter how many times the user clicks Play.

The draw loop

Each animation frame, getByteFrequencyData() fills a Uint8Array with the current amplitude of each frequency bin (values 0–255). The loop draws a bar for each bin whose height is proportional to its amplitude, then requests the next frame — but only while the audio is still playing.

var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches; var rafId = null; function drawFrame() { var W = canvas.width; var H = canvas.height; var data = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(data); ctx2d.clearRect(0, 0, W, H); ctx2d.fillStyle = '#111827'; ctx2d.fillRect(0, 0, W, H); var barW = W / data.length; for (var i = 0; i < data.length; i++) { var barH = (data[i] / 255) * H; ctx2d.fillStyle = PALETTE[Math.floor((i / data.length) * (PALETTE.length - 1))]; ctx2d.fillRect(i * barW, H - barH, barW - 1, barH); } } function loop() { drawFrame(); if (!audio.paused && !audio.ended) { rafId = requestAnimationFrame(loop); /* keep looping while playing */ } else { rafId = null; } } /* Start: respect prefers-reduced-motion */ audio.play().then(function () { if (reduce) { drawFrame(); /* one static snapshot — no rAF loop */ } else { loop(); /* continuous animation */ } });

Why stop the loop on pause? Continuing to call requestAnimationFrame while paused wastes CPU and battery. The loop checks audio.paused every frame and exits naturally — no extra bookkeeping needed.

Reduced-motion accessibility

Some users configure their operating system to request reduced motion — for example, to avoid triggering vestibular disorders. The prefers-reduced-motion media query exposes this preference to CSS and JavaScript. When it matches, the visualizer draws exactly one static frame and skips the requestAnimationFrame loop entirely.

var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches; audio.play().then(function () { if (reduce) { drawFrame(); /* single snapshot — motion-safe */ } else { loop(); /* animated — only for users who are OK with it */ } });

You can test this in Chrome DevTools: open the Rendering panel (via the three-dot menu → More tools → Rendering) and check Emulate CSS media feature prefers-reduced-motion: reduce. Click Play on the demo above — you should see a single frozen frame instead of the animated bars.