Tutorial · Beginner · ~45 min
Web Audio API · Canvas 2D · Three.js

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.