# Audio Waveform

> Canvas audio visualiser — live bars, mirrored history or oscilloscope line from a mic or media element, or recorded peaks with a seek scrubber.

- **Slug:** `audio-waveform` · **Category:** voice · **Status:** beta
- **Import:** `import { AudioWaveform } from "@kura-ui/react/audio-waveform";`
- **Docs:** https://kuro.abstergo.fyi/components/audio-waveform
- **Registry item:** https://kuro.abstergo.fyi/r/audio-waveform.json
- **Tags:** audio, waveform, visualizer, canvas, voice, microphone, spectrum, player

## Install

```bash
# shadcn registry (copies source into your project)
npx shadcn@latest add https://kuro.abstergo.fyi/r/audio-waveform.json
# Kura CLI (same, plus dependency resolution and --json output)
npx @kura-ui/cli add audio-waveform
# npm package
npm install @kura-ui/react
```

## When to use

- Show that the mic is live while dictating or talking to a voice agent (mirrored history).
- Visualise playback of a model's spoken reply or an <audio> element (spectrum bars or line).
- A recorded voice note / audio attachment with a played/unplayed split and drag-to-seek.

## When not to use

- A complete dictation control with permissions, transcript and states — use voice-dictation (it renders this).
- An ambient “the agent is speaking” blob — use agent-orb.
- A non-audio progress indicator — use a progress bar or context-meter.

## Props

| Prop | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `analyser` | `AudioAnalyser` | — | no | Live analyser from useAudioAnalyser. Share one between several visuals. |
| `source` | `MediaStream \| HTMLMediaElement \| null` | — | no | Mic stream or media element to analyse internally when no analyser is passed. |
| `simulate` | `boolean \| "speech" \| "music" \| "silence"` | — | no | Synthetic data for docs, previews and pre-permission states. |
| `peaks` | `readonly number[]` | — | no | Static mode: amplitudes 0..1 of any length (resampled to the bar count). Build them with peaksFromSamples(). |
| `progress` | `number` | — | no | Playback position 0..1 in static mode; bars after it are faded. |
| `onSeek` | `(progress: number) => void` | — | no | Turns the waveform into a seek slider (drag, click, arrow keys). Receives 0..1. |
| `duration` | `number` | — | no | Duration in seconds for the slider's spoken value (“0:12 of 1:04”). |
| `variant` | `"bars" \| "mirrored" \| "line"` | `"mirrored"` | no | bars = bottom-aligned, mirrored = symmetric around the centre line, line = oscilloscope (live) or filled envelope (static). |
| `display` | `"history" \| "spectrum"` | — | no | Live data for bars/mirrored: history scrolls recent loudness right→left, spectrum shows frequency bands. Default: history for mirrored, spectrum for bars. |
| `barWidth` | `number` | `3` | no | Bar width in CSS px. |
| `barGap` | `number` | `2` | no | Gap between bars in CSS px. |
| `minBarHeight` | `number` | `2` | no | Minimum bar height so silence reads as a dotted baseline. |
| `tone` | `"default" \| "muted" \| "accent"` | `"default"` | no | Colour from tokens: ink, subtle ink, or the jade signal (only for live AI listening — once per view). |
| `active` | `boolean` | `true` | no | Pause the live animation; the last frame stays on screen. |
| `label` | `string` | — | no | Accessible name. Default "Audio waveform", or "Seek" when onSeek is set. |

Native attributes of the root element, `className` and `ref` are passed through.

## Anatomy

- **AudioWaveform** (`data-slot="audio-waveform"`) — Root sized by CSS (default h-10 w-full). data-mode = live | static, data-variant, data-status (analyser status), data-scrubbing.

## Accessibility

- Role: `img (display) or slider (with onSeek)`
- Display-only waveforms are role=img with a label; the canvas itself is aria-hidden.
- With onSeek it is a focusable slider with aria-valuenow (0–100) and aria-valuetext (“0:12 of 1:04” when duration is set).
- Colours come from tokens via currentColor, so contrast follows light/dark themes automatically.
- Pair live waveforms with a text status (“Listening”, “Muted”) — the visual alone is not an accessible state.
- Reduced motion: Live data becomes a calm level meter redrawn ~6×/s without sub-frame scrolling; static waveforms are unaffected.

| Keys | Action |
| --- | --- |
| ArrowRight / ArrowUp | Seek forward 5% (Shift: 10%) |
| ArrowLeft / ArrowDown | Seek back 5% (Shift: 10%) |
| Home / End | Start / end |

## Notes for agents

- Size it with CSS (className="h-8 w-48"); the canvas tracks the element size and device pixel ratio automatically.
- For a mic: const rec = useMediaRecorder(); <AudioWaveform source={rec.stream} />. For playback pass the <audio> element as source.
- Pass one `analyser` to several visuals instead of creating an analyser per component.
- peaksFromSamples(audioBuffer.getChannelData(0), 200) turns decoded audio into peaks.
- Use tone="accent" only while the AI is actively listening, and only once per view.

## Examples

### Simulated live input (minimal)

```tsx
import { AudioWaveform } from "@kura-ui/react/audio-waveform";

export function AudioWaveformMinimal() {
  return (
    <div className="w-64">
      <AudioWaveform simulate />
    </div>
  );
}
```

### Player with scrub, and every variant (full)

Recorded peaks with play/pause, drag-to-seek and a timer; below, bars/spectrum, mirrored/history and line.

