Урок · Початковий рівень · ~45 хв
Web Audio API · Canvas 2D · Three.js

Візуалізатор спектра на WebAudio API

Вузол AnalyserNode з Web Audio API надає FFT-дані в реальному часі, які можна відображати у вигляді стовпців, осцилографа або тривимірного частотного ландшафту — і все це без надсилання аудіо на сервер. У цьому уроці ми пропустимо сигнал з мікрофона через аудіограф і намалюємо кілька візуалізацій.

1Налаштування AudioContext та AnalyserNode

const ctx = new AudioContext(); const analyser = ctx.createAnalyser(); // Розмір FFT має бути степенем 2; frequencyBinCount = fftSize / 2 analyser.fftSize = 2048; // 1024 кошиків analyser.smoothingTimeConstant = 0.85; // 0 = без згладжування, 1 = максимальне // буфери для зчитування даних const freqData = new Uint8Array(analyser.frequencyBinCount); // 0–255 на кошик const timeData = new Uint8Array(analyser.fftSize); // форма хвилі // з'єднуємо граф: source → analyser → destination analyser.connect(ctx.destination);
frequencyBinCount — це половина fftSize. Кожен кошик охоплює sampleRate / fftSize Гц. За fftSize = 2048 на частоті 44100 Гц це ~21,5 Гц на кошик.

2Запит доступу до мікрофона

let source; document.getElementById('start').addEventListener('click', async () => { // Має запускатися дією користувача — без автовідтворення await ctx.resume(); try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); source = ctx.createMediaStreamSource(stream); source.connect(analyser); // ПРИМІТКА: НЕ з'єднуйте analyser з ctx.destination, якщо не хочете акустичного зворотного зв'язку draw(); } catch (err) { console.error('Мікрофон недоступний:', err); } });

3Малювання 2D-діаграми спектра

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; // Колір: низькі частоти = синій, високі = червоний 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(); }

4Малювання хвилі осцилографа

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; // нормалізуємо до 0-2, центр у 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(); } // Викликайте drawWaveform() усередині циклу draw() разом із drawSpectrum()
getByteTimeDomainData повертає необроблений буфер відліків (PCM). Значення лежать у діапазоні 0–255, де 128 — тиша. Осцилограф — найпростіший спосіб переконатися, що аудіо надходить.

53D-стовпці з Three.js InstancedMesh

import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160/build/three.module.js'; const BINS = 128; // використовуємо перші 128 частотних кошиків 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); // Градієнт кольору за частотою colors.setHSL((1 - v) * 0.66, 1, 0.5); mesh.setColorAt(i, colors); } mesh.instanceMatrix.needsUpdate = true; if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true; } // Викликайте update3DBars() у циклі рендерингу
Використання InstancedMesh зводить усі 128 стовпців до єдиного виклику відмалювання, тримаючи GPU у комфортному стані навіть за високої частоти кадрів.