Audio player

A waveform scrubber for an audio file, with elapsed and total time.

0:000:00
Installation
$Terminal
npx shadcn@latest add https://sdk-components.thesqd.com/r/audio-player.json
Usage
Give it a `src` and a width. Everything else — fetching, decoding, peak extraction, seeking — is handled internally.
TSImport
import { AudioPlayer, type AudioPlayerHandle } from "@/components/ui/audio-player";
TSExample
import { AudioPlayer } from "@/components/ui/audio-player";

export function Example() {
  return <AudioPlayer src="/audio/voice-sample.wav" />;
}
Composition
Anatomy of the AudioPlayer primitive.
AudioPlayer                    (single self-contained client component)
├── src                        (audio URL — fetched, decoded, cached per URL)
├── peaks                      (optional number[] 0…1 — skips the decode pass)
├── height / barWidth          (waveform geometry)
├── barGap / barRadius         (…)
├── minBarHeight               (floor so silence still reads as a waveform)
├── playOnSeek                 (click/drag also starts playback — default true)
├── showHandle                 (playhead line + drag handle — default true)
├── showTime                   (elapsed / total time row — default true)
├── scroll / pixelsPerSecond   (fixed-scale rail — on by default, 60px/s)
├── showSpeed                  (speed toggle in the time row — default false)
├── speeds / defaultSpeed      (cycle steps, default [0.5…2] starting at 1)
├── onPlayingChange / onSeeked (playback + seek callbacks)
└── ref → AudioPlayerHandle    ({ play, pause, toggle, seek, audio })

  ┌──────────────────────────────────────────┐
  │  ▂▅█▃▁▇▄  waveform canvas  ╎  ▁▃▂▅▁▂▄   │  ← scrub surface (role="slider")
  │                            ●             │  ← playhead + handle
  └──────────────────────────────────────────┘
    0:07              1×                  1:24     ← time + speed row

Renders a <canvas> waveform over a hidden <audio> element. The waveform IS
the control — there is no play button, no volume, no speed menu. While
dragging, the elapsed label tracks the handle rather than playback. Keyboard:
←/→ nudge 5s (10s with Shift), Space/Enter toggles playback.
Default
Point it at a URL and it does the rest: the clip is fetched once, decoded with the Web Audio API, and reduced to RMS peaks that render to a canvas. The waveform is laid out at a fixed `pixelsPerSecond` scale inside a horizontal `scroll-fade-x` rail, so bar spacing means the same thing whatever the clip's length, and the view follows the playhead as it plays. Played bars take the brand color, the rest stay muted; the row underneath carries the elapsed time and total duration. Clicking anywhere seeks and starts playback.
0:000:00
Compact
Thinner bars and a 32px height for inline use — a chat voice message, a table row, a list item.
0:000:00
Tall
Wider bars and more height when the waveform is the focus of the view. Bar count is derived from the container width, so the shape holds at any size.
0:000:00
Speed control
Set `showSpeed` to put a playback-speed toggle in the time row. Clicking it cycles through `speeds` — `0.5×` to `2×` by default — and applies the rate to the underlying element. Pass your own `speeds` array to change the steps, `defaultSpeed` to start somewhere other than `1×`, and `onSpeedChange` to persist the choice.
0:000:00
Long clip
The same default behaviour at length — a 4½-minute clip, which squeezed into one container width would be an unreadable smear. The rail runs past the container, `scroll-fade-x` hints at the audio either side, and the canvas only ever draws the visible slice (a full-width canvas here would be ~16,500px, past the browser's limit). Scroll the rail to scan by hand — or drag the playhead, or hit play and let the view follow. Pass `scroll={false}` if you'd rather fit the whole clip to the container.
0:000:00
External transport
The component ships no transport chrome. Grab a ref to get `play` / `pause` / `toggle` / `seek` plus the underlying `<audio>` element, and use `onPlayingChange` to keep your own button in sync. Set `playOnSeek={false}` so scrubbing doesn't start playback on its own.
0:000:00
API Reference
Props exposed by the AudioPlayer component.
PropTypeDefaultDescription
srcstring—Audio file URL. Fetched once and decoded to peaks; the result is cached per URL across mounts.
peaksnumber[]—Pre-computed waveform peaks in the 0–1 range. Supply these to skip the fetch + decode pass entirely — useful when the server already stores them.
heightnumber | string64Waveform height. A number is treated as pixels.
barWidthnumber3Width of each bar in px.
barGapnumber2Gap between bars in px. Bar count is `floor(width / (barWidth + barGap))`.
barRadiusnumber2Corner radius of each bar in px.
minBarHeightnumber2Minimum bar height in px, so silent stretches still read as a waveform rather than a gap.
playOnSeekbooleantrueStart playback when the user clicks or finishes a drag. Turn off when an external transport owns play/pause.
showHandlebooleantrueDraw the playhead line and its circular drag handle over the waveform.
showTimebooleantrueShow the elapsed / total time row under the waveform. Elapsed follows the handle while dragging; total reads the clip duration once metadata loads.
scrollbooleantrueLay the waveform out at a fixed pixels-per-second scale inside a horizontal `scroll-fade-x` scroller, following the playhead during playback. Set `false` to squeeze the whole clip into the container width instead.
pixelsPerSecondnumber60Horizontal scale of the rail. Ignored when `scroll` is `false`, where the waveform always spans the container.
showSpeedbooleanfalseShow the playback-speed toggle in the time row. Clicking it cycles through `speeds` and sets `playbackRate` on the underlying element.
speedsnumber[][0.5, 0.75, 1, 1.25, 1.5, 2]The speeds the toggle cycles through, in order.
defaultSpeednumber1Speed to start at. Include it in `speeds` or the cycle can never return to it.
onSpeedChange(speed: number) => void—Fires with the new rate each time the toggle advances.
onPlayingChange(playing: boolean) => void—Fires on play, pause, and end — use it to drive your own transport button.
onSeeked(time: number) => void—Fires with the new time (seconds) after a seek commits.
classNamestring—Merged onto the root element via `cn`.
Ref handle
Methods available on the `AudioPlayerHandle` ref.
PropTypeDefaultDescription
play()() => Promise<void> | void—Start playback.
pause()() => void—Pause playback.
toggle()() => void—Play if paused, pause if playing.
seek(time)(time: number) => void—Jump to an absolute time in seconds.
audioHTMLAudioElement | null—The underlying media element, for anything the handle doesn't cover (volume, playbackRate, …).