Skip to content
Scrim UI

Audio Waves

A voice-input waveform with a live microphone test and a clear recording state.

Independent Scrim UI implementation of this interface pattern. This is not the original AICSS Pro source. React + Tailwind.

Preview

Voice input

Waveform preview · microphone off

Audio stays in your browser and is not uploaded.

Component source

audio-waves.tsx
"use client";

import { useEffect, useRef, useState } from "react";
import { Mic, Square } from "lucide-react";

const BARS = 24;
export function AudioWaves() {
  const [recording, setRecording] = useState(false);
  const [error, setError] = useState("");
  const [levels, setLevels] = useState<number[]>(Array(BARS).fill(0.2));
  const streamRef = useRef<MediaStream | null>(null);
  const audioRef = useRef<AudioContext | null>(null);
  const frameRef = useRef<number | null>(null);
  const stop = () => {
    if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);
    frameRef.current = null;
    streamRef.current?.getTracks().forEach((track) => track.stop());
    streamRef.current = null;
    void audioRef.current?.close();
    audioRef.current = null;
    setRecording(false);
    setLevels(Array(BARS).fill(0.2));
  };
  useEffect(() => () => {
    if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);
    streamRef.current?.getTracks().forEach((track) => track.stop());
    void audioRef.current?.close();
  }, []);
  const start = async () => {
    setError("");
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      const audio = new AudioContext();
      const analyser = audio.createAnalyser();
      analyser.fftSize = 128;
      audio.createMediaStreamSource(stream).connect(analyser);
      streamRef.current = stream;
      audioRef.current = audio;
      setRecording(true);
      const data = new Uint8Array(analyser.frequencyBinCount);
      let last = 0;
      const update = (time: number) => {
        if (time - last > 55) {
          analyser.getByteFrequencyData(data);
          setLevels(Array.from({ length: BARS }, (_, i) => Math.max(0.12, Math.min(1, data[i + 2] / 180))));
          last = time;
        }
        frameRef.current = requestAnimationFrame(update);
      };
      frameRef.current = requestAnimationFrame(update);
    } catch {
      setError("Microphone unavailable. Check browser permission and try again.");
      stop();
    }
  };
  return <div className="w-full rounded-2xl border border-(--border) bg-(--card) p-5 shadow-sm">
    <div className="flex items-center justify-between gap-3"><div><h3 className="text-sm font-semibold">Voice input</h3><p className="mt-1 text-xs text-(--muted-foreground)">{recording ? "Listening to your microphone" : "Waveform preview · microphone off"}</p></div><span className={`size-2 rounded-full ${recording ? "bg-red-500" : "bg-(--muted-foreground)"}`} aria-hidden="true" /></div>
    <div className="mt-5 flex h-20 items-center justify-center gap-1 rounded-xl bg-(--stage) px-3" role="img" aria-label={recording ? "Live microphone levels" : "Decorative audio waveform preview"}>{levels.map((level, i) => <span key={i} className={`w-1 rounded-full bg-(--primary) ${recording ? "" : "audio-waves-demo"}`} style={{ height: `${Math.max(9, Math.round(level * 72))}px`, animationDelay: `${i * 55}ms` }} />)}</div>
    <button type="button" onClick={recording ? stop : () => void start()} className="mt-4 inline-flex min-h-10 items-center gap-2 rounded-lg bg-(--primary) px-4 text-xs font-medium text-(--primary-foreground) focus-visible:outline-2 focus-visible:outline-offset-2">{recording ? <Square size={14} aria-hidden="true" /> : <Mic size={14} aria-hidden="true" />}{recording ? "Stop microphone" : "Test microphone"}</button>
    <p className="mt-3 text-xs text-(--muted-foreground)">Audio stays in your browser and is not uploaded.</p>
    <p role="status" className="mt-1 text-xs text-red-600">{error}</p>
    <style>{`@media (prefers-reduced-motion: no-preference) { @keyframes audio-waves-demo { 0%, 100% { transform: scaleY(.45) } 50% { transform: scaleY(1.5) } } .audio-waves-demo { animation: audio-waves-demo 1.2s ease-in-out infinite; } }`}</style>
  </div>;
}