Audio-reactive visual

오디오 반응형 비주얼

Driving bar heights or object scale in real time from a music track’s per-frequency-band energy.

Also known as: AnalyserNode spectrumMusic visualizerFFT bars
···
js
import * as THREE from 'three';
import { SimplexNoise } from 'three/addons/math/SimplexNoise.js';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x05060c);
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
camera.position.set(0, 2.6, 5.4);
camera.lookAt(0, 0, 0);
scene.add(new THREE.AmbientLight(0x445577, 1.3));
const dl = new THREE.DirectionalLight(0xffffff, 1.1);
dl.position.set(3, 5, 3);
scene.add(dl);

const base = new THREE.Mesh(new THREE.PlaneGeometry(12, 4), new THREE.MeshBasicMaterial({ color: 0x0a0e1a }));
base.rotation.x = -Math.PI / 2;
base.position.y = -0.02;
scene.add(base);

const BARS = 28;
const geo = new THREE.BoxGeometry(0.28, 1, 0.28);
const mat = new THREE.MeshStandardMaterial({ roughness: 0.4 });
const mesh = new THREE.InstancedMesh(geo, mat, BARS);
const dummy = new THREE.Object3D();
const color = new THREE.Color();
const simplex = new SimplexNoise();
scene.add(mesh);

function resize() { renderer.setSize(innerWidth, innerHeight); camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); }
addEventListener('resize', resize); resize();
renderer.setAnimationLoop((t) => {
  const time = t * 0.001;
  for (let i = 0; i < BARS; i++) {
    const band = i / (BARS - 1);
    const bass = Math.max(0, Math.sin(time * 2.4) * 0.5 + 0.5) * Math.exp(-band * 2.5);
    const n = (simplex.noise(band * 3.0, time * 1.6) + 1) * 0.5;
    const mag = 0.15 + bass * 1.1 + n * 0.9 * (1.0 - band * 0.4);
    dummy.position.set((i - (BARS - 1) / 2) * 0.36, mag / 2, 0);
    dummy.scale.set(1, Math.max(0.05, mag), 1);
    dummy.updateMatrix();
    mesh.setMatrixAt(i, dummy.matrix);
    color.setHSL(0.62 - band * 0.45, 0.85, 0.55 + mag * 0.12);
    mesh.setColorAt(i, color);
  }
  mesh.instanceMatrix.needsUpdate = true;
  if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
  renderer.render(scene, camera);
});

In a real app, the Web Audio API’s AnalyserNode.getByteFrequencyData() returns audio as an array of per-frequency-band energy (an FFT — Fast Fourier Transform — result), and each slot maps directly to one bar’s height — low indices are bass, high indices are treble. Since this site can’t use a microphone or audio file, it fakes "a plausible spectrum" instead by blending several sine waves with SimplexNoise: the bass band pulses slowly and hard with a sin(), the rest jitter with noise, and overall magnitude shrinks exponentially as the band index rises (Math.exp(-band*2.5)) — the same lows-are-big, highs-are-small shape real music spectra have.

All 28 bars are drawn with a single THREE.InstancedMesh (see instanced-mesh), so there’s one draw call. Every frame each instance’s transform (scaling dummy.scale.y for height) and color (mesh.setColorAt) get recomputed, then pushed to the GPU with mesh.instanceMatrix.needsUpdate = true and mesh.instanceColor.needsUpdate = true.

Hooking up real audio just means swapping this demo’s "fake spectrum" step for the result of analyser.getByteFrequencyData(dataArray) — the rendering pipeline carries over unchanged. Keep in mind browsers only let an AudioContext play after a user gesture like a click, so getByteFrequencyData won’t return real data before that.

When to use

Music player spectrum equalizers, live-stream overlays, or a "listening" indicator while waiting for speech input.