Lab

waveform

A waveform's drawing and controllers from @audio-ui/core: peaks fitted to bars and painted sharp on a canvas, live levels and a scrolling history, an oscilloscope trace, a seekable position and a region dragged on it.

pnpm add @audio-ui/core
import { barCount, barLayout, barRect, barRects, bitmapLength, /* … */ } from "@audio-ui/core/waveform";
Adapters@audio-ui/react/waveform, @audio-ui/elements/waveform
ExportKindWhat it is
BarLayoutinterfaceWhere the bars go on the canvas bitmap.
BarLayoutOptionsinterfaceWhat barLayout lays bars out for: the canvas's size and pixel ratio, the bars' width, gap and shortest height, and the variant.
BarRectinterfaceA bar's rectangle on the bitmap, in whole device pixels.
barCountfunctionHow many bars of barWidth separated by barGap fit in width.
barLayoutfunctionLay the bars out on the device pixel grid.
barRectfunctionWhere bar index goes for a level of 0…1, in whole device pixels.
barRectsfunctionWhere each level goes, left to right.
bitmapLengthfunctionHow many device pixels a CSS length covers on the canvas bitmap.
envelopeRectsfunctionWhere each bar of an envelope goes, in whole device pixels: from its high to its low around the middle, never thinner than the shortest bar.
mirroredScrollRectsfunctionWhere each level of a history goes when it scrolls outwards from the middle, left to right: the newest stays in the center, growing out of the one before it, while older ones slide away on both sides.
scrollShiftfunctionHow far to shift a row of count bars so the last one sits flush with the right edge once it has fully slid in.
WaveformVarianttypebars stands the bars on the baseline, mirror centers them on the middle line, and line joins the levels into one continuous, centered shape.
createLevelHistoryfunctionRecords a level every intervalMs into a window of size entries that scrolls: each new entry pushes the oldest one out.
LevelHistoryinterfaceWhat createLevelHistory returns: feed it levels, step it every frame, and read the window it scrolls and how far the newest entry has slid in.
isAudiblefunctionWhether levels carry a signal, rather than silence.
loudestfunctionThe loudest magnitude in levels.
SILENCE_THRESHOLDconstLoudest level under which a signal counts as silence.
createLevelsEnginefunctionTurns an input of any length into the level of each of count bars: fitted to the bar count, rising at once and falling smoothly, optionally mirrored, with an idle animation while silent and a sweep while loading.
LevelsEngineinterfaceWhat createLevelsEngine returns: step it every frame with the input, and read the levels and whether they have settled.
LevelsEngineOptionsinterfaceWhat a levels engine fits its input to: the bar count, the gain, the shortest level, whether the bars mirror, and what they do while idle, loading or with reduced motion.
WaveformIdletypeWhat the bars do while there is no signal: none rests them, pulse makes them breathe together and wave runs a ripple along them.
attachWaveformBarsfunctionDraws a waveform as count elements, bar(index) each: writes each one's level, 0…1, to its --waveform-level CSS variable, rising at once and falling smoothly, and nothing while they rest or root is off screen.
WaveformBarsOptionsinterfaceThe options of attachWaveformBars: the bar count, the peaks or a live signal, the stretch shown and the played share, and how the bars move.
WaveformPlayedinterfaceThe played share of the audio, 0…1, as it changes.
attachWaveformCanvasfunctionDraws a waveform on canvas, laid out on the device pixel grid: as many bars as fit its width, every edge on a physical pixel, so it stays sharp at any pixel density and zoom level.
WaveformCanvasOptionsinterfaceThe options of attachWaveformCanvas: the peaks or a live signal, the stretch shown and the played share, the variant, the bars' size, and how the drawing moves.
fitPeaksfunctionFits the stretch of peaks from from to to (shares of the whole, 0…1) to one high and one low per bar of envelope.
PeakEnvelopeinterfaceWhere each bar reaches above and below the middle: high 0…1 up, low 0…-1 down.
peakCountfunctionHow many entries peaks holds.
peakMagnitudesfunctionA level per entry of the stretch from from to to: how far each slice reaches either way.
rangeSharesfunctionThe shares of the whole audio that the stretch range covers, for fitPeaks: [start, end] in duration units becomes [from, to] in 0…1.
WaveformPeakstypeThe audio's shape, one entry per slice of it: either a level per slice (0…1, drawn the same above and below the middle) or the lowest and highest sample of each slice (-1…1), which draws the waveform as it really is, lopsided where the audio is.
createWaveformRegionControllerfunctionA region's behavior with no view attached: its span on the timeline, held or controlled, every way of moving it by its body or its edges, and the keys and input that move an edge.
WaveformRegionChangeEventDetailstypeWhat onValueChange receives with each change to a region: which edge moved, the reason ("drag", "keyboard" or "input-change"), the event, and cancel() to refuse the change.
WaveformRegionCommitEventDetailstypeWhat onValueCommitted receives once a gesture on a region ends: which edge moved, the reason and the event behind it.
WaveformRegionControllerinterfaceWhat createWaveformRegionController returns: the snapshot to draw from, and the presses, keys and typed values that move its edges.
WaveformRegionEdgetypeWhat a change to a region moves: its start, its end, or the whole region with "both".
WaveformRegionOptionsinterfaceKeys move an edge by this share of the timeline, a large step with Shift or Page Up/Down.
WaveformRegionSnapshotinterfaceWhat a view draws a region from.
WaveformRegionValuetypeA region as [start, end], in duration units.
ScopeOptionsinterfaceThe box scopePoints draws into, in px, its inset, and the gain applied to the samples.
ScopePointinterfaceA point of the trace, in px.
scopePointsfunctionWhere a line through samples (-1…1 each) goes: left to right across the width, around the middle of the height.
createWaveformControllerfunctionA seekable waveform's behavior with no view attached: its position, held, controlled or live, every way of moving it, and the stretch selected.
WaveformSeekChangeEventDetailstypeWhat onValueChange receives with each seek: the reason ("press", "drag", "keyboard" or "input-change"), the event, and cancel() to refuse it.
WaveformSeekCommitEventDetailstypeWhat onValueCommitted receives once a seek ends: the reason and the event behind it.
WaveformSeekControllerinterfaceWhat createWaveformController returns: the snapshot to draw from, the hover, and the presses, keys and typed values that seek.
WaveformSeekOptionsinterfaceThe options of createWaveformController: the position, held, controlled or live, the audio's length and the stretch shown, the steps, and the callbacks.
WaveformSeekSnapshotinterfaceWhat a view draws a seekable waveform from.
WaveformSelectionChangeEventDetailstypeA stretch selected by dragging across the waveform with Shift held.
WaveformSelectionCommitEventDetailstypeThe end of a stretch selected with Shift held: the reason and the event behind it.
WaveformSelectionHandlersinterfaceListens to the stretch a Shift + drag selects: set by the control that is dragged on.

