← Back
Canvas

Audio Visualizer

Web Audio API oscillator feeds an AnalyserNode; FFT frequency bins rendered as a color-shifting bar chart.

freq220Hz

Web Audio API · OscillatorNode · AnalyserNode · FFT 256 bins

AudioVisualizer.tsx137 lines
"use client";

import { useEffect, useRef, useState } from "react";

type WaveType = OscillatorType;
const WAVE_TYPES: WaveType[] = ["sine", "sawtooth", "square", "triangle"];

export default function AudioVisualizer() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const audioRef = useRef<{ ctx: AudioContext; osc: OscillatorNode; gain: GainNode } | null>(null);
  const rafRef = useRef<number>(0);
  const [playing, setPlaying] = useState(false);
  const [wave, setWave] = useState<WaveType>("sawtooth");
  const [freq, setFreq] = useState(220);
  const waveRef = useRef(wave);
  const freqRef = useRef(freq);

  useEffect(() => { waveRef.current = wave; if (audioRef.current?.osc) audioRef.current.osc.type = wave; }, [wave]);
  useEffect(() => { freqRef.current = freq; audioRef.current?.osc.frequency.setValueAtTime(freq, audioRef.current.ctx.currentTime); }, [freq]);

  function start() {
    const ac = new AudioContext();
    const analyser = ac.createAnalyser();
    analyser.fftSize = 256;

    const osc = ac.createOscillator();
    const gain = ac.createGain();
    gain.gain.value = 0.08;
    osc.type = waveRef.current;
    osc.frequency.value = freqRef.current;

    osc.connect(gain);
    gain.connect(analyser);
    analyser.connect(ac.destination);
    osc.start();

    audioRef.current = { ctx: ac, osc, gain };
    setPlaying(true);
    drawLoop(analyser);
  }

  function stop() {
    cancelAnimationFrame(rafRef.current);
    audioRef.current?.osc.stop();
    audioRef.current?.ctx.close();
    audioRef.current = null;
    setPlaying(false);
    const canvas = canvasRef.current;
    if (!canvas) return;
    canvas.getContext("2d")?.clearRect(0, 0, canvas.width, canvas.height);
  }

  function drawLoop(analyser: AnalyserNode) {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    if (!ctx) return;
    const buf = new Uint8Array(analyser.frequencyBinCount);

    const frame = () => {
      analyser.getByteFrequencyData(buf);
      const W = canvas.width;
      const H = canvas.height;
      ctx.clearRect(0, 0, W, H);

      const bw = W / buf.length;
      buf.forEach((v, i) => {
        const h = (v / 255) * H;
        const hue = (i / buf.length) * 240 + 180;
        ctx.fillStyle = `hsla(${hue},80%,60%,0.85)`;
        ctx.fillRect(i * bw, H - h, bw - 1, h);
      });

      rafRef.current = requestAnimationFrame(frame);
    };
    frame();
  }

  useEffect(() => () => stop(), []);

  return (
    <div className="w-full flex flex-col items-center gap-5">
      <canvas
        ref={canvasRef}
        width={512}
        height={160}
        className="w-full h-40 rounded-xl bg-zinc-950"
      />

      <div className="flex flex-wrap justify-center gap-3 w-full">
        {/* wave type */}
        <div className="flex gap-1.5">
          {WAVE_TYPES.map((w) => (
            <button
              key={w}
              onClick={() => setWave(w)}
              className={`px-3 py-1.5 rounded-full text-xs font-medium transition-colors ${
                wave === w
                  ? "bg-violet-600 text-white"
                  : "bg-zinc-100 dark:bg-zinc-800 text-zinc-500 hover:bg-zinc-200 dark:hover:bg-zinc-700"
              }`}
            >
              {w}
            </button>
          ))}
        </div>

        {/* freq slider */}
        <div className="flex items-center gap-2">
          <span className="text-[10px] font-mono text-zinc-400">freq</span>
          <input
            type="range" min={55} max={880} value={freq}
            onChange={(e) => setFreq(Number(e.target.value))}
            className="w-24 accent-violet-600"
          />
          <span className="text-[10px] font-mono text-zinc-400 tabular-nums w-10">{freq}Hz</span>
        </div>
      </div>

      <button
        onClick={playing ? stop : start}
        className={`px-6 py-2.5 rounded-full text-sm font-medium transition-colors ${
          playing
            ? "bg-red-500 hover:bg-red-600 text-white"
            : "bg-violet-600 hover:bg-violet-700 text-white"
        }`}
      >
        {playing ? "Stop" : "Play & Visualize"}
      </button>

      <p className="text-[10px] font-mono text-zinc-400 dark:text-zinc-600">
        Web Audio API · OscillatorNode · AnalyserNode · FFT 256 bins
      </p>
    </div>
  );
}

More in Canvas