# Sounds

> Release history for @arraypress/waveform-sounds.

<!-- GENERATED by scripts/sync-changelogs.mjs — do not edit by hand. -->

:::note
Generated from [`@arraypress/waveform-sounds`'s CHANGELOG](https://github.com/arraypress/waveform-sounds/blob/main/CHANGELOG.md). Run `npm run sync:changelogs` after a release to refresh.
:::

The Astro, React, Svelte and Vue wrappers (`@arraypress/waveform-sounds-astro`, `-react`, `-svelte`, `-vue`) version separately — see [Framework wrappers](/changelog/frameworks/#arraypresswaveform-sounds-astro).

## [Unreleased]

## [0.3.1] — 2026-10-08

### Fixed

- **The strip's docked player no longer starts empty.** It showed a play
  button, no waveform and `0:00 / 0:00`, and its button did nothing. The
  first sound the list shows is now cued in it — title, waveform, length —
  without playing (metadata only, the browser default for any `<audio>`),
  and its button plays it. Its row isn't marked until it plays, `current`
  stays `null`, `next()` starts with it, and a filter change re-cues the
  first sound still shown until something plays.
- **The docked player stays in place.** It was `position: sticky`, so it
  slid along the bottom of the screen over the rows as the page scrolled.
  It's static now; `--ws-strip-position: sticky` brings the old behaviour
  back for a long list.
- **Rows and the player agree on lengths.** Rows rounded to the nearest
  second and the player's clock rounds down, so a 7.6s sound read `0:08`
  in its row and `0:07` in the player. Rows round down too now.

## [0.3.0] — 2026-10-07

### Changed

- **BPM is a menu with a two-handle range** instead of two number fields.
  The button reads `Any BPM`, or `120–128 BPM` once set; its panel holds a
  range across the pack's own tempos (two native range inputs on one track:
  arrow keys, screen readers and touch work as they do on any slider), the
  live readout and an `Any BPM` reset. The label follows the drag; the list
  re-filters once it settles. A handle left at an end is no limit on that
  side, so a range nobody narrowed never hides the sounds without a BPM.
  `?bpm=` in the address and `setFilter({bpmMin, bpmMax})` work as before.
  New strings: `anyBpm`, `bpmRange`.
- **On a list that marks loops, Loop starts on.** Only loops repeat (as in
  0.2.0), so loops repeat out of the box and one-shots still play once. The
  `loop` option now defaults to `null` (follow the data); `true` / `false`
  still force it. A list that marks nothing starts with it off, as before.

### Removed

- `.ws-bpm-range` and its number inputs (`data-ws-bpm-min` / `-max` are
  now the range handles).

## [0.2.0] — 2026-10-07

### Added

- **Loops and one-shots.** A sound can be marked `loop: true`; anything
  else is a one-shot. Marking loops does three things:
  - a small loop icon beside the name (with "Loop" for screen readers);
  - an **All / Loops / One-shots** filter, offered only when the list has
    both kinds (`filters` entry `'loop'`, on by default; in the address
    as `?loop=loop` / `?loop=one-shot` with `urlState`);
  - the Loop toggle repeats only loops. A one-shot always plays once.
  A list that marks no loops behaves exactly as before.
- `isLoop()` and `LOOP_FILTERS` in the data module; `facets()` reports
  `loops` and `oneShots`; four new strings (`loopFilter`, `loops`,
  `oneShots`, `isLoop`).

### Changed

- The title cell wraps its text in `.ws-title-text` (so a loop's icon can
  sit after an ellipsised name). Styling `.ws-title` still works.

## [0.1.3] — 2026-10-07

### Fixed

- **Sounds under a second show their length.** `formatDuration` rounded to
  whole seconds, so a 0.38s one-shot read `0:00` — which looks like a
  missing length, and one-shots (kicks, hats, single notes) are routinely
  that short. Under a second it now shows tenths: `0.4s`.
- **The `strip` player matches a dark page.** Its background fell back to the
  system `Canvas` colour, which is white on a dark page that doesn't declare
  `color-scheme: dark` — a white box under a dark list. It now falls back to
  `--ws-surface` (the measured page surface), as the dropdowns already did.
- **No stray `·` on a narrow screen.** The mobile meta line put a separator
  before a cell whenever any cell came first, even an empty one, so a sound
  with no key read `· 0:08`. It now follows only a cell with content.

## [0.1.2] — 2026-10-07

### Fixed

- **The `strip` layout keeps its docked styling.** The engine was mounted on
  the `.ws-engine` slot itself, and `WaveformPlayer` resets its container's
  `className`, so `ws-engine--strip` was wiped on the first play: no sticky
  position, border, padding, `--ws-strip-bg` or `--ws-radius`. The engine now
  mounts into a child of the slot (`[data-ws-engine-mount]`, made at runtime,
  so server-rendered markup from 0.1.0/0.1.1 works unchanged) and `destroy()`
  removes it.
- `render.d.ts` declares `escapeHtml`, which `/render` has always exported.

## [0.1.1] — 2026-10-06

### Fixed

- **The playing row's progress is smooth.** It was painted a whole bar at a
  time (a bar switched colour once the playhead passed its middle), so a
  short clip advanced in visible steps — an 8-second loop across ~120 bars
  moved ~15 times a second however often it was redrawn. The bars are now
  drawn twice, the second pass clipped at the exact playhead pixel: the
  played part grows through the middle of a bar. Measured in Chrome: the
  edge moves every ~1px (≈37 times a second on a 12-second clip at 60fps).
- Bar geometry is a pure, tested function (`barRects`), and each pass is a
  single `fill()` of a `Path2D` instead of one `fillRect` per bar.

## [0.1.0] — 2026-10-06

### Added

- First release: a searchable, filterable list of sounds for showcasing the
  previews inside a sample pack, preset bank or sound library.
- One `WaveformPlayer` (self mode) is the audio engine for the whole list, so
  playback gets the player's seeking (including hosts that ignore byte
  ranges), error handling, Media Session card and `singlePlay` hand-off.
- Two layouts: `inline` (a mini waveform per row; the playing row fills and
  is click-to-seek) and `strip` (plain rows, the engine docked below).
- Search (every word, any order; a number matches the BPM), type chips (a
  menu past `maxTypeChips`), key menu, BPM range, sort, Loop toggle, paging.
- Keyboard: ↑/↓ move and, while a sound plays, audition the next; ←/→ seek;
  `/` focuses the search; ↓ from the search enters the list.
- Peaks as an 8-bit hex string (two characters per bar).
- `@arraypress/waveform-sounds/render`: a DOM-free renderer for server
  rendering; the runtime adopts its markup instead of rebuilding it.
- `@arraypress/waveform-sounds/no-autoinit`.
- Dropdowns for type (past `maxTypeChips`), key and sort: a button and a
  popup listbox, with a search field past `menuSearch` (8) options; ↑/↓,
  Enter, Esc, type to narrow (WAI-ARIA combobox/listbox).
- Every control is optional: `search`, `filters`, `sorts` (which orders,
  in order; the first is the starting order), `loopToggle`, `showCount`,
  `columns`.
- Optional per-sound `download` link (a download button on that row only).
- `urlState`: the filters and sort live in the address (shareable,
  survive a refresh), optionally prefixed for several lists on a page.
- The engine is built when the list is ready (no audio loaded), so
  waveform-tracker and other `waveformplayer:ready` hooks see the first play.
- Colour-agnostic by default: the "on" states are inverted (text colour as
  fill, page surface as ink). The surface is detected (`--ws-surface`) and
  re-read on theme flips; set it yourself for the first server paint.