Usage

A waveform draws either a recording's shape, known upfront, or a live signal. The shape is its peaks: a level per slice from 0 to 1, or the lowest and highest sample of each slice, which draws the waveform as it really is, lopsided where the audio is. computePeaks makes them from samples.

Painting

attachWaveformCanvas(canvas, options) draws on a canvas, as many bars as fit its width with every edge on a device pixel, so it stays sharp at any density and zoom. It paints only when something changed, and not at all while it is off screen, and returns what stops it:

import { attachWaveformCanvas } from "@audio-ui/core/waveform";

const stop = attachWaveformCanvas(canvas, {
  peaks,
  duration: buffer.duration,
  range: [0, buffer.duration],
  played: { get: () => audio.currentTime / buffer.duration, subscribe: onTimeUpdate },
  variant: "mirror",
});

variant is "mirror" (centred on the middle line), "bars" (standing on the baseline), "line" or "scope" for raw samples. range is the stretch shown, for a zoomed view; the played share is drawn in the --waveform-progress-color CSS color and the rest in --waveform-color. With levels, a live source, it follows a signal instead: across the width, or in mode: "scrolling" as a record of the loudest that moves at scrollSpeed. idle makes the bars rest, breathe or ripple while there is no signal, loading sweeps them while the audio is on its way, and reducedMotion repaints a moving waveform four times a second at most.

attachWaveformBars draws the same as one element per bar, writing each bar's level to a CSS variable, for bars styled in CSS. The layout and the fitting are functions of their own: barLayout and barRects place bars on the pixel grid, fitPeaks fits a stretch of peaks to one high and one low per bar, createLevelsEngine turns a live input into a level per bar, createLevelHistory keeps the scrolling record, and scopePoints is the trace through raw samples.

Seeking

createWaveformController makes a waveform seekable, and follows the contract every controller shares. Its position is in the units of duration, held, controlled with value, or live with source, which it follows without a render:

import { createWaveformController } from "@audio-ui/core/waveform";

const seek = createWaveformController({
  duration: buffer.duration,
  range: [0, buffer.duration],
  step: 5,
  largeStep: 30,
  onValueCommitted: (seconds) => {
    audio.currentTime = seconds;
  },
});
surface.addEventListener("pointerdown", (event) => seek.pressSeek(event, surface));

pressSeek follows a drag, keyDown takes the arrows (step), Page Up, Page Down and Shift (largeStep), typeAt sets the position from an input, and hoverAt with leave keeps the position under the pointer in hover, a store. A drag with Shift held selects a stretch instead, reported to the handlers given to setSelection. toShare and atShare convert between a position and a share of the shown stretch.

createWaveformRegionController drives a region on the waveform, [start, end]: press(edge, event, handle, track, shownRange) drags one edge, or the whole region with "both", keeping it at least minLength long; keyDown(edge, event) moves an edge with the arrows and Page keys and sends it to its ends with Home and End, and typeAt sets it from an input.

The Waveform draws these in React.

Built by Ouest Labs, the source code is available on GitHub.
RSS