# 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.

```bash
npm install @audio-ui/core
```

```ts
import { barCount, barLayout, barRect, barRects, bitmapLength, /* … */ } from "@audio-ui/core/waveform";
```

- Adapters: `@audio-ui/react/waveform`, `@audio-ui/elements/waveform`

| Export | Kind | What it is |
| --- | --- | --- |
| `BarLayout` | interface | Where the bars go on the canvas bitmap. |
| `BarLayoutOptions` | interface | What `barLayout` lays bars out for: the canvas's size and pixel ratio, the bars' width, gap and shortest height, and the variant. |
| `BarRect` | interface | A bar's rectangle on the bitmap, in whole device pixels. |
| `barCount` | function | How many bars of `barWidth` separated by `barGap` fit in `width`. |
| `barLayout` | function | Lay the bars out on the device pixel grid. |
| `barRect` | function | Where bar `index` goes for a level of 0…1, in whole device pixels. |
| `barRects` | function | Where each level goes, left to right. |
| `bitmapLength` | function | How many device pixels a CSS length covers on the canvas bitmap. |
| `envelopeRects` | function | Where 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. |
| `mirroredScrollRects` | function | Where 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. |
| `scrollShift` | function | How far to shift a row of `count` bars so the last one sits flush with the right edge once it has fully slid in. |
| `WaveformVariant` | type | `bars` stands the bars on the baseline, `mirror` centers them on the middle line, and `line` joins the levels into one continuous, centered shape. |
| `createLevelHistory` | function | Records a level every `intervalMs` into a window of `size` entries that scrolls: each new entry pushes the oldest one out. |
| `LevelHistory` | interface | What `createLevelHistory` returns: feed it levels, step it every frame, and read the window it scrolls and how far the newest entry has slid in. |
| `isAudible` | function | Whether `levels` carry a signal, rather than silence. |
| `loudest` | function | The loudest magnitude in `levels`. |
| `SILENCE_THRESHOLD` | const | Loudest level under which a signal counts as silence. |
| `createLevelsEngine` | function | Turns 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. |
| `LevelsEngine` | interface | What `createLevelsEngine` returns: step it every frame with the input, and read the levels and whether they have settled. |
| `LevelsEngineOptions` | interface | What 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. |
| `WaveformIdle` | type | What the bars do while there is no signal: `none` rests them, `pulse` makes them breathe together and `wave` runs a ripple along them. |
| `attachWaveformBars` | function | Draws 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. |
| `WaveformBarsOptions` | interface | The options of `attachWaveformBars`: the bar count, the peaks or a live signal, the stretch shown and the played share, and how the bars move. |
| `WaveformPlayed` | interface | The played share of the audio, 0…1, as it changes. |
| `attachWaveformCanvas` | function | Draws 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. |
| `WaveformCanvasOptions` | interface | The 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. |
| `fitPeaks` | function | Fits the stretch of `peaks` from `from` to `to` (shares of the whole, 0…1) to one high and one low per bar of `envelope`. |
| `PeakEnvelope` | interface | Where each bar reaches above and below the middle: `high` 0…1 up, `low` 0…-1 down. |
| `peakCount` | function | How many entries `peaks` holds. |
| `peakMagnitudes` | function | A level per entry of the stretch from `from` to `to`: how far each slice reaches either way. |
| `rangeShares` | function | The shares of the whole audio that the stretch `range` covers, for `fitPeaks`: `[start, end]` in `duration` units becomes `[from, to]` in 0…1. |
| `WaveformPeaks` | type | The 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. |
| `createWaveformRegionController` | function | A 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. |
| `WaveformRegionChangeEventDetails` | type | What `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. |
| `WaveformRegionCommitEventDetails` | type | What `onValueCommitted` receives once a gesture on a region ends: which edge moved, the reason and the event behind it. |
| `WaveformRegionController` | interface | What `createWaveformRegionController` returns: the snapshot to draw from, and the presses, keys and typed values that move its edges. |
| `WaveformRegionEdge` | type | What a change to a region moves: its start, its end, or the whole region with `"both"`. |
| `WaveformRegionOptions` | interface | Keys move an edge by this share of the timeline, a large step with Shift or Page Up/Down. |
| `WaveformRegionSnapshot` | interface | What a view draws a region from. |
| `WaveformRegionValue` | type | A region as `[start, end]`, in `duration` units. |
| `ScopeOptions` | interface | The box `scopePoints` draws into, in px, its inset, and the gain applied to the samples. |
| `ScopePoint` | interface | A point of the trace, in px. |
| `scopePoints` | function | Where a line through `samples` (-1…1 each) goes: left to right across the width, around the middle of the height. |
| `createWaveformController` | function | A seekable waveform's behavior with no view attached: its position, held, controlled or live, every way of moving it, and the stretch selected. |
| `WaveformSeekChangeEventDetails` | type | What `onValueChange` receives with each seek: the reason (`"press"`, `"drag"`, `"keyboard"` or `"input-change"`), the event, and `cancel()` to refuse it. |
| `WaveformSeekCommitEventDetails` | type | What `onValueCommitted` receives once a seek ends: the reason and the event behind it. |
| `WaveformSeekController` | interface | What `createWaveformController` returns: the snapshot to draw from, the hover, and the presses, keys and typed values that seek. |
| `WaveformSeekOptions` | interface | The options of `createWaveformController`: the position, held, controlled or live, the audio's length and the stretch shown, the steps, and the callbacks. |
| `WaveformSeekSnapshot` | interface | What a view draws a seekable waveform from. |
| `WaveformSelectionChangeEventDetails` | type | A stretch selected by dragging across the waveform with Shift held. |
| `WaveformSelectionCommitEventDetails` | type | The end of a stretch selected with Shift held: the reason and the event behind it. |
| `WaveformSelectionHandlers` | interface | Listens 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`](/docs/engine/analysis#waveforms) 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:

```ts
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](/docs/core/controllers) 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:

```ts
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.

**In React**

The [Waveform](/docs/components/base/waveform) draws these in React.

**In Vue, Svelte and Web**

[`<audio-ui-waveform>`](/docs/elements/waveform) draws these in your framework.

