Tutorial · Beginner · ~45 min
WebAudio API Spectrum Visualizer
The Web Audio API's AnalyserNode exposes real-time FFT data
that you can render as bars, an oscilloscope, or a 3D frequency
landscape — all without sending audio to a server. This tutorial wires a
microphone input through the audio graph and draws several
visualizations.
1Set up the AudioContext and AnalyserNode
const ctx = new AudioContext(); const analyser = ctx.createAnalyser();
// FFT size must be power of 2; frequencyBinCount = fftSize / 2
analyser.fftSize = 2048; // 1024 bins analyser.smoothingTimeConstant =
0.85; // 0 = no smoothing, 1 = sticky // buffers for data reads const
freqData = new Uint8Array(analyser.frequencyBinCount); // 0–255 per
bin const timeData = new Uint8Array(analyser.fftSize); // waveform //
connectthe graph: source → analyser → destination
analyser.connect(ctx.destination);
frequencyBinCount is half of
fftSize.
Each bin spans sampleRate / fftSize Hz. With
fftSize = 2048 at 44100 Hz that's ~21.5 Hz per bin.
2Request microphone access
let source; document.getElementById('start').addEventListener('click',
async () => { // Must be triggered by user gesture — no autoplay await
ctx.resume(); try { const stream = await
navigator.mediaDevices.getUserMedia({ audio: true, video: false });
source = ctx.createMediaStreamSource(stream);
source.connect(analyser); // NOTE: do NOT connect analyser to
ctx.destination if you don't want audio feedback draw(); } catch (err)
{ console.error('Microphone denied:', err); } });
3Draw a 2D spectrum bar chart
const canvas = document.getElementById('spectrum'); const c2d =
canvas.getContext('2d'); const W = canvas.width, H = canvas.height;
function drawSpectrum() { analyser.getByteFrequencyData(freqData);
c2d.clearRect(0, 0, W, H); const barW = W / freqData.length; for (let
i = 0; i < freqData.length; i++) { const value = freqData[i]; //
0–255 const barH = (value / 255) * H; // Colour: low freq = blue, high
= red const hue = 240 - (value / 255) * 240; c2d.fillStyle =
`hsl(${hue},100%,50%)`; c2d.fillRect(i * barW, H - barH, barW - 1,
barH); } } function draw() { requestAnimationFrame(draw);
drawSpectrum(); }
4Draw an oscilloscope waveform
const waveCanvas = document.getElementById('waveform'); const wCtx =
waveCanvas.getContext('2d'); function drawWaveform() {
analyser.getByteTimeDomainData(timeData); wCtx.clearRect(0, 0, W, H);
wCtx.lineWidth = 2; wCtx.strokeStyle = '#22c55e'; wCtx.beginPath();
const sliceW = W / timeData.length; let x = 0; for (let i = 0; i <
timeData.length; i++) { const v = timeData[i] / 128.0; // normalise
0-2, centre at 1 const y = (v / 2) * H; if (i === 0) wCtx.moveTo(x,
y); else wCtx.lineTo(x, y); x += sliceW; } wCtx.lineTo(W, H / 2);
wCtx.stroke(); } // Call drawWaveform() inside the draw() loop
alongside drawSpectrum()
getByteTimeDomainData returns the raw sample buffer
(PCM). Values range 0–255 where 128 is silence. The oscilloscope is
the simplest way to confirm audio is flowing.
53D bars with Three.js InstancedMesh
import * as THREE from
'https://cdn.jsdelivr.net/npm/three@0.160/build/three.module.js';
const BINS = 128; // use first 128 frequency bins const geo = new
THREE.BoxGeometry(0.8, 1, 0.8); const mat = new
THREE.MeshStandardMaterial({ color: 0x22c55e }); const mesh = new
THREE.InstancedMesh(geo, mat, BINS); scene.add(mesh); const dummy =
new THREE.Object3D(); const colors = new THREE.Color(); function
update3DBars() { analyser.getByteFrequencyData(freqData); for (let i =
0; i < BINS; i++) { const v = freqData[i] / 255; // 0–1 const barH
= Math.max(0.05, v * 20); const x = (i - BINS / 2) * 1.1;
dummy.position.set(x, barH / 2, 0); dummy.scale.set(1, barH, 1);
dummy.updateMatrix(); mesh.setMatrixAt(i, dummy.matrix); // Colour
gradient by frequency colors.setHSL((1 - v) * 0.66, 1, 0.5);
mesh.setColorAt(i, colors); } mesh.instanceMatrix.needsUpdate = true;
if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true; } //
Call update3DBars() in the render loop
Using InstancedMesh collapses all 128 bars into a
single draw call, keeping the GPU happy even at high frame rates.