```tsx
"use client";

import { Pause, Play } from "lucide-react";
import * as React from "react";
import { IconButton } from "@kura-ui/react/icon-button";
import { AudioWaveform } from "@kura-ui/react/audio-waveform";

function demoPeaks(count: number, seed = 7): number[] {
  let s = seed;
  const rand = () => {
    s = (s * 16807) % 2147483647;
    return s / 2147483647;
  };
  return Array.from({ length: count }, (_, i) => {
    const phrase = Math.sin((i / count) * Math.PI * 5.3) * 0.5 + 0.5;
    const gap = i % 37 < 3 ? 0.08 : 1;
    return Math.min(1, (0.15 + 0.85 * phrase * (0.45 + rand() * 0.55)) * gap);
  });
}

const DURATION = 64;

export function AudioWaveformFull() {
  const peaks = React.useMemo(() => demoPeaks(220), []);
  const [progress, setProgress] = React.useState(0.32);
  const [playing, setPlaying] = React.useState(false);

  React.useEffect(() => {
    if (!playing) return;
    const id = setInterval(() => {
      setProgress((p) => {
        const next = p + 0.1 / DURATION;
        if (next >= 1) {
          setPlaying(false);
          return 1;
        }
        return next;
      });
    }, 100);
    return () => clearInterval(id);
  }, [playing]);

  const t = progress * DURATION;
  return (
    <div className="flex w-full max-w-lg flex-col gap-6">
      <div className="flex items-center gap-3 rounded-kura-xl bg-kura-surface p-2 pr-4 shadow-kura-sm">
        <IconButton
          label={playing ? "Pause" : "Play"}
          icon={playing ? <Pause className="fill-current" /> : <Play className="fill-current" />}
          variant="primary"
          size="md"
          className="rounded-full"
          tooltip={false}
          onClick={() => {
            if (progress >= 1) setProgress(0);
            setPlaying((p) => !p);
          }}
        />
        <AudioWaveform
          peaks={peaks}
          progress={progress}
          onSeek={setProgress}
          duration={DURATION}
          variant="mirrored"
          barWidth={2}
          barGap={2}
          minBarHeight={2}
          label="Seek recording"
          className="h-9 flex-1"
        />
        <span className="w-9 shrink-0 text-right text-kura-fg-muted text-xs tabular-nums">
          {Math.floor(t / 60)}:{String(Math.floor(t % 60)).padStart(2, "0")}
        </span>
      </div>
      <div className="grid grid-cols-3 gap-4">
        <figure className="flex flex-col gap-2">
          <AudioWaveform simulate="music" variant="bars" display="spectrum" className="h-12" />
          <figcaption className="text-center text-kura-fg-subtle text-xs">
            bars · spectrum
          </figcaption>
        </figure>
        <figure className="flex flex-col gap-2">
          <AudioWaveform simulate="speech" variant="mirrored" display="history" className="h-12" />
          <figcaption className="text-center text-kura-fg-subtle text-xs">
            mirrored · history
          </figcaption>
        </figure>
        <figure className="flex flex-col gap-2">
          <AudioWaveform simulate="speech" variant="line" tone="muted" className="h-12" />
          <figcaption className="text-center text-kura-fg-subtle text-xs">line</figcaption>
        </figure>
      </div>
    </div>
  );
}
```

### Voice note in a chat bubble (composition)

A compact muted waveform with an icon-button play control.

```tsx
"use client";

import { Play } from "lucide-react";
import * as React from "react";
import { IconButton } from "@kura-ui/react/icon-button";
import { AudioWaveform } from "@kura-ui/react/audio-waveform";

function demoPeaks(count: number, seed = 7): number[] {
  let s = seed;
  const rand = () => {
    s = (s * 16807) % 2147483647;
    return s / 2147483647;
  };
  return Array.from({ length: count }, (_, i) => {
    const phrase = Math.sin((i / count) * Math.PI * 5.3) * 0.5 + 0.5;
    const gap = i % 37 < 3 ? 0.08 : 1;
    return Math.min(1, (0.15 + 0.85 * phrase * (0.45 + rand() * 0.55)) * gap);
  });
}

export function AudioWaveformVoiceNote() {
  const peaks = React.useMemo(() => demoPeaks(48, 3), []);
  const [progress, setProgress] = React.useState(0);
  return (
    <div className="flex w-full max-w-sm flex-col items-end gap-2">
      <div className="flex w-64 items-center gap-2.5 rounded-kura-2xl rounded-br-kura-sm bg-kura-surface-sunken py-2 pr-3.5 pl-2 shadow-kura-xs">
        <IconButton
          label="Play voice note"
          icon={<Play className="fill-current" />}
          variant="secondary"
          size="sm"
          className="rounded-full"
          tooltip={false}
          onClick={() => setProgress((p) => (p >= 1 ? 0 : Math.min(1, p + 0.25)))}
        />
        <AudioWaveform
          peaks={peaks}
          progress={progress}
          onSeek={setProgress}
          duration={14}
          barWidth={2}
          barGap={2}
          tone="muted"
          label="Seek voice note"
          className="h-7 flex-1"
        />
        <span className="text-[11px] text-kura-fg-subtle tabular-nums">0:14</span>
      </div>
      <span className="pr-1 text-[11px] text-kura-fg-subtle">Voice note · transcribed</span>
    </div>
  );
}
```

## Dependencies

- npm: none
- Kura registry items: `kura-theme`, `kura-merge-refs`, `use-audio-analyser`, `use-reduced-motion`, `utils`
- Related: `voice-dictation`, `agent-orb`, `attachment`
