Skip to content

Waveform ​

The loudness of a piece of audio over time. It colors what has played up to the playhead, and a press or a drag anywhere on it seeks there.

tsx
import { Waveform } from '@adecore/ui';
tsx
import { useState } from 'react';
import { SegmentBar, Waveform } from '@adecore/ui';

const DURATION = 225;

const SECTIONS = [
    { name: 'Intro', length: 22 },
    { name: 'Verse', length: 58 },
    { name: 'Chorus', length: 64 },
    { name: 'Bridge', length: 36 },
    { name: 'Outro', length: 45 }
];

const STARTS = SECTIONS.map((_, i) => SECTIONS.slice(0, i).reduce((sum, section) => sum + section.length, 0));

// One sample per 100 ms, as a loudness analysis gives them.
const LEVELS = Array.from({ length: DURATION * 10 }, (_, i) => {
    const swell = 0.55 + 0.35 * Math.sin((i / (DURATION * 10)) * Math.PI * 3);
    const grain = 0.3 * Math.abs(Math.sin(i * 1.7) * Math.cos(i * 0.9));

    return Math.min(1, swell * 0.75 + grain);
});

export default function WaveformDemo() {
    const [time, setTime] = useState(83);
    const [scrubbing, setScrubbing] = useState(false);
    const at = STARTS.findLastIndex((start) => start <= time);

    return (
        <div className="flex w-full max-w-xl flex-col gap-2">
            <SegmentBar
                label="Sections"
                parts={SECTIONS.map((section, i) => ({ value: section.length, label: section.name, current: i === at }))}
                onSelect={(index) => setTime(STARTS[index] ?? 0)}
            />
            <Waveform
                label="Record"
                levels={LEVELS}
                duration={DURATION}
                value={time}
                onValueChange={(next) => {
                    setScrubbing(true);
                    setTime(next);
                }}
                onValueCommitted={() => setScrubbing(false)}
                marks={STARTS.slice(1)}
                className="h-32"
            />
            <span className="text-xs text-text-faint">{scrubbing ? 'Scrubbing' : 'Playing'}</span>
            <span className="mt-4 text-xs text-text-muted">Zoomed in on the chorus</span>
            <Waveform label="Score" levels={LEVELS} duration={DURATION} range={[70, 150]} value={time} marks={STARTS.slice(1)} className="h-8" />
        </div>
    );
}

levels holds loudness samples from 0 (silence) to 1, spread evenly over duration, at any rate. The waveform fits them to its width.

It fills the height className gives it, so it works as a player and as a timeline row.

tsx
<Waveform label="Record" levels={levels} duration={225} value={time} onValueChange={seek} className="h-32" />

A part of the audio ​

range shows only a stretch of the audio, such as the visible part of a zoomable timeline. Bars, marks, playhead and seeking all follow it. A SegmentBar takes the same range.

tsx
<Waveform label="Score" levels={levels} duration={225} range={[70, 150]} className="h-8" />

Seeking ​

With onValueChange the waveform is a slider. It takes focus, and a press or drag calls onValueChange with the time under the pointer. onValueCommitted follows when the drag ends.

KeyMoves the playhead
Arrow keysstep seconds
Page Up, Page DownTen steps
Home, EndTo either end

Each key commits at once and stops there, so a transport higher up does not move the playhead twice. A screen reader hears the position as "01:23 of 03:45".

onKeyDown hears every key first. The waveform leaves a key you call preventDefault on alone and lets it bubble, so a player can bind Up and Down to something else:

tsx
<Waveform
    label="Record"
    levels={levels}
    duration={225}
    value={time}
    onValueChange={seek}
    onKeyDown={(e) => {
        if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
            e.preventDefault();
        }
    }}
/>

Without onValueChange it is an image named by its label. Without value it draws no playhead.

marks draws a line through the waveform at each time, such as a cut.

Props ​

PropTypeDefault
levelsreadonly number[]Required. Loudness samples from 0 to 1, spread evenly over duration.
durationnumberRequired. In seconds.
labelstringRequired. The name a screen reader reads.
rangereadonly [from: number, to: number][0, duration]The stretch it shows, in seconds.
valuenumberHow far it has played, in seconds. Left out, it draws no playhead.
onValueChange(value: number) => voidSeeks to a time in seconds. Left out, the waveform only shows.
onValueCommitted(value: number) => voidThe time a drag lets go at, or a key moved to.
stepnumber1How far an arrow key moves the playhead, in seconds.
onKeyDown(e: KeyboardEvent<HTMLDivElement>) => voidHears a key first; after preventDefault the waveform lets it bubble.
marksreadonly number[][]Times in seconds drawn as lines.
classNamestringIts height, which it fills.
refRef<HTMLDivElement>

WaveformProps is an exported type.

Licenses and third-party notices are listed in each package.