Skip to content

Waveform

Live level bars and a static, seekable waveform for recordings.

Status
Stable
Level
Atom
Category
Voice
Adoption
Not used yet
import { Waveform } from "@oration/canon/components/voice/waveform";
packages/canon/src/components/voice/waveform.tsx

Call recording

Northwind Freight, Thursday, Sep 24
2:12
7:05
import { Button } from "@oration/canon/components/button";import {  DropdownMenu,  DropdownMenuContent,  DropdownMenuRadioGroup,  DropdownMenuRadioItem,  DropdownMenuTrigger,} from "@oration/canon/components/dropdown-menu";import { IconAction } from "@oration/canon/components/icon-action";import { toast } from "@oration/canon/components/toast";import { formatClock, StaticWaveform } from "@oration/canon/components/voice/waveform";import {  DownloadIcon,  PauseIcon,  PlayIcon,  RotateCcwIcon,  RotateCwIcon,} from "lucide-react";import * as React from "react";export function Hero() {    const duration = 425;    const peaks = React.useMemo(        () =>            Array.from({ length: 160 }, (_, i) => {                const pause = i % 20 > 16;                const caller = Math.floor(i / 20) % 2 === 1;                const shape =                    0.55 +                    0.45 * Math.abs(Math.sin(i * 1.7) * Math.cos(i * 0.43));                // Round peaks: long decimals in inline % heights break hydration.                return pause                    ? 0.06                    : Math.round((caller ? 0.66 : 0.52) * shape * 100) / 100;            }),        [],    );    const [progress, setProgress] = React.useState(0.31);    const [playing, setPlaying] = React.useState(false);    const [rate, setRate] = React.useState(1);    React.useEffect(() => {        if (!playing) return;        const id = window.setInterval(() => {            setProgress((p) => {                const next = p + (0.25 * rate) / duration;                if (next >= 1) {                    setPlaying(false);                    return 1;                }                return next;            });        }, 250);        return () => window.clearInterval(id);    }, [playing, rate]);    const seekBy = (seconds: number) =>        setProgress((p) => Math.min(1, Math.max(0, p + seconds / duration)));    return (        <div className="flex w-full max-w-2xl flex-col gap-3 rounded-xl bg-card p-4 text-left shadow-border">            <div className="flex items-baseline justify-between gap-3">                <h3 className="text-13 font-medium text-foreground">                    Call recording                </h3>                <span className="text-xs text-muted-foreground">                    Northwind Freight, Thursday, Sep 24                </span>            </div>            <div className="flex items-center gap-2 sm:gap-3">                <IconAction                    label={playing ? "Pause recording" : "Play recording"}                    variant="secondary"                    size="icon"                    className="shrink-0 rounded-full"                    onClick={() => {                        if (progress >= 1) setProgress(0);                        setPlaying((p) => !p);                    }}                >                    {playing ? (                        <PauseIcon                            aria-hidden="true"                            className="fill-current"                        />                    ) : (                        <PlayIcon                            aria-hidden="true"                            className="translate-x-px fill-current"                        />                    )}                </IconAction>                <div className="hidden items-center sm:flex">                    <IconAction                        label="Back 10 seconds"                        onClick={() => seekBy(-10)}                    >                        <RotateCcwIcon aria-hidden="true" />                    </IconAction>                    <IconAction                        label="Forward 10 seconds"                        onClick={() => seekBy(10)}                    >                        <RotateCwIcon aria-hidden="true" />                    </IconAction>                </div>                <span className="w-10 shrink-0 text-right text-xs text-foreground tabular-nums">                    {formatClock(progress * duration)}                </span>                <StaticWaveform                    peaks={peaks}                    progress={progress}                    onSeek={setProgress}                    duration={duration}                    height={36}                    label="Recording position"                    className="min-w-0 flex-1"                />                <span className="w-10 shrink-0 text-xs text-muted-foreground tabular-nums">                    {formatClock(duration)}                </span>                <DropdownMenu>                    <DropdownMenuTrigger                        render={                            <Button                                type="button"                                variant="ghost"                                size="sm"                                aria-label={`Playback speed, ${rate}x`}                                className="w-12 shrink-0 tabular-nums"                            />                        }                    >                        {rate}x                    </DropdownMenuTrigger>                    <DropdownMenuContent align="end" className="w-32">                        <DropdownMenuRadioGroup                            value={String(rate)}                            onValueChange={(value) => setRate(Number(value))}                        >                            {[0.75, 1, 1.25, 1.5, 2].map((option) => (                                <DropdownMenuRadioItem                                    key={option}                                    value={String(option)}                                    className="tabular-nums"                                >                                    {option}x                                </DropdownMenuRadioItem>                            ))}                        </DropdownMenuRadioGroup>                    </DropdownMenuContent>                </DropdownMenu>                <span className="hidden sm:inline-flex">                    <IconAction                        label="Download recording"                        onClick={() =>                            toast.add({                                title: "Recording download started",                                description: "cv_48213.wav, dual channel",                            })                        }                    >                        <DownloadIcon aria-hidden="true" />                    </IconAction>                </span>            </div>        </div>    );}

Usage#

Waveform draws audio in two ways. Waveform is a row of live level bars for a call in progress: the minimized call pill, the contact center call bar, a supervisor's monitor. StaticWaveform draws a recording from precomputed peaks and, given onSeek, becomes a slider you drag, click or step with the keyboard. Both are ink, never indigo. The common mistake is letting the live bars stand in for the call's status: they show that sound is happening, while a status label says whether the call is live, on hold or muted.

When to use

  • Beside a live call's status and clock, to show that someone is speaking: On call 3:42 and the bars.
  • Inside compact call chrome where the orb doesn't fit: the minimized call pill, a queue row being monitored.
  • For a call recording or voicemail with a play button, the position, the duration and a speed menu.
  • Read-only (no onSeek) for a recording preview in a list, where the row opens the full player.
  • With simulateVoiceLevel to drive demos and previews that have no audio.

When not to use

  • For the centerpiece of a live call with the agent's state. Use Voice orb
  • For latency or any other value over time. Use Latency timeline
  • For a trend in a table cell or a stat. Use Mini chart
  • For a plain progress bar of a known task. Use Progress
  • To pick a voice and play a sample of it. Use Voice picker

The Ink Fill Rule

Bars and peaks are Graphite Ink at fixed alphas: 65% for live bars, 80% for the played part of a recording, 20% for the rest. They are magnitudes, so they are never indigo.

The Label-Beside-Color Rule

Live bars show that audio is flowing, not what state the call is in. Put a status label beside them (On call, On hold, Muted) and let the bars fall flat when nobody is speaking.

The Tabular Figures Rule

Position and duration beside a recording, and the call clock beside live bars, are set in tabular figures so they don't jitter as they count.

Anatomy#

  1. Live bars. Waveform: round-ended bars, 2px wide with 2px gaps by default, tallest in the middle. Each bar scales vertically with the smoothed level.
  2. Played peaks. StaticWaveform peaks before the playhead, in Ink 80.
  3. Unplayed peaks. Peaks after the playhead in Ink 20. Every peak is at least 8% of the height, so silence still shows.
  4. Hover line. A 1px Ink 50 line under the pointer on a seekable waveform, showing where a click will land. Not shown on touch.
  5. Focus ring. A 3px Focus Indigo ring at 40% on keyboard focus, with 6px corners.

Examples#

Live tones

Ink for the agent or the person you are listening to, muted for the other side, current to inherit the parent's color. Primary exists but breaks the Ink Fill Rule.

Nora is speaking
Halcyon is speaking
Inherits text color
Primary (avoid)
import { Waveform } from "@oration/canon/components/voice/waveform";import { cn } from "@oration/canon/lib/utils";export function Tones() {    const rows = [        { tone: "ink" as const, label: "Nora is speaking" },        { tone: "muted" as const, label: "Halcyon is speaking" },        { tone: "current" as const, label: "Inherits text color" },        { tone: "primary" as const, label: "Primary (avoid)" },    ];    return (        <div className="flex flex-col gap-3">            {rows.map((row) => (                <div key={row.tone} className="flex items-center gap-4">                    <Waveform                        tone={row.tone}                        className={cn(                            row.tone === "current" && "text-destructive",                        )}                    />                    <span className="text-13 text-muted-foreground">                        {row.label}                    </span>                </div>            ))}        </div>    );}

Bars and height

Set bars, height, barWidth and gap to fit the chrome. These are the sizes the product uses.

Call pill10 bars, 16px
Call bar18 bars, 18px
Default24 bars, 24px
Monitor panel36 bars, 32px
import { Waveform } from "@oration/canon/components/voice/waveform";export function Density() {    const presets = [        { bars: 10, height: 16, where: "Call pill" },        { bars: 18, height: 18, where: "Call bar" },        { bars: 24, height: 24, where: "Default" },        { bars: 36, height: 32, barWidth: 3, gap: 3, where: "Monitor panel" },    ];    return (        <div className="flex flex-wrap items-end justify-center gap-8">            {presets.map((preset) => (                <div                    key={preset.where}                    className="flex flex-col items-center gap-2"                >                    <Waveform                        bars={preset.bars}                        height={preset.height}                        barWidth={preset.barWidth}                        gap={preset.gap}                    />                    <span className="flex flex-col items-center text-xs">                        <span className="font-medium text-foreground">                            {preset.where}                        </span>                        <span className="text-muted-foreground tabular-nums">                            {preset.bars} bars, {preset.height}px                        </span>                    </span>                </div>            ))}        </div>    );}

Driving the level

Pass level from your audio source and active for whose turn it is. Here simulateVoiceLevel stands in for two speakers, seeded differently so they don't match.

Nora0.83
Caller0.00
import { simulateVoiceLevel, Waveform } from "@oration/canon/components/voice/waveform";import * as React from "react";export function OwnLevel() {    const [t, setT] = React.useState(0);    React.useEffect(() => {        const started = performance.now();        const id = window.setInterval(            () => setT((performance.now() - started) / 1000),            60,        );        return () => window.clearInterval(id);    }, []);    const agentTurn = Math.floor(t / 3) % 2 === 0;    const agent = agentTurn ? simulateVoiceLevel(t, 1) : 0;    const caller = agentTurn ? 0 : simulateVoiceLevel(t, 4);    return (        <div className="flex w-full max-w-sm flex-col gap-3">            <div className="flex items-center gap-3">                <span className="w-20 text-13 text-foreground">Nora</span>                <Waveform level={agent} active={agentTurn} bars={28} />                <span className="ml-auto text-xs text-muted-foreground tabular-nums">                    {agent.toFixed(2)}                </span>            </div>            <div className="flex items-center gap-3">                <span className="w-20 text-13 text-foreground">Caller</span>                <Waveform                    level={caller}                    active={!agentTurn}                    bars={28}                    tone="muted"                />                <span className="ml-auto text-xs text-muted-foreground tabular-nums">                    {caller.toFixed(2)}                </span>            </div>        </div>    );}

In the call bar

The contact center strip: status label, clock, bars, then the controls. Mute or hold and the bars fall flat while the label says why.

On call3:42Jordan Lee with Orchard Street
import { Button } from "@oration/canon/components/button";import { IconAction } from "@oration/canon/components/icon-action";import { StatusLabel } from "@oration/canon/components/status-dot";import { toast } from "@oration/canon/components/toast";import { formatClock, Waveform } from "@oration/canon/components/voice/waveform";import { MicIcon, MicOffIcon, PhoneOffIcon } from "lucide-react";import * as React from "react";export function CallBar() {    const [muted, setMuted] = React.useState(false);    const [held, setHeld] = React.useState(false);    const [seconds, setSeconds] = React.useState(222);    React.useEffect(() => {        const id = window.setInterval(() => setSeconds((s) => s + 1), 1000);        return () => window.clearInterval(id);    }, []);    return (        <div className="flex w-full max-w-2xl flex-wrap items-center gap-x-3 gap-y-2 rounded-xl bg-background px-3 py-2 shadow-border">            <StatusLabel tone={held ? "warning" : "primary"} pulse={!held}>                {held ? "On hold" : muted ? "On call, muted" : "On call"}            </StatusLabel>            <span className="text-13 text-foreground tabular-nums">                {formatClock(seconds)}            </span>            <Waveform active={!muted && !held} bars={18} height={18} />            <span className="truncate text-13 text-muted-foreground">                Jordan Lee with Orchard Street            </span>            <div className="ml-auto flex items-center gap-1">                <IconAction                    label={muted ? "Unmute" : "Mute"}                    active={muted}                    onClick={() => setMuted((m) => !m)}                >                    {muted ? (                        <MicOffIcon aria-hidden="true" />                    ) : (                        <MicIcon aria-hidden="true" />                    )}                </IconAction>                <Button                    type="button"                    variant="ghost"                    size="sm"                    onClick={() => setHeld((h) => !h)}                >                    {held ? "Resume call" : "Hold"}                </Button>                <Button                    type="button"                    variant="destructive"                    size="sm"                    onClick={() =>                        toast.add({                            title: "Call ended",                            description: `Orchard Street, ${formatClock(seconds)}. Wrap-up is open.`,                        })                    }                >                    <PhoneOffIcon data-icon="inline-start" aria-hidden="true" />                    End                </Button>            </div>        </div>    );}

In the minimized call pill

Ten bars at 16px. Ink while the agent speaks, muted while it listens to you, flat when you are muted.

import { AgentAvatar } from "@oration/canon/components/agent-avatar";import { IconAction } from "@oration/canon/components/icon-action";import { toast } from "@oration/canon/components/toast";import { formatClock, Waveform } from "@oration/canon/components/voice/waveform";import { MicIcon, MicOffIcon, PhoneOffIcon } from "lucide-react";import * as React from "react";export function CallPill() {    const [speaking, setSpeaking] = React.useState(true);    const [muted, setMuted] = React.useState(false);    const [seconds, setSeconds] = React.useState(84);    React.useEffect(() => {        const id = window.setInterval(() => {            setSeconds((s) => s + 1);        }, 1000);        const turn = window.setInterval(() => setSpeaking((s) => !s), 2800);        return () => {            window.clearInterval(id);            window.clearInterval(turn);        };    }, []);    const hearing = !speaking && !muted;    return (        <div className="flex h-12 items-center gap-1 rounded-full bg-popover p-1.5 shadow-popover">            <button                type="button"                onClick={() =>                    toast.add({                        title: "Call restored",                        description: "Test call with Nora.",                    })                }                aria-label="Restore call with Nora"                className="flex h-9 min-w-0 items-center gap-2 rounded-full pr-2.5 pl-0.5 outline-none transition-colors duration-150 hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50"            >                <AgentAvatar                    agent={{                        id: "ag_payment_status",                        name: "Payments desk agent",                    }}                    size={28}                    state="live"                />                <span className="text-13 font-medium text-foreground">                    Nora                </span>                <span className="text-xs text-muted-foreground tabular-nums">                    {formatClock(seconds)}                </span>                <Waveform                    bars={10}                    height={16}                    active={speaking || hearing}                    tone={speaking ? "ink" : "muted"}                />            </button>            <IconAction                label={muted ? "Unmute" : "Mute"}                active={muted}                onClick={() => setMuted((m) => !m)}                className="rounded-full"            >                {muted ? (                    <MicOffIcon aria-hidden="true" />                ) : (                    <MicIcon aria-hidden="true" />                )}            </IconAction>            <IconAction                label="End call"                onClick={() => toast.add({ title: "Call ended" })}                className="rounded-full text-destructive hover:text-destructive"            >                <PhoneOffIcon aria-hidden="true" />            </IconAction>        </div>    );}

Read-only recordings

Without onSeek the waveform is an image. A played voicemail is drawn at full progress. The row's play button opens the player.

  • Halcyon accounts payable9:12 AM, 0:48
  • Northwind FreightYesterday, 1:11
import { IconAction } from "@oration/canon/components/icon-action";import { toast } from "@oration/canon/components/toast";import { formatClock, StaticWaveform } from "@oration/canon/components/voice/waveform";import { PlayIcon } from "lucide-react";export function ReadOnly() {    const voicemails = [        {            id: "vm1",            from: "Halcyon accounts payable",            at: "9:12 AM",            length: 48,            heard: false,        },        {            id: "vm2",            from: "Northwind Freight",            at: "Yesterday",            length: 71,            heard: true,        },    ];    return (        <ul className="flex w-full max-w-md flex-col divide-y divide-border rounded-xl bg-card shadow-border">            {voicemails.map((vm, index) => (                <li key={vm.id} className="flex items-center gap-3 px-3 py-2.5">                    <IconAction                        label={`Play voicemail from ${vm.from}`}                        variant="outline"                        onClick={() =>                            toast.add({                                title: "Playing voicemail",                                description: vm.from,                            })                        }                    >                        <PlayIcon                            aria-hidden="true"                            className="translate-x-px fill-current"                        />                    </IconAction>                    <div className="flex min-w-0 flex-1 flex-col gap-1">                        <div className="flex items-baseline justify-between gap-2">                            <span className="truncate text-13 font-medium text-foreground">                                {vm.from}                            </span>                            <span className="shrink-0 text-xs text-muted-foreground tabular-nums">                                {vm.at}, {formatClock(vm.length)}                            </span>                        </div>                        <StaticWaveform                            peaks={Array.from(                                { length: 64 },                                (_, i) =>                                    Math.round(                                        (0.2 +                                            0.75 *                                                Math.abs(                                                    Math.sin(                                                        (i + index * 9) * 1.3,                                                    ) * Math.cos(i * 0.27),                                                )) *                                            100,                                    ) / 100,                            )}                            progress={vm.heard ? 1 : 0}                            height={20}                            label={`Voicemail from ${vm.from}, ${vm.heard ? "played" : "not played"}`}                        />                    </div>                </li>            ))}        </ul>    );}

States#

Not started
Part played
Focus visible
Read-only
Live, active
Live, inactive
import { StaticWaveform, Waveform } from "@oration/canon/components/voice/waveform";import { cn } from "@oration/canon/lib/utils";export function StatesMatrix() {    const peaks = Array.from(        { length: 40 },        (_, i) =>            Math.round(                (0.25 +                    0.7 * Math.abs(Math.sin(i * 1.7) * Math.cos(i * 0.31))) *                    100,            ) / 100,    );    const cells = [        { label: "Not started", progress: 0, seek: true, className: "" },        { label: "Part played", progress: 0.42, seek: true, className: "" },        {            label: "Focus visible",            progress: 0.42,            seek: true,            className: "ring-3 ring-ring/40",        },        { label: "Read-only", progress: 0.42, seek: false, className: "" },    ];    return (        <div className="flex w-full flex-col gap-6">            <div className="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2">                {cells.map((cell) => (                    <div key={cell.label} className="flex flex-col gap-2">                        <span className="text-xs text-muted-foreground">                            {cell.label}                        </span>                        <StaticWaveform                            peaks={peaks}                            progress={cell.progress}                            onSeek={cell.seek ? () => {} : undefined}                            duration={180}                            label={`${cell.label} example`}                            className={cn(                                "pointer-events-none",                                cell.className,                            )}                        />                    </div>                ))}            </div>            <div className="grid grid-cols-2 gap-6">                <div className="flex flex-col gap-2">                    <span className="text-xs text-muted-foreground">                        Live, active                    </span>                    <Waveform />                </div>                <div className="flex flex-col gap-2">                    <span className="text-xs text-muted-foreground">                        Live, inactive                    </span>                    <Waveform active={false} />                </div>            </div>        </div>    );}
States
StateTreatment
Live, activeBars follow level, or a simulated voice when level is left out. Peaks land fast and fall slowly.
Live, inactiveactive={false} lets the bars fall to 12% height, then stops the animation loop. Use it for muted, on hold and silence.
Live, reduced motionNo loop runs. The bars hold their envelope shape at a height that steps with level in quarters, or flat when inactive.
Recording, not startedEvery peak in Ink 20.
Recording, playingPeaks before progress turn Ink 80. The caller advances progress; the waveform doesn't play audio.
Recording, hoverThe hover line follows the pointer on seekable waveforms.
Recording, draggingThe waveform captures the pointer, so a drag keeps seeking even when it leaves the bars.
Recording, focus visibleThe 3px ring. Arrow keys step 5 seconds from here.
Read-onlyWithout onSeek it is role="img", not focusable, with no hover line.

Behavior#

  • Waveform runs one requestAnimationFrame loop that writes scaleY to each bar directly. It stops once the bars settle after active turns false and restarts when it turns true again.
  • Smoothing is frame-rate independent: 40% of the way to a louder target per 60fps frame, 10% toward a quieter one.
  • Each bar gets its own flutter on top of a centered envelope, so the row reads as speech rather than an equalizer.
  • StaticWaveform is controlled: pass progress (0 to 1) and update it in onSeek. It never changes position on its own.
  • Pointer down seeks at once and captures the pointer; moving while held keeps seeking. Hover (not touch) shows the line.
  • Keyboard steps are 5 seconds when duration is given, otherwise 5%. Page Up and Page Down step twice as far.
  • simulateVoiceLevel(t, seed) returns a speech-like envelope for a time in seconds; different seeds decorrelate two speakers. formatClock(seconds) gives m:ss.

Do and don't#

On hold3:42
Do. Pair live bars with a status label and the clock, and flatten them when the line is muted or on hold.
Don't. Show bouncing bars as the only sign of a call. They look the same on hold as on a call.
Do. Keep bars and peaks in ink, with the played part darker.
Don't. Paint the waveform indigo. It is a magnitude, and indigo is for the one action and selection.
0:21
1:11
Do. Give a seekable waveform a play button, position and duration beside it.
Don't. Ship a seekable waveform with no time readout. People can't tell where they are or how long is left.

Content#

  • Name the slider for what it scrubs: Recording position, Voicemail position. The default is Playback position.
  • Give a live waveform a label only when it stands alone, such as Call audio in a supervisor's monitor. Beside a status label it stays decorative.
  • Write positions and durations as m:ss in tabular figures: 2:14 of 7:05.
  • Label the play button with its object: Play recording, Pause recording.

Accessibility#

  • A seekable StaticWaveform is role="slider" with aria-valuenow in percent and aria-valuetext in time, such as 2:14 of 7:05, when duration is given.
  • Without onSeek it is role="img" named by label.
  • Waveform is aria-hidden unless you pass label, which makes it role="img". Beside a visible status it should stay hidden.
  • Live bars don't animate under reduced motion; they hold a stepped shape. The status label carries the state either way.
  • Hit area: the waveform is full width and at least 32px tall by default; keep it at 32px or more on touch.
  • Play and pause are separate buttons with names. The waveform never starts playback itself.
Keyboard interactions
KeysAction
TabFocuses a seekable waveform.
→Forward 5 seconds, or 5% without a duration. ↑ does the same.
←Back 5 seconds, or 5%. ↓ does the same.
Page UpForward 10 seconds, or 10%.
Page DownBack 10 seconds, or 10%.
HomeJumps to the start.
EndJumps to the end.

Design tokens#

Design tokens
TokenUsed for
--foregroundLive bars at 65% (ink); peaks at 80% played and 20% unplayed; hover line at 50%
--muted-foregroundLive bars at tone="muted", for the other speaker
--primaryLive bars at tone="primary" (avoid)
currentColorLive bars at tone="current", inheriting the parent's text color
--ringFocus ring at 40% on a seekable waveform
--radius-md6px corners of the focus ring (rounded-md)

API reference#

Waveform

Live level bars.

Other props spread onto Nothing. Only the props below are read..

Props of Waveform
PropTypeDefaultDescription
levelnumberNo default0 to 1. Leave it out while active for a simulated voice.
barsnumber24Number of bars.
activebooleantrueFalse lets the bars fall flat and stops the loop.
tone"ink" | "muted" | "primary" | "current""ink"Bar color. muted for the other speaker; current inherits.
heightnumber24Height in px.
barWidthnumber2Bar width in px.
gapnumber2Gap between bars in px.
labelstringNo defaultMakes it role="img" with this name. Otherwise decorative.
classNamestringNo defaultMerged onto the row.

StaticWaveform

A recording's peaks, and a seek slider when onSeek is given.

Other props spread onto Nothing.

Props of StaticWaveform
PropTypeDefaultDescription
peaksRequirednumber[]No defaultPrecomputed peaks, 0 to 1. One bar each, sharing the width.
progressnumber0Playhead from 0 to 1. Peaks before it are drawn darker.
onSeek(progress: number) => voidNo defaultMakes it a slider. Called with 0 to 1 on click, drag and keys.
durationnumberNo defaultSeconds. Sets the spoken position and the 5-second key step.
heightnumber32Height in px.
labelstring"Playback position"Accessible name of the slider or image.
classNamestringNo defaultMerged onto the root.

simulateVoiceLevel

A speech-like level for demos: syllable flutter, phrase swells and pauses.

Props of simulateVoiceLevel
PropTypeDefaultDescription
tRequirednumberNo defaultTime in seconds.
seednumber0Decorrelates two speakers.
returnsnumberNo defaultLevel from 0 to 1.

smoothLevel

Frame-rate independent attack and release toward a target level, as the bars use it.

Props of smoothLevel
PropTypeDefaultDescription
currentRequirednumberNo defaultThe current level.
targetRequirednumberNo defaultThe level to move toward.
dtRequirednumberNo defaultSeconds since the last frame.
returnsnumberNo defaultThe next level.

formatClock

Formats seconds as m:ss.

Props of formatClock
PropTypeDefaultDescription
secondsRequirednumberNo defaultSeconds, rounded.
returnsstringNo defaultFor example 7:05.

Known gaps#

Where the implementation and the system disagree today. Follow the system, not the gap.

No product surface passes a real level: every live waveform runs on the simulated voice, and simulateVoiceLevel isn't imported anywhere in the app.

formatClock takes seconds and has no hour unit (an hour reads 60:00). The history recording player and the contact center each use their own clock formatter, one taking milliseconds, one handling hours.

tone="primary" paints the bars indigo. Nothing uses it, and it breaks the Ink Fill Rule.

StaticWaveform has no disabled or processing state. The history player hand-rolls a Well Gray box for The recording is still processing.

smoothLevel is exported but missing from the registry's export list.