# WebMusic complete documentation
> All public documentation pages, including setup, API, components and Agent Toolkit guidance.
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/kernel@0.1.0, @webmusic/ui@0.1.0, @webmusic/score@0.1.0, @webmusic/audio@0.1.0, @webmusic/bridge@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline. Documentation revision 82bd1e410be200e9c5fe745bab18f9dab31e43c4c3237acf649d401a6b7a2b1a.
[Documentation index](https://koperative-lab.github.io/WebMusic/llms.txt)
# AGENTS.md
> Copy WebMusic guidance into your application’s AGENTS.md.
Canonical documentation: https://koperative-lab.github.io/WebMusic/agent-toolkit/agents-md/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/kernel@0.1.0, @webmusic/ui@0.1.0, @webmusic/score@0.1.0, @webmusic/audio@0.1.0, @webmusic/bridge@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
Copy the section below into your application's `AGENTS.md`. Keep its existing
rules and adapt the guidance to your installed WebMusic version. See the
[AGENTS.md format guide](https://agents.md/) for general project instructions.
## Copy into your project instructions
```markdown
## WebMusic
- Check package.json, the lockfile and installed @webmusic packages before
coding. Confirm imports against their public exports and TypeScript
declarations. Match references to the installed version; do not assume a
development API is released or silently upgrade packages to use it.
- Follow this application's integration choice: Web Components for ready-made
interactions, Headless for behavior behind custom UI, or API + UI for explicit
composition. API-only parsing and analysis need no UI. Preserve the existing
framework and styling conventions.
- Start from the relevant public reference:
- [Context index](https://koperative-lab.github.io/WebMusic/llms.txt)
- [Setup](https://koperative-lab.github.io/WebMusic/score/)
- [First composition](https://koperative-lab.github.io/WebMusic/quick-start/)
- [Components](https://koperative-lab.github.io/WebMusic/score/element/)
- [Headless](https://koperative-lab.github.io/WebMusic/score/headless/)
- [API and shared playback](https://koperative-lab.github.io/WebMusic/score/api/)
- [UI presenters](https://koperative-lab.github.io/WebMusic/uikit/)
- Use the documented dependencies and resources for the chosen entry. Keep
browser-only operations in the client lifecycle. Start sound from a user
action and provide loading and error feedback.
- Keep units explicit: nominal seconds, transport seconds, quarter notes, MIDI
pitches and velocity ranges follow the owning API. Use matching position
and duration axes; do not infer units from a field name alone.
- Let related views and analysis borrow the intended player's score, state and
time where supported. Do not create a second player merely to draw a view.
- Track ownership of players, audio contexts, workers and subscriptions.
Release owned resources during teardown; detach from borrowed resources
without disposing them. Handle cancellation and late results when inputs
change. Destroying a UI presenter does not dispose its state owner.
- Run this application's checks and verify the affected browser interaction,
including cleanup. State which checks passed and which audible, timing or
device behavior remains unverified.
```
Add your application's music-file location, integration choice and verification
commands alongside this section. If `AGENTS.md` does not exist, create it in
your application's root.
For offline work, save the relevant [documentation bundles]()
and add their local paths to the section. Review saved context and project
instructions when upgrading WebMusic.
## Related
- [Agent Toolkit overview]()
- [Documentation downloads]()
- [Reusable Skills]()
---
# Agent Toolkit
> Use WebMusic public references and project instructions to develop music applications with an AI agent.
Canonical documentation: https://koperative-lab.github.io/WebMusic/agent-toolkit/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/kernel@0.1.0, @webmusic/ui@0.1.0, @webmusic/score@0.1.0, @webmusic/audio@0.1.0, @webmusic/bridge@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
Give your AI agent the WebMusic references for the interaction you want to
build, along with your application's framework and installed package versions.
The agent can use those references to choose public imports, connect playback
and views, and check its work in your application.
Start with [machine-readable documentation](), install
the [WebMusic skill]() from a source checkout, or copy a
[project instruction section](). Each works independently;
choose the entry that fits your agent.
## Choose an integration
| Your application needs | Start with | Public reference |
| --- | --- | --- |
| A ready-made player, musical input or view | Web Components | [Quick Start]() and [component inventory]() |
| WebMusic behavior behind your own interface | Headless objects | [Headless inventory]() |
| Score data operations, custom rendering or independent controls | API + UI | [Score API]() and [UI Kit]() |
These choices can coexist. For example, a custom interface can use a Headless
player and attach views to its shared playback source. See
[Score setup]() for dependencies and entry-specific requirements.
The published toolkit covers Score, UI Kit and Kernel. Use the installed
package's exports and declarations to confirm an API before adding code;
development-branch features may not be available in that version.
## Start with a concrete request
Copy this into your agent's conversation from your application project:
```text
Add a MIDI or MusicXML player with synchronized staff notation and a keyboard
readout to this application using WebMusic.
Start with https://koperative-lab.github.io/WebMusic/quick-start/
and https://koperative-lab.github.io/WebMusic/score/.
Inspect the framework, package manager and installed WebMusic versions first.
Use the public entries supported by those versions. Let one player own the
score and playback; attach both views to it. Include loading and error feedback
and start sound from a user action. Follow this project's styling conventions.
Run the application's checks and verify loading, play, pause, seeking and
cleanup. Report what was tested and any browser or sound behavior not verified.
```
Provide the music file or its public URL, the component or page to change, and
any interaction requirements. If the agent cannot open the documentation,
paste the relevant example and reference sections into the conversation.
## Keep useful context close
- [llms.txt and documentation downloads](): start with the index, or select full, component or pattern context.
- [Skills](): install a reusable development workflow and query references with six lookup scripts.
- [AGENTS.md](): keep WebMusic guidance with your application's existing rules.
---
# llms.txt
> Download WebMusic documentation by task: a reference index, full documentation, components, or common patterns.
Canonical documentation: https://koperative-lab.github.io/WebMusic/agent-toolkit/llms-txt/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/kernel@0.1.0, @webmusic/ui@0.1.0, @webmusic/score@0.1.0, @webmusic/audio@0.1.0, @webmusic/bridge@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
Give your agent documentation that matches the work. Start with the
[llms.txt index]() to locate a reference, then read the linked Markdown
page. For a larger task or an agent that works with attached files, choose one
of the bundles below. All are generated from the same public sources as this site.
## Available files
| File | Use it for | Download |
| --- | --- | --- |
| [llms.txt]() | Locate documentation by development task, then follow selected references. | Download index |
| [llms-full.txt]() | Read the complete public documentation for the supported release, including API references. | Download full documentation |
| [llms-components.txt]() | Work with Web Components, Headless objects and UI presenters. | Download components |
| [llms-patterns.txt]() | Find common integration and composition examples for building an application. | Download patterns |
Each bundle identifies its included references. Components and patterns are
focused selections; follow their links when you need another API's complete
contract. The full bundle is useful for local search, but may exceed an agent's
context window. Prefer the index and individual pages for a narrow change.
## Use a URL
Ask the agent to read the index and fetch only the relevant pages:
```text
Read https://koperative-lab.github.io/WebMusic/llms.txt.
Find the references for a score player with synchronized staff and keyboard
views, then read those Markdown pages before implementing it in this app.
Check the installed package versions, exports and types. Keep one playback
source and handle loading, errors and teardown.
```
For a composition task, give it the patterns bundle directly:
```text
Use https://koperative-lab.github.io/WebMusic/llms-patterns.txt to choose a
WebMusic integration for this application's framework and UI. Follow the
linked component or API reference for the exact members and lifecycle.
```
Use `llms-components.txt` when selecting or customizing components. Use
`llms-full.txt` when the task spans several APIs and your agent can search a
large document. Providing a URL requires an agent that can fetch documentation;
it does not automatically configure every editor's context system.
## Download and use locally
Use the download links above, or save a chosen bundle from the command line:
```sh
curl -fL https://koperative-lab.github.io/WebMusic/llms-components.txt -o webmusic-components.txt
```
Attach the downloaded file to the conversation, or place it in a documentation
folder your coding agent can read. Tell the agent which file to use and what to
build. For example:
```text
Read the attached webmusic-components.txt. Add play, pause and seek controls
using the installed WebMusic version. Follow this application's conventions
and verify loading, error handling and cleanup.
```
The index contains links; downloading it alone does not download their contents.
For an offline task, save the relevant bundle or individual references too.
The [Quick Start in Markdown]() is a small starting
point for a player with following views.
Text preserves static code, tables and API explanations. Interactive demos stay
on the linked HTML pages, and downloading documentation does not download music
files, sound resources or npm packages.
## Match the installed version
The [context manifest]() identifies the release
baseline, package versions and documentation revision. Check it against the
packages in your application. Refresh saved files when adopting a new reference
revision; the manifest and files used together must come from the same build.
A newer reference does not establish that an older installed package exports
the same API. Use matching references and installed declarations; do not upgrade
a project just to match an example. Historical version archives are not provided.
## Related
- [Agent Toolkit overview]()
- [Reusable Skills and lookup scripts]()
- [Project instructions with AGENTS.md]()
---
# Skills
> Install the WebMusic application-development skill and use it in your own project.
Canonical documentation: https://koperative-lab.github.io/WebMusic/agent-toolkit/skills/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/kernel@0.1.0, @webmusic/ui@0.1.0, @webmusic/score@0.1.0, @webmusic/audio@0.1.0, @webmusic/bridge@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
The `webmusic` skill helps an agent develop applications with the published
packages. It includes an integration guide, playback and resource-lifetime
rules, troubleshooting references, and six
scripts for looking up documentation and implementation details.
## Install from source
Use a WebMusic source checkout that contains `skills/webmusic/SKILL.md`.
From your application directory, the [Skills CLI](https://github.com/vercel-labs/skills)
can install the local folder. Replace the example path with your checkout:
```sh
npx skills add /path/to/WebMusic/skills/webmusic --copy
```
Choose your agent and project installation in the CLI. This installs a copy of
the skill; it does not install WebMusic's npm packages or rewrite `AGENTS.md`.
Alternatively, copy the complete `skills/webmusic/` directory into the skill directory
supported by your agent. Keep its `scripts/` and `references/`
alongside `SKILL.md`; copying the entry file alone loses supporting guidance
and executable helpers.
Follow your agent's normal skill discovery or reload procedure, then select
`webmusic` for a task in your application. If the agent does not support skill
installation, ask it to read the copied `SKILL.md` and follow the relevant
references. Installing the npm packages alone does not install the skill.
The skill targets WebMusic 0.1.0. It reads the installed package's exports and
types before selecting an API. Detailed API lookup uses the public references
and may need network access; the bundled files are not a complete offline API
manual. To update, replace your installed skill folder with the complete folder
from the source revision you choose, then review its compatibility metadata.
Keep project-specific guidance in your application's instruction file.
## What the skill covers
The workflow begins with your application: its framework, package manager,
installed WebMusic versions, existing conventions and requested interaction.
It helps the agent:
1. Choose Web Components, Headless, or API + UI for the task.
2. Find the owning public reference and confirm the installed package's exports.
3. Identify required dependencies, music files, sound resources and browser requirements.
4. Share a player's data and time with following views and analysis where supported.
5. Handle loading, failure, cancellation and cleanup according to resource ownership.
6. Run application checks and exercise the requested interaction in the browser.
Detailed API members will remain in the public references. The skill guides
the agent to those owners instead of maintaining another API manual.
## Included files
```text
skills/webmusic/
├── SKILL.md
├── LICENSE
├── scripts/
│ ├── list_components.mjs
│ ├── get_component_docs.mjs
│ ├── get_source.mjs
│ ├── get_styles.mjs
│ ├── get_theme.mjs
│ ├── get_docs.mjs
│ └── _context.mjs # Shared lookup and validation helpers
├── references/ # Integration, lifecycle and script guidance
└── evals/ # Application-development evaluation scenarios
```
The Skill uses WebMusic's MIT license. Its scripts require Node.js 22.19 or
later and no extra npm dependencies. They print retrieved content to standard
output and do not edit your application.
## Query with the scripts
Run these commands from the installed `webmusic` Skill folder, or replace
`scripts/` with its full path. Start by listing components, then use the exact
identifier returned by the catalog:
```sh
node scripts/list_components.mjs
node scripts/get_component_docs.mjs element/score-player
```
| Script | Purpose | Example |
| --- | --- | --- |
| `list_components.mjs` | List Web Components, Headless objects and UI presenters with stable identifiers. | `node scripts/list_components.mjs --kind headless` |
| `get_component_docs.mjs` | Read a selected component's public reference. | `node scripts/get_component_docs.mjs headless/score-player` |
| `get_source.mjs` | Inspect selected implementation entry files from the verified release. | `node scripts/get_source.mjs element/score-player` |
| `get_styles.mjs` | Read a component's documented styling contract, such as tokens, parts and visual bindings. | `node scripts/get_styles.mjs ui/transport` |
| `get_theme.mjs` | Read shared theming guidance and its selected source definitions. | `node scripts/get_theme.mjs` |
| `get_docs.mjs` | Read a documentation page or one of the four bundles. | `node scripts/get_docs.mjs patterns` |
Source inspection returns selected owning files, not the complete dependency
tree. Use the package's exported entries when developing your application.
Styling output is reference text, not a generated stylesheet; Headless objects
have no visual styling contract. Use their UI presenter's reference when adding
custom visuals.
Every script provides help, for example `node scripts/get_docs.mjs --help`.
`list_components.mjs --json` provides structured
output for other tools. `get_docs.mjs` accepts `index`, `full`, `components`,
`patterns`, or a documentation route:
```sh
node scripts/get_docs.mjs /quick-start/
node scripts/get_docs.mjs components
```
By default the scripts read the public WebMusic documentation site. For a local
build, a preview deployment or a saved context snapshot, choose the source:
```sh
node scripts/get_docs.mjs patterns --base-url http://localhost:4321/WebMusic/
node scripts/get_component_docs.mjs element/score-player --context-dir /path/to/WebMusic/apps/doc/webmusic/dist
```
The `--base-url URL` option must point to the documentation root, including its
deployment prefix. `--context-dir PATH` points to the built site's root directory. Keep its
`agent-context/` directory and the four `llms*.txt` files from one build together;
a single downloaded bundle is sufficient to read or attach manually, but is
not a complete input for the scripts.
The scripts check the context's package compatibility and content hashes.
A missing reference, unsupported version or mixed snapshot produces an error
instead of supplying mismatched content. Match your application's installed
versions separately before using an example.
## Use the skill
This example asks for a custom interface using Headless behavior:
```text
Use the webmusic skill to build a custom score player, using this application's
framework and visual conventions. Add play, pause and seek controls, plus
loading and error feedback.
Read https://koperative-lab.github.io/WebMusic/score/headless/play/score-player/
and https://koperative-lab.github.io/WebMusic/score/api/io/.
Check the installed versions and public types before choosing imports. Keep
one authoritative player for this interaction and use each command's documented
time units. Start browser sound from a user action. Release owned players and
subscriptions when the interface unmounts, and handle a replaced or failed load.
Run the application's checks. Verify the controls and cleanup in the browser,
then summarize the changes and what remains unverified.
```
Add your file URL and the page or component to update. For a ready-made
interface, use the [Quick Start composition]() instead.
## Related
- [Agent Toolkit overview]()
- [Documentation context with llms.txt]()
- [Persistent project instructions]()
---
# @webmusic/audio/analyze
> Audio analysis — peaks, loudness, spectrogram, onsets, key, tempo, pitch and Meyda features, plus an incremental session, a Web Worker, and audio→score transcription.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/api/analyze/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`@webmusic/audio/analyze` is the **analysis** capability of the `@webmusic/audio` package. It reads decoded samples (the `Float32Array` channels of an `AudioClip` from [`@webmusic/audio`]()) and returns plain data — a multi-resolution peaks pyramid, loudness, an STFT spectrogram, onsets, musical key, tempo, a pitch track, and frame-level timbral features — and it adds an incremental analysis **session**, a **Web Worker** client, and an optional audio→score **transcriber**.
The APIs are headless and have no DOM dependency, so they run in Node, workers, browsers, tests, and batch jobs. Importing this entry never touches the DOM, audio globals, or any optional peer. The capability does not decode audio, draw anything, or play — those live in [`@webmusic/audio/play`]() and [`@webmusic/audio/view`](). For live level, spectrum or tuning interaction over an existing audio graph, use the separate [Analyze Web Components]().
## Install
`npm install @webmusic/audio` — use an available published version or the local workspace; [Install]() has the full setup.
The root model and this capability ship in that one package. The default engines are bundled and dependency-light: `fft.js` (spectrogram FFT), `music-tempo` (tempo), `pitchy` (pitch), and `meyda` (frame features). Everything else is an **optional peer** reached only through guarded dynamic `import()` — nothing extra is installed by default, and the build and typecheck pass without any of them:
```txt
essentia.js / aubiojs / realtime-bpm-analyzer advanced tempo engines
pitchfinder / aubiojs advanced pitch engines
webfft high-performance FFT backend
@spotify/basic-pitch + @tensorflow/tfjs default transcription engine
@magenta/music + @tensorflow/tfjs Onsets & Frames transcription
```
When an advanced engine's peer is missing the analyzer falls back to its bundled default with a `console.warn`; the AGPL/GPL-licensed peers are dynamic-import only and are never bundled.
The analysis capability has several entry points — the [package subpath table]() lists them with every other `@webmusic/audio` entry:
The `@webmusic/audio/analyze` root is the stateless API; `/element` is the opt-in styled Web Component entry. The Worker runtime remains isolated, so code-only imports stay free of DOM and worker-global side effects.
## Choose an analysis surface
Whole-clip key, tempo, loudness, chroma, summary and pitch-track results are
static data. Compute them here and render a custom report with application UI or
the [UI Kit analysis presenter](). A cached
[analysis session]() supports
repeated work after edits, and the [Worker client](#analysis-in-a-worker) moves
large offline jobs away from the UI thread. For a live signal with custom UI,
use the [realtime analyzer]().
The [Analyze Elements]() instead inspect a live signal:
sampled level dynamics, probeable FFT bins, or a sounding note against an A4
reference. They do not replace these report APIs. To show estimated beat
positions without a dedicated Analyze Element, convert a Headless session's
`tempo.grid.beats` to point regions for
[`.regions`]().
---
Every analyzer takes decoded samples and returns plain data. The single-channel analyzers (`computeSpectrogram`, `detectOnsets`, `trackPitch`, `extractFeatures`) take one `Float32Array`; the whole-clip analyzers (`computePeaks`, `measureLoudness`, `detectAudioKey`, `detectTempo`) take the channel array. The examples below assume a decoded `clip` from `@webmusic/audio/play`:
```ts
import {loadClipFromUrl} from '@webmusic/audio/play';
const clip = await loadClipFromUrl('/wav/Arabesque%20No.1.wav');
const channels = clip.channels()!; // Float32Array[] — null for a streaming-only clip
const mono = clip.channelData(0)!; // Float32Array of the first channel
```
## Peaks pyramid
`computePeaks` builds a multi-resolution min/max peaks pyramid in one pass: the finest level summarizes every `baseSamplesPerPeak` source samples into a min/max pair per channel, and each coarser level is a 2:1 reduction of the one below, so any zoom is an O(1) read. The result is quantized to 8-bit (`Int8Array`), structured-clone / transfer friendly, and laid out exactly as `@webmusic/audio`'s `readPeak` (and `renderWaveformVisualizer` in `@webmusic/audio/view`) expect.
```ts
import {computePeaks} from '@webmusic/audio/analyze';
const peaks = computePeaks(channels, clip.sampleRate, {baseSamplesPerPeak: 256});
// peaks.levels[0] is the finest level; peaks.levels[n] is the coarsest.
```
| Option | Default | Description |
| --- | --- | --- |
| `baseSamplesPerPeak` | `256` | Source samples summarized by one min/max pair at the finest level. Smaller = finer detail and larger output. |
| `minPeaksPerLevel` | `1` | Stop coarsening once a level has fewer than this many peaks. |
The returned [`AudioPeaks`]() carries `sampleRate`, `channels`, `baseSamplesPerPeak`, and the `levels` array.
## Loudness
`measureLoudness` returns RMS, a peak dBFS field named `truePeakDb`, and integrated loudness in LUFS. The current `truePeakDb` implementation uses the largest decoded sample, without oversampling, so it does **not** establish inter-sample true peak. Integrated loudness follows ITU-R BS.1770 / EBU R128: per-channel K-weighting (a two-stage IIR re-derived for the clip's sample rate), then mean-square gating over 400 ms blocks (100 ms hop) with an absolute gate at −70 LUFS and a relative gate 10 LU below the ungated mean.
```ts
import {measureLoudness} from '@webmusic/audio/analyze';
const {rms, truePeakDb, integratedLufs, momentary} = measureLoudness(channels, clip.sampleRate);
```
| Field | Description |
| --- | --- |
| `rms` | Linear root-mean-square amplitude over the whole clip, in `[0, 1]`. |
| `truePeakDb` | Largest decoded sample in dBFS (`0` dB = full scale; `-Infinity` for digital silence). The field name does not imply inter-sample true-peak measurement. |
| `integratedLufs` | Integrated loudness in LUFS (`-Infinity` for digital silence). |
| `momentary` | `Float32Array` momentary-loudness curve, one gated 400 ms value per 100 ms hop. |
## Spectrogram
`computeSpectrogram` runs a self-built STFT (framing → Hann window → hop → magnitude) over a pluggable `FFTBackend` for one channel. The default backend is `fft.js` (radix-4 real FFT); `webfft` can be slotted in behind the same interface. An optional triangular mel filterbank reduces the linear bins to `mel` mel bands.
```ts
import {computeSpectrogram} from '@webmusic/audio/analyze';
const spec = computeSpectrogram(mono, clip.sampleRate, {fftSize: 1024, hopSize: 256});
// magnitude of bin b in frame f:
const value = spec.magnitudes[f * spec.binsPerFrame + b];
```
| Option | Default | Description |
| --- | --- | --- |
| `fftSize` | `2048` | FFT window size (power of two recommended). |
| `hopSize` | `fftSize / 4` | Samples between successive frames. |
| `mel` | — | When set, project the linear bins onto this many mel bands. |
| `backend` | `fft.js` | Override the `FFTBackend` (its `size` must equal `fftSize`). |
The result is a [`SpectrogramData`](#shared-result-types): `times`, `frequencies`, the flat row-major `magnitudes`, and `binsPerFrame`. It is exactly the structural shape `renderSpectrogramVisualizer` in `@webmusic/audio/view` consumes.
### Custom FFT backend
`FFTBackend` is a one-method interface (`forward(frame)` returns the magnitude spectrum of length `size / 2 + 1`). `createFftJsBackend(size)` builds the default; pass your own to swap in `webfft` or a WASM kernel.
```ts
import {computeSpectrogram, createFftJsBackend, type FFTBackend} from '@webmusic/audio/analyze';
const backend: FFTBackend = createFftJsBackend(2048);
const spec = computeSpectrogram(mono, clip.sampleRate, {fftSize: 2048, backend});
```
## Onsets
`detectOnsets` does spectral-flux onset detection with an adaptive threshold over a single channel: it reuses the STFT, sums the half-wave-rectified bin-to-bin magnitude increases per frame, then peak-picks above a local-mean threshold (with an energy-relative floor that suppresses the spectral-leakage "onsets" a sustained tone would otherwise trigger). It returns ascending onset times in seconds.
```ts
import {detectOnsets} from '@webmusic/audio/analyze';
const onsets = detectOnsets(mono, clip.sampleRate, {thresholdFactor: 1.5});
```
| Option | Default | Description |
| --- | --- | --- |
| `fftSize` | `1024` | STFT window size. |
| `hopSize` | `fftSize / 2` | Samples between frames. |
| `windowFrames` | `6` | Sliding-window radius (frames) for the adaptive threshold. |
| `thresholdFactor` | `1.5` | Multiplier on the local mean flux. |
| `delta` | auto | Absolute floor added to the local mean (auto-scaled from peak flux). |
| `minIntervalSeconds` | `0.03` | Minimum gap between onsets. |
For an offline onset-spacing report, `histogramOnsetIntervals` groups positive
gaps between consecutive onset times. This is report data, not an Analyze
Element or a playback cursor:
```ts
import {detectOnsets, histogramOnsetIntervals} from '@webmusic/audio/analyze';
const onsets = detectOnsets(mono, clip.sampleRate);
const bins = histogramOnsetIntervals(onsets);
// Each OnsetIntervalBin has startSeconds, optional endSeconds and count.
```
`bucketSeconds` defaults to `0.05` seconds. Gaps from `overflowSeconds`
(default `2` seconds) go into the final open-ended bin, whose
`endSeconds` is absent. Non-finite or non-increasing adjacent gaps are skipped;
invalid bucket or overflow options throw `RangeError`. Use
`OnsetIntervalBin[]` in application-owned charts or tables.
## Key
`detectAudioKey` estimates the musical key: it folds every spectral bin onto its pitch class (a 12-bin chromagram, magnitude-weighted across frames) and correlates that histogram against all 24 rotated Krumhansl–Schmuckler major/minor key profiles. Channels are mixed to mono internally. The two building blocks are exported too — `computeChromagram` (channels → 12-bin `Float64Array`) and `keyFromChroma` (chroma → ranked result).
```ts
import {detectAudioKey, computeChromagram, keyFromChroma} from '@webmusic/audio/analyze';
const {tonic, mode, confidence, scores} = detectAudioKey(channels, clip.sampleRate);
// Or in two steps:
const chroma = computeChromagram(channels, clip.sampleRate, {minFreq: 55, maxFreq: 5000});
const key = keyFromChroma(chroma);
```
| Option | Default | Description |
| --- | --- | --- |
| `fftSize` | `4096` | STFT window size. |
| `hopSize` | `fftSize / 2` | Samples between frames. |
| `minFreq` | `55` (A1) | Lowest frequency folded into the chromagram. |
| `maxFreq` | `5000` | Highest frequency folded in (capped at Nyquist). |
The [`AudioKeyResult`](#shared-result-types) carries `tonic`, `mode`, a `confidence` in `[0, 1]` (the gap between the best and runner-up candidate), and the full ranked `scores`.
## Tempo
`detectTempo` estimates BPM and a beat grid, returning a Promise. The default engine is `music-tempo` (offline); on silence or a signal with no detectable onsets it falls back to a synthetic 120-BPM grid so the call never rejects. The advanced engines dynamically import their optional peer and fall back to the default on a missing peer.
```ts
import {detectTempo} from '@webmusic/audio/analyze';
const {bpm, confidence, grid} = await detectTempo(channels, clip.sampleRate);
// grid is a @webmusic/audio BeatGrid → anchors looping / quantization.
```
| Option | Default | Description |
| --- | --- | --- |
| `engine` | `'music-tempo'` | One of `TempoEngine`: `'music-tempo'`, `'essentia'`, `'aubio'`, `'realtime'`. |
| `sampleRate` | — | Reserved option on `TempoOptions`. |
`detectTempo` accepts either a channel array or a single mono `Float32Array`. The `'realtime'` engine (`realtime-bpm-analyzer`) is an AudioWorklet-based online analyzer with no offline buffer API, so it gracefully falls back to the offline default. The [`TempoResult`](#shared-result-types) carries `bpm`, a `confidence` in `[0, 1]` derived from beat-interval regularity, and the `grid`.
## Pitch
`trackPitch` does per-frame monophonic fundamental-frequency tracking over a single channel, returning a Promise. The default engine is `pitchy` (McLeod Pitch Method, pure TS); `'pitchfinder'` and `'aubio'` are optional peers with graceful fallback. Frames below the clarity threshold report a frequency of `0`.
```ts
import {trackPitch} from '@webmusic/audio/analyze';
const {times, frequencies, confidences} = await trackPitch(mono, clip.sampleRate, {
clarityThreshold: 0.6,
});
// frequencies[f] is 0 for unvoiced / sub-threshold frames.
```
| Option | Default | Description |
| --- | --- | --- |
| `engine` | `'pitchy'` | One of `PitchEngine`: `'pitchy'`, `'pitchfinder'`, `'aubio'`. |
| `frameSize` | `2048` | Analysis window length in samples. |
| `hopSize` | `frameSize / 4` | Samples between frames. |
| `clarityThreshold` | `0.6` | Discard detections below this clarity (`0..1`). |
The [`PitchTrackResult`](#shared-result-types) carries `times`, `frequencies`, and `confidences`, all `Float32Array`s of one value per frame.
## Frame features
`extractFeatures` computes offline frame-level audio features via Meyda over a single channel. Framing and hopping are done here (Meyda has no offline streaming); scalar features (e.g. `rms`, `spectralCentroid`) collect into one `Float32Array` per feature, while vector features (e.g. `mfcc`, `chroma`) collect into an array of per-frame `Float32Array`s. `DEFAULT_FEATURES` is a compact timbral fingerprint.
```ts
import {extractFeatures, DEFAULT_FEATURES} from '@webmusic/audio/analyze';
const f = extractFeatures(mono, clip.sampleRate, ['rms', 'mfcc'], {bufferSize: 512});
f.features.rms; // Float32Array — one value per frame
f.features.mfcc; // Float32Array[] — one MFCC vector per frame
DEFAULT_FEATURES; // ['rms', 'spectralCentroid', 'spectralRolloff', 'spectralFlatness', 'zcr', 'mfcc']
```
| Option | Default | Description |
| --- | --- | --- |
| `bufferSize` | `512` | Analysis window length in samples (power of two). |
| `hopSize` | `bufferSize` | Samples between frames (default: no overlap). |
The [`AudioFeatureFrames`](#shared-result-types) carries `times`, the resolved `featureNames`, and the `features` record.
## Clip summary
`summarizeClip` is the cheap, headless overview of an `AudioClip` — duration, sample rate, channel count, length, and file-tag title/artist read straight off the clip. Integrated LUFS is included only when the clip actually carries decoded samples (a streaming-only clip has none).
```ts
import {summarizeClip} from '@webmusic/audio/analyze';
const {durationSeconds, sampleRate, channels, integratedLufs} = summarizeClip(clip);
```
`ClipSummary` fields: `durationSeconds`, `sampleRate`, `channels`, `length`, optional `title` / `artist`, and optional `integratedLufs`.
## The session and the realtime analyzer
`createAudioAnalysisSession` and `createRealtimeAnalyzer` live on
`@webmusic/audio/analyze/headless` and are documented under
[Headless › Analyze](). Everything on this page is
one-shot: hand it a clip, get the numbers.
## Analysis in a Worker
Even incrementally, the session runs on the calling thread. To move peaks / loudness / STFT / tempo off the main thread, `createAnalysisWorker` spawns the bundled module worker (`@webmusic/audio/analyze/worker`), which hosts a worker-resident session and exposes the same results behind a Promise API. The worker is **stateful**: `analyze` first makes defensive channel copies, then transfers those copies without another copy. Later `update` calls re-analyze by `clipId` from a serializable list of edits — the big sample arrays do not cross the boundary again. `release` drops a clip's cache.
```ts
import {createAnalysisWorker} from '@webmusic/audio/analyze';
const client = createAnalysisWorker(); // spawns @webmusic/audio/analyze/worker as a module worker
const result = await client.analyze(clip, {tasks: ['peaks', 'loudness', 'onsets']});
const updated = await client.update(clip, [
{op: 'gain', factor: 0.5, startSeconds: 1, endSeconds: 2},
]); // incremental, off-thread, replayed in the worker
client.release(clip); // drop the worker-side cache for this clip
client.dispose(); // terminate the worker; in-flight requests reject
```
- **Register once, analyze by id.** `analyze` registers defensive copies of the clip's channels the first time it sees a `clipId`; later `update` calls ship only the `ClipEditDescriptor[]` and the worker replays them onto the cached samples, so the session's incremental caches still apply.
- **Fallback.** When `Worker` is unavailable (Node, SSR) — or constructing one throws — the client transparently runs an in-process session behind the same Promise API.
- **Custom bundling.** Pass your own Worker or factory — `createAnalysisWorker(worker)` or `createAnalysisWorker(() => worker)` — if the default `new Worker(new URL('./worker.js', import.meta.url), {type: 'module'})` does not suit your bundler.
The wire protocol is exported for advanced use: `handleAnalyzeRequest(state, message)` is the pure, worker-global-free message handler (paired with `createAnalysisWorkerState()`), and the request/response shapes (`AnalyzeWorkerRequest`, `AnalyzeWorkerResponse`, and the per-message `RegisterRequest` / `AnalyzeRequest` / `UpdateRequest` / `ReleaseRequest`) describe the `@webmusic/audio/analyze/worker` boundary. `createRequestTracker()` is the pure id-correlation helper the client uses to match responses to pending promises.
## Audio → Score transcription
`transcribe` turns audio into **family-neutral note events** (audio → MIDI-style notes). It lives at its **own subpath** so consumers who never call it pay nothing, and all of its heavy dependencies are **optional peers** reached only through guarded dynamic `import()` — the build and typecheck pass with none installed, and calling it without them throws a clear, actionable error. Assembling the notes into an `@webmusic/score` `Score` is deliberately not this package's job — that cross-family step belongs to `@webmusic/bridge`.
```ts
import {transcribe} from '@webmusic/audio/analyze/transcribe';
import {scoreFromTranscription} from '@webmusic/bridge';
const {notes, confidence, bpm} = await transcribe(clip, {quantize: true});
const score = scoreFromTranscription(notes, {bpm});
```
The default engine is `'basic-pitch'` (`@spotify/basic-pitch` + `@tensorflow/tfjs`, a CNN audio→MIDI model resampled to 22.05 kHz mono); `'magenta-oaf'` selects `@magenta/music`'s Onsets & Frames (narrow: solo piano). When `quantize` is on, note times snap to a sixteenth-note grid of the clip's `beatGrid` (or one you supply).
| Option | Default | Description |
| --- | --- | --- |
| `engine` | `'basic-pitch'` | `'basic-pitch'` or `'magenta-oaf'`. |
| `onsetThreshold` | `0.5` | Note-onset probability threshold in `[0, 1]`. |
| `frameThreshold` | `0.3` | Frame (sustain) probability threshold in `[0, 1]`. |
| `minNoteLength` | `0.058` | Drop notes shorter than this (seconds). |
| `quantize` | `true` | Snap note times to a beat grid. |
| `beatGrid` | `clip.beatGrid` | The `BeatGrid` to quantize against. |
The `TranscriptionResult` carries `notes` — an array of `TranscribedNote` (`startSeconds`, `endSeconds`, `midi`, `velocity`) — a `confidence` in `[0, 1]` (mean of note amplitudes), and `bpm` (the supplied beat-grid tempo when present, otherwise the clip beat-grid tempo, or `120` when neither exists). It does not contain a `Score`; pass `notes` and `bpm` to `scoreFromTranscription` from `@webmusic/bridge` when you need one.
Install the optional engine peers to use transcription, and add the bridge when you want to assemble a `Score`:
```bash
npm i @spotify/basic-pitch @tensorflow/tfjs @webmusic/bridge
```
## Shared result types
The result shapes are exported as types for typing your own pipelines (re-exported from `./types`):
| Type | Description |
| --- | --- |
| `AudioAnalysisResult` | The full session result: `peaks`, `loudness`, and the optional `tempo` / `key` / `onsets` / `pitchTrack` / `spectrogram` / `features`. |
| `AudioAnalysisTask` | A task name accepted by the session (`'peaks' \| 'loudness' \| 'tempo' \| 'key' \| 'onsets' \| 'pitch' \| 'spectrogram' \| 'features'`). |
| `LoudnessResult` | `rms`, `truePeakDb`, `integratedLufs`, optional `momentary`. |
| `TempoResult` | `bpm`, `confidence`, `grid` (a core `BeatGrid`). |
| `AudioKeyResult` | `tonic`, `mode`, `confidence`, optional ranked `scores`. |
| `PitchTrackResult` | `times`, `frequencies`, `confidences` (`Float32Array` each). |
| `SpectrogramData` | `times`, `frequencies`, `magnitudes`, `binsPerFrame`. |
| `AudioFeatureFrames` | `times`, `featureNames`, and the `features` record. |
## API Reference — `@webmusic/audio/analyze`
This table names exports of the capability root. The two stateful factories
are values from [`/analyze/headless`](), while
`transcribe` and its result types are exclusive to
[`/analyze/transcribe`](#audio--score-transcription).
| API | Kind | Use it for |
| --- | --- | --- |
| `computePeaks(channels, sampleRate, opts?)` | Peaks | Build a multi-resolution min/max peaks pyramid for waveform rendering. |
| `measureLoudness(channels, sampleRate)` | Loudness | RMS, true-peak (dBFS), integrated LUFS, and the momentary curve. |
| `computeSpectrogram(channel, sampleRate, opts?)` | Spectrogram | STFT magnitude spectrogram (optionally mel-projected). |
| `createFftJsBackend(size)` | Spectrogram | The default `fft.js` `FFTBackend`; swap for `webfft` or your own. |
| `detectOnsets(channel, sampleRate, opts?)` | Onsets | Spectral-flux onset times in seconds. |
| `histogramOnsetIntervals(onsets, opts?)` | Onsets | Static spacing distribution, with 50 ms bins and a 2 s overflow by default. |
| `detectAudioKey(channels, sampleRate, opts?)` | Key | Krumhansl–Schmuckler key, mode, confidence, and candidates. |
| `computeChromagram(channels, sampleRate, opts?)` | Key | The 12-bin pitch-class chromagram (`Float64Array`). |
| `keyFromChroma(chroma)` | Key | Rank the 24 key profiles against a chroma histogram. |
| `detectTempo(input, sampleRate, opts?)` | Tempo | Async BPM + `BeatGrid` (default `music-tempo`; optional engines). |
| `trackPitch(channel, sampleRate, opts?)` | Pitch | Async per-frame monophonic f0 track (default `pitchy`). |
| `extractFeatures(channel, sampleRate, names?, opts?)` | Features | Per-frame Meyda features (scalar + vector). |
| `DEFAULT_FEATURES` | Features | The default Meyda feature set. |
| `summarizeClip(clip)` | Summary | Duration, sample rate, channels, tags, and integrated LUFS. |
| `createAnalysisWorker(workerOrFactory?)` | Worker | Off-main-thread analysis behind `analyze` / `update` promises, with an in-process fallback. |
| `configureAnalysisWorkerScriptUrl(scriptUrl?)`, `resolveAnalysisWorkerUrl(moduleUrl?, classicScriptUrl?)` | Worker | Resolve or configure the shipped worker URL for classic-script hosting; see [Analysis in a Worker](#analysis-in-a-worker). |
| `createRequestTracker()` | Worker | Pure request-id correlation helper used by the client. |
| `createAnalysisWorkerState()`, `handleAnalyzeRequest(state, msg)` | Worker | The worker's mutable state and its pure, testable message handler. |
| `ComputePeaksOptions`, `SpectrogramOptions`, `FFTBackend`, `OnsetOptions`, `OnsetIntervalHistogramOptions`, `OnsetIntervalBin`, `KeyOptions`, `TempoOptions`, `TempoEngine`, `PitchOptions`, `PitchEngine`, `ExtractFeaturesOptions` | Type | Analyzer option bags, interval bins and engine unions. |
| `RealtimeAnalyzer`, `RealtimeAnalyzerOptions`, `RealtimeFrame`, `RealtimeSource` | Type | The realtime analyzer handle, options, frame, and source. |
| `AudioAnalysisSession`, `AudioAnalysisSessionOptions` | Type | The incremental session interface and its options. |
| `AnalysisWorkerClient`, `AnalysisWorkerLike`, `AnalysisWorkerFactory`, `AnalyzeCallOptions`, `RequestTracker` | Type | The worker client and the worker/factory shapes it accepts. |
| `AnalyzeWorkerRequest`, `AnalyzeWorkerResponse`, `RegisterRequest`, `AnalyzeRequest`, `UpdateRequest`, `ReleaseRequest`, `AudioWorkerState` | Type | The `@webmusic/audio/analyze/worker` message protocol. |
| `AudioAnalysisResult`, `AudioAnalysisTask`, `LoudnessResult`, `TempoResult`, `AudioKeyResult`, `PitchTrackResult`, `SpectrogramData`, `AudioFeatureFrames`, `ClipSummary` | Type | Shared analysis result and summary shapes. |
The [`@webmusic/audio/analyze/headless`]() entry owns
`createAudioAnalysisSession` and `createRealtimeAnalyzer`.
`@webmusic/audio/analyze/transcribe` exports `transcribe`, `TranscribeOptions`,
`TranscribedNote` and `TranscriptionResult`; its options and conversion boundary
are explained [above](#audio--score-transcription).
---
# @webmusic/audio
> The thin, DOM-free Audio root — immutable AudioClip model, regions, beat grid, peaks types, a typed event emitter, and JSON serialization.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/api/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
The `@webmusic/audio` root is the **foundation** of Audio: the immutable `AudioClip` model, `Region` markers, the `BeatGrid` time map, the `ClipEditSession` editing transaction, the `AudioPeaks` waveform pyramid type, a typed `EventEmitter`, and JSON serialization. This root entry touches no browser APIs (no `AudioContext`, no `AudioBuffer`), so it is safe in Node, workers, and SSR.
It is intentionally thin. Functional APIs live in capability subpaths of the same package:
- **Decode, loading, playback, mixing, recording, effects, and audio export** live in [`@webmusic/audio/play`]().
- **Analysis** (peak computation, tempo/beat detection, …) lives in `@webmusic/audio/analyze`.
- **Visualization** lives in `@webmusic/audio/view`; its Web Components and those from [`@webmusic/audio/play`]() live under each capability's `/element` subpath.
Install `@webmusic/audio` once, then import the root model or a capability entry such as `@webmusic/audio/play` as needed.
## Package subpaths
Every published entry of `@webmusic/audio`, grouped by capability. A
capability root is its API; `/headless` is code-only domain objects;
`/element` is side-effect-free Web Component classes and `/auto` registers
them; `/global` is the browser IIFE. This table is the package map — the
capability pages below document what is inside each entry.
| Entry | Form | What is there |
| --- | --- | --- |
| `@webmusic/audio` | package root | `AudioClip`, `Region`, `BeatGrid`, `ClipEditSession`, `AudioPeaks`, events, JSON — this page. |
| `@webmusic/audio/play` | [capability root]() | Decoding and loading, playback engines, mixing, recording, effects, WAV export. |
| `@webmusic/audio/play/headless` | [Headless]() | `AudioClipPlayer`, `AudioMixer`, `AudioRecorder`, the playlist controller, `Effect`. |
| `@webmusic/audio/play/element` | [Web Components]() | ``, ``, ``, ``. |
| `@webmusic/audio/play/auto` | registration | Imports the play elements and registers them on evaluation. |
| `@webmusic/audio/play/global` | browser IIFE | `
```
The demo pairs one player with this tool. Start playback for live samples;
the tool's Freeze, Reset hold and Peak threshold controls operate on its own
display. Copy includes both elements, and Reset restores their demo settings.
## Signal and lifetime
The element borrows the bound player's `AnalyserNode` while that player is
playing. Assign `.analyser` to inspect a caller-owned graph instead; an
explicit analyser takes precedence and is sampled while the element remains
connected. The element does not create an `AudioContext`, decode a clip, seek,
or own the borrowed node. It samples about every 50 ms, so the RMS and peak
describe sampled windows, not continuous loudness or inter-sample true peak.
The crest value is sample peak minus RMS in dB.
Freeze holds the current display while sampling continues. Reset hold starts
the held peak again from the current sample. A pause, seek, source change or
missing live input clears stale samples; a frame stall marks the signal
unavailable. The element stops its own polling and subscriptions on rebind or
disconnect without stopping the player. `player` must resolve uniquely in
the current Document or ShadowRoot; invalid or ambiguous selectors remain
unbound, and later target changes are observed.
API
| Member | Kind | Behavior |
| --- | --- | --- |
| `player` | attribute | Unique selector for the playback owner whose analyser is borrowed while playing. |
| `threshold-dbfs` | attribute | Sample-peak comparison threshold in dBFS, clamped to `-60..0`; default `-12`. Invalid values use `-12`. |
| `.analyser` | property | Caller-owned `AnalyserNode`; takes precedence over the player's analyser. |
| `.thresholdDbfs` | property | Reflected peak threshold in dBFS. |
| `.sample` | read-only property | Latest `{rmsDbfs, peakDbfs}` sample, or `undefined` without a live sample. |
| `.heldPeakDbfs` | read-only property | Greatest sampled peak since the latest reset, or `undefined` before a sample. |
| `.frozen` | read-only property | Whether the display is frozen; the tool's Freeze control changes it. |
| `webaudio:levelchange` | event | `{rmsDbfs, peakDbfs, crestDb, heldPeakDbfs, thresholdDbfs}` for an accepted sample; bubbles and is composed. |
| `webaudio:error` | event | `{error: Error}` for a live-input or frame failure; bubbles and is composed. |
The threshold is visual feedback against the sampled peak; crossing it does
not mute, compress or otherwise change the signal.
Styling
The full-width host honors native `hidden`. Its outer surface uses
`--wm-component-background`, `--wm-component-border`,
`--wm-component-padding` and `--wm-component-radius`; the
`--wm-audio-level-analyzer-surface-*` tokens override this tag. The
[level presenter]() exposes `--wm-level-analyzer-rms`,
`--wm-level-analyzer-peak` and `--wm-level-analyzer-threshold` for its graph.
---
#
> Monitor normalized RMS level or FFT bars from a player, borrowed analyser or transparent audio tap.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-meter/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`` is the compact, always-visible Analyze monitor for a live
signal. Use it to watch normalized level or spectrum activity at a glance
beside an [``](). For sampled
dBFS history, a peak threshold and freeze, choose
[``]();
for frequency probing, choose
[``]().
The meter is not a LUFS or inter-sample true-peak measurement.
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-meter/)
## Import
```html
```
The demo pairs one real player with this meter. Its Parameters switch level and
spectrum display modes and tune the monitor. Copy includes both Elements;
Reset restores the demo settings. `@webmusic/audio/analyze/auto` also registers
the tag. Existing `view/element` and `view/auto` imports remain compatibility
paths to the same implementation and must not register a second meter.
## Signal and ownership
The `player` selector must match one element in the meter's Document or
ShadowRoot. The meter borrows the player's current analyser, follows late
upgrade and source replacement, and never owns its transport. `.player` can
instead borrow a nonvisual player directly and takes precedence over the
selector. A missing or ambiguous selector leaves the meter empty; invalid
selector syntax emits `webaudio:error`.
For another live graph, assign `.analyser` to borrow an `AnalyserNode`, or
assign `.context` to create an owned transparent input -> analyser -> output
tap. The caller supplies that context and connects the tap into its graph;
the meter does not create an `AudioContext`. Direct `.analyser` or `.context`
input takes precedence over player binding until cleared. Assigning a
nonempty `.player` clears direct input. Disconnect or rebind releases the
presenter, subscriptions and only owned tap nodes; it never closes or
disconnects the borrowed player, analyser or context.
```ts
// For a caller-owned graph that should pass through the meter:
import type {AudioMeterElement} from '@webmusic/audio/analyze/element';
const context = new AudioContext();
const meter = document.querySelector('audio-meter') as AudioMeterElement | null;
if (!meter) throw new Error('Audio meter is missing');
meter.context = context;
const input = meter.input;
const output = meter.output;
if (!input || !output) throw new Error('Audio meter tap is unavailable');
const sourceNode: AudioNode = context.createGain(); // replace with your source
sourceNode.connect(input);
output.connect(context.destination);
```
The level fill is scaled RMS. Its held marker retains scaled RMS with an
absolute decrement **per read**, not elapsed-time decay or instantaneous
sample-peak hold. Spectrum bars aggregate the borrowed analyser's current FFT
bins. The displayed values are normalized, not dBFS, LUFS or calibrated SPL.
The presenter's frame loop reads while connected; changing a source builds a
replacement before releasing the prior tap, and failed replacement preserves
the working route.
API
| Member | Kind | Behavior |
| --- | --- | --- |
| `player` | attribute | Unique selector for one player Element in the current root; default absent. Late insertion, upgrade, replacement and removal are observed. |
| `mode` | attribute | `level` (default) or `spectrum`; other values use level. Changing it remounts the presenter, not the audio graph. |
| `bars` | attribute | Spectrum bar count, minimum `4`, default `28`; changing it remounts the presenter. |
| `aria-label` | attribute | Accessible meter name; updates without remounting the presenter. |
| `fft-size` | attribute | Owned tap analyser FFT size, power of two from `32` to `32768`; default `1024`. Does not retune a borrowed analyser. |
| `smoothing-time-constant` | attribute | Owned tap analyser smoothing in `0..1`; default `0.8`. Does not retune a borrowed analyser. |
| `level-scale` | attribute | Non-negative multiplier for displayed RMS/peak; default `1.8`. |
| `peak-decay` | attribute | Non-negative absolute decrement of the held scaled-RMS marker per read; default `0.012`. |
| `.player` | property | Borrow an `AudioMeterPlayer`; takes precedence over selector binding. Clearing it returns to the selector. |
| `.analyser` | property | Borrow an existing `AnalyserNode` without connecting or disconnecting it; the getter exposes the current analyser, if any. |
| `.context` | property | Caller-supplied `BaseAudioContext` used to build an owned transparent tap. |
| `.input`, `.output` | read-only properties | Stable tap splice points when using `.context`; absent for a borrowed analyser. |
| `webaudio:error` | event | The thrown error value is passed directly as `detail` for tuning, read or cleanup failures; bubbles and is composed. |
Removing a tuning attribute restores its default. Invalid live tuning reports
an error and keeps the previous working graph and settings. The controller's
input/output ports remain stable during valid tuning changes. Subclasses may
override the protected `createMeterController`, `createMeterBinding`,
`mountMeterUI` and `onMeterError` hooks; they do not change the tag's resource
ownership by default.
The presenter's level and spectrum surfaces use an accessible name supplied by
`aria-label`. This meter has no built-in
transport or parameter gestures. The surrounding application controls its
mode and tuning attributes; its readout itself does not seek or alter audio.
Styling
The host has an open shadow root. Its outer surface uses
`--wm-audio-meter-surface-background`, `--wm-audio-meter-surface-border`,
`--wm-audio-meter-surface-padding` and `--wm-audio-meter-surface-radius`, then
the corresponding `--wm-meter-surface-*` and shared `--wm-component-*`
fallbacks. The neutral [meter presenter]() uses
public `--wm-meter-*` colors and dimensions. Legacy `--wameter-bg`,
`--wameter-track`, `--wameter-fill`, `--wameter-peak` and `--wameter-height`
aliases remain effective. Its root exposes `part="wrap"`; the presenter's
other documented parts remain available inside the shadow root.
```css
audio-meter {
--wm-meter-fill: #258a80;
--wm-meter-peak: #cb6541;
--wm-meter-height: 3rem;
}
```
---
#
> Inspect a trigger-aligned live waveform window with timebase, freeze and time-amplitude probing.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-oscilloscope/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
The oscilloscope inspects the most recent time-domain window from a live
signal. Adjust the trigger edge and level, narrow the timebase, freeze a frame,
and probe its time and linear sample amplitude. Unlike the scrolling
[``](), it is a short-window
inspection tool, not a history or transport timeline.
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-oscilloscope/)
## Import
```html
```
The demo pairs one player with the scope. Start playback for live samples;
the tool's trigger, timebase, Freeze and pointer/keyboard probe work on its
current frame. Copy includes both Elements, and Reset restores the demo
settings.
## Signal and lifetime
The scope borrows the bound player's `AnalyserNode` while the player is
playing. A caller can assign `.analyser` for another live graph; that explicit
node takes precedence and is sampled while connected. The `player` selector
must resolve uniquely in the current Document or ShadowRoot, including after
late insertion or upgrade. Invalid or ambiguous targets leave it unbound.
The scope never starts playback, creates an `AudioContext`, or retunes a
borrowed node's FFT settings.
About every 50 ms it copies one `getFloatTimeDomainData()` window. Timebase is
clamped to the samples available from that analyser; a trigger chooses a
matching threshold crossing in that bounded window, or the latest available
window when no edge matches. The displayed time axis starts at the selected
window's left edge, **not** at the player's clip position. Freeze stops this
tool's polling and holds a copied frame. When following a player, pause, seek
and source replacement clear stale readings; replacing an explicit analyser,
document hiding and graph loss also clear them. A stopped frame source reports
unavailable and is retried. Disconnect stops only this tool's poller, observers
and presenter, never the borrowed player or graph.
API
| Member | Kind | Behavior |
| --- | --- | --- |
| `player` | attribute | Unique selector for a playback owner whose analyser is borrowed while playing; default absent. |
| `frozen` | boolean attribute | Stop this tool's polling and retain its current display; default absent. Transport/source changes release the freeze. |
| `timebase-ms` | attribute | Requested visible window in milliseconds, default `10`; invalid or non-positive values use `10`. The effective value is clamped to the analyser's sampled window. |
| `trigger-level` | attribute | Threshold in linear sample amplitude, clamped to `-1..1`; default `0`. Invalid values use `0`. |
| `trigger-edge` | attribute | `rising` (default), `falling` or `off`; other values use rising. `off` displays the latest untriggered window. |
| `aria-label` | attribute | Accessible group name; default `Oscilloscope`. |
| `.analyser` | property | Explicit caller-owned `AnalyserNode`, preferred over the bound player's node; getter exposes the current node. |
| `.frozen`, `.timebaseMs`, `.triggerLevel`, `.triggerEdge` | properties | Reflected display controls with the parsing and bounds above. |
| `.trace` | read-only property | Latest `{samples, timebaseMs, triggered, silent}` copied display window, or `undefined` without a frame. `silent` distinguishes a sampled quiet window from missing input. |
| `.selectedTimeMs` | read-only property | Probe position in milliseconds from the left edge of the displayed window. |
| `webaudio:oscilloscopeprobe` | event | `{timeMs, amplitude?}` after a pointer or keyboard probe; time is relative to the displayed window. Bubbles and is composed. |
| `webaudio:error` | event | `{error: Error}` for input, frame or presenter failures; bubbles and is composed. |
The trigger is visual alignment within sampled data, not sample-accurate
synchronization with playback or an external signal. The probe reports a
nearest displayed sample when one exists; its amplitude is omitted while the
trace is unavailable.
**Accessibility.** The canvas is a focusable slider named "Inspect waveform
time", with its selected time and amplitude in `aria-valuetext`. Arrow keys
move the probe by one percent of the visible window, Shift+Arrow by ten
percent, and Home/End select its ends. Native range controls set timebase and
trigger level; the edge select and Freeze button remain keyboard-operable.
The current probe and status are also text outside the canvas.
Styling
The full-width host honors native `hidden` and uses the shared
`--wm-component-*` outer-surface tokens, with
`--wm-audio-oscilloscope-surface-*` overrides. The
[oscilloscope presenter]() exposes
`--wm-oscilloscope-height`, foreground, muted, border, control-background and
selection tokens. Its public parts are `root surface`, `header`, `readout`,
`plot`, `controls`, `freeze`, `timebase`, `trigger-level`, `trigger-edge` and
`status`. The canvas trace and axis colors are currently fixed drawing
colors; changing CSS background tokens alone does not recolor those pixels.
---
#
> Probe a live log-frequency FFT spectrum, freeze a frame and hold peaks.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-spectrum-analyzer/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
The spectrum analyzer is an inspectable live frequency plot beside one
[``](). Hover or use the
keyboard to probe a frequency, freeze the visual frame, or compare held
per-bin peaks. For a
time-axis spectrogram of a decoded clip, use
[`computeSpectrogram`]() with
[``]().
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-spectrum-analyzer/)
## Import
```html
```
The demo pairs one player with the plot. Start playback for live frames;
Freeze, Peak hold, Reset peaks and the focusable frequency probe act on the
real tool. Copy includes both elements, and Reset restores their demo settings.
The probe accepts pointer movement, Arrow keys, Home/End for range ends, and
Shift+Arrow for octave steps.
## Signal and lifetime
The element borrows the bound player's `AnalyserNode` while playing, or uses
a caller-supplied `.analyser` in preference to it. It samples about every
50 ms and maps that node's existing FFT bins onto a logarithmic frequency
axis. The display ranges do not change the borrowed node's `fftSize`,
`smoothingTimeConstant` or other FFT settings. Its values are
`getByteFrequencyData()` bins converted through the borrowed node's current
`minDecibels`/`maxDecibels` range; they are not calibrated sound-pressure
levels.
Freezing stops frame polling and keeps the inspected frame until unfreeze
or a transport/source change. Peak hold retains the largest displayed bin
values until Reset peaks or a source change. Pause, seek, source replacement
and missing input clear the current frame; a stalled frame source is reported as
unavailable. Disconnect stops only this element's polling and subscriptions,
not the player or its graph. `player` must resolve uniquely in the current
Document or ShadowRoot, including when a target appears later.
API
| Member | Kind | Behavior |
| --- | --- | --- |
| `player` | attribute | Unique selector for the playback owner whose analyser is borrowed while playing. |
| `frozen` | boolean attribute | Hold the current frame and stop this tool's polling; default absent. |
| `peak-hold` | boolean attribute | Show held spectrum peaks alongside the live bins; default absent. |
| `min-frequency` | attribute | Lower displayed frequency in Hz; default `20`. Invalid or below-1 values use `20`. |
| `max-frequency` | attribute | Upper displayed frequency in Hz; default `20000`. Invalid or at-most-1 values use `20000`. Display is limited by the input's Nyquist frequency. |
| `aria-label` | attribute | Accessible group name; default `Spectrum analyzer`. |
| `.analyser` | property | Explicit caller-owned `AnalyserNode` takes precedence over the player input. |
| `.frozen`, `.peakHold` | properties | Reflected booleans for frame freeze and peak hold. |
| `.minFrequency`, `.maxFrequency` | properties | Reflected displayed frequency limits in Hz. |
| `.selectedFrequency` | read-only property | Last probed frequency in Hz; initial value `1000`. |
| `.frame` | read-only property | Latest `{bins, sampleRate, fftSize, minDb, maxDb}` frame, or `undefined` when unavailable. |
| `.resetPeaks()` | method | Clear held spectrum peaks without changing the audio source. |
| `webaudio:spectrumprobe` | event | `{frequency, decibels?}` after a pointer or keyboard probe; bubbles and is composed. |
| `webaudio:error` | event | `{error: Error}` for an input, frame or presenter failure; bubbles and is composed. |
Styling
The full-width host honors native `hidden`. Its outer surface uses the
`--wm-component-*` tokens, with `--wm-audio-spectrum-analyzer-surface-*`
overrides for this tag. The [spectrum presenter]() exposes
`--wm-spectrum-height`, `--wm-spectrum-plot-background` and related
`--wm-spectrum-*` colors for the plot and controls.
---
#
> Inspect live attack cues with sensitivity, a refractory interval, freeze and clear controls.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-transient-analyzer/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
The transient analyzer makes recent attacks visible while a signal sounds.
It compares successive frequency and RMS snapshots, shows strength against a
sensitivity threshold, and marks accepted cues in a short history. Freeze
holds the display; Clear starts a fresh inspection. For a decoded clip's
stable onset times or a DJ beat grid, use the
[Analyze API]() and compose positions with
[`.regions`](). This live
tool does not estimate BPM or align a beat grid.
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-transient-analyzer/)
## Import
```html
```
The demo pairs one player with this tool. Start playback, then adjust the
Sensitivity control or use Freeze and Clear on the real component. Copy
includes both Elements, and Reset restores their demo settings.
## Signal and lifetime
The tool borrows the bound player's analyser while playing, or a caller-owned
`.analyser` in preference to it. The `player` selector must resolve uniquely
in the current Document or ShadowRoot; late targets and source replacement
are observed. Neither the element nor its presenter starts playback, opens a
microphone, creates an `AudioContext`, or changes borrowed analyser settings.
Sampling is approximately every 50 ms. Positive FFT-bin change and rising
RMS contribute to a normalized `0..1` strength; sensitivity changes its
acceptance threshold, and `min-interval-ms` suppresses hits too close to a
previous hit. Silence is shown separately from an unavailable analyser.
The recent plot keeps at most 64 sampled observations, not a clip-wide event
timeline. Freeze stops sampling but keeps the current history; unfreezing
resets the detector's comparison frame before new cues. When following a
player, pause, seek and source change invalidate old readings. Replacing an
explicit analyser, graph loss, hiding the document or disconnect also releases
old evidence and polling/subscriptions. A stalled source is reported and
retried without disposing the borrowed graph.
API
| Member | Kind | Behavior |
| --- | --- | --- |
| `player` | attribute | Unique selector for a playback owner whose analyser is borrowed while playing; default absent. |
| `sensitivity` | attribute | Detection sensitivity in `0..1`, default `0.55`; invalid values use the default. Higher sensitivity lowers the cue threshold. |
| `min-interval-ms` | attribute | Minimum spacing between accepted cues in milliseconds, clamped to `50..1000`; default `160`. Invalid values use the default. |
| `.analyser` | property | Explicit caller-owned `AnalyserNode`, preferred over player binding; getter returns the explicitly assigned node. |
| `.sensitivity`, `.minIntervalMs` | properties | Reflected detection settings with the bounds above. |
| `.sample` | read-only property | Latest `{strength, hit}` observation, or `undefined` before a frame. Strength is normalized `0..1`. |
| `.hitCount` | read-only property | Accepted cues since the last reset or source/transport invalidation. |
| `.frozen` | read-only property | Whether the tool's Freeze control holds its current history. |
| `.reset()` | method | Clear current detector evidence, history and hit count without changing playback or the audio graph. |
| `webaudio:transient` | event | `{timeMs, strength, threshold, intervalMs?}` for each accepted cue. `timeMs` is an approximate `performance.now()`-style capture timestamp, **not** clip seconds; `intervalMs` is the time since the prior accepted cue when available. Bubbles and is composed. |
| `webaudio:error` | event | `{error: Error}` for input, sampling or stalled-frame failures; bubbles and is composed. |
The detector reports **onset cues**, not exact onset positions. Its timing and
strength depend on the borrowed analyser's FFT size, smoothing and this
tool's polling cadence. The threshold is an internal visual/detection
quantity, not a gain or playback control.
**Accessibility.** Freeze and Clear are native buttons; Freeze exposes
`aria-pressed`. Sensitivity is a labelled native range input. The plot has a
text alternative reporting current strength, threshold and hit count; status
and numeric readouts remain outside the visual bars.
Styling
The host's neutral outer surface honors shared `--wm-component-*` tokens and
`--wm-audio-transient-analyzer-surface-*` overrides. The
[transient presenter]() exposes
`--wm-transient-analyzer-foreground`, `--wm-transient-analyzer-background`,
`--wm-transient-analyzer-border`, `--wm-transient-analyzer-padding`,
`--wm-transient-analyzer-strength`, `--wm-transient-analyzer-hit` and
`--wm-transient-analyzer-threshold` tokens. Public parts are `root surface`,
`plot`, `freeze`, `clear` and `sensitivity`; structural `wui-*` child classes
are not additional styling contracts.
---
# Audio Web Components
> Every drop-in Audio element by capability — what each tag is for, how the groups compose, the registration entries and the shared theming variables.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/element/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
The drop-in custom elements of `@webmusic/audio` — place a tag on the page, no
JavaScript wiring required. Each capability ships its elements from its
`/element` subpath, so the capability root tree-shakes free of DOM code. To load
and register them — via a CDN script, a bundler, or individual `define*` calls —
see [Audio setup]().
| Capability | Elements |
| --- | --- |
| [Play](#play) | A central player, attached queue/mix controls and recording. |
| [Analyze](#analyze) | Five live signal tools for metering, waveform, spectrum and transient inspection. |
| [View](#view) | Drawing audio: a multi-view stage for clips and regions, plus a live signal strip. |
## Play
Use one [`audio-player`]() for transport and
connect the task-specific controls through `player="#session"` or `.player`.
The player delegates to one clip, queue or mix backend. The linked playlist and
mixer display selection and mix controls without a duplicate transport bar.
| Element | Register | Purpose |
| --- | --- | --- |
| [``]() | `defineAudioPlayerElement(tag?)` | Core play/pause, seek and time control over the selected backend. |
| [``]() | `defineAudioPlaylistElement(tag?)` | Queue selection, auto-advance, list loop and prefetch, attached to the player. |
| [``]() | `defineAudioMixerElement(tag?)` | Master/member volume, mute and solo for the player's mix backend. |
| [``]() | `defineAudioRecorderElement(tag?)` | Capture microphone input and audition the finished take through the player. |
| — | `defineAllAudioElements()` | Register all four Play tags. |
Imports come from `@webmusic/audio/play/element`; `/play/auto` registers them on
import.
Play events use `webaudio:*` CustomEvents with `bubbles: true` and `composed: true`.
Queues keep their sequencing policy and mixers keep their member state. The
central facade does not add a clock or promise sample-accurate group playback.
The compact meter is an [Analyze tool](#analyze), not another transport.
## Analyze
The five Analyze Elements are live tools. They borrow the graph of one
[``]() while it plays, without
loading another URL or adding a transport. Choose the measurement or playing
task and place the surface beside the player:
| Element | Live task |
| --- | --- |
| [``]() | Inspect sampled RMS, sample peak, crest and a peak threshold; freeze or reset the hold. |
| [``]() | Monitor normalized level or FFT bars at a glance beside playback. |
| [``]() | Inspect a sampled waveform with trigger edge, level, timebase, freeze and a time probe. |
| [``]() | Probe a live log-frequency FFT; freeze the frame or hold peaks. |
| [``]() | Inspect approximate live attack strength and adjust detection sensitivity. |
These tools work on a borrowed live `AnalyserNode`, including when a caller
supplies `.analyser` directly. A stream can feed them without decoded clip
samples, but an `AudioContext` and signal graph must already exist. They do
not seek the player. The sampled level is not integrated LUFS or true peak;
FFT bins are not calibrated acoustic SPL. A streaming transient cue is not a
beat-grid or sample-accurate onset result. The compact meter may also build a
transparent tap in a caller-supplied context; see its resource contract.
Whole-clip key, tempo, loudness and onset estimates, beat grids, clip summaries,
chroma, distributions and offline pitch tracks are data workflows under the
[Analyze API](). Use an [analysis session]()
for repeated or incremental work, the [realtime analyzer]()
for custom live UI, or the API's Worker client for off-main-thread clip analysis.
For visible beat positions, convert a Headless session's `tempo.grid.beats` to
point regions and assign them to [`.regions`]().
### Compose live tools
```html
```
`import '@webmusic/audio/analyze/auto'` registers the five tags at their default
names. `defineAllAudioElements()` does the same from the tree-shakeable
`@webmusic/audio/analyze/element` entry, which does not auto-register. Individual
registration helpers allow a smaller selected surface.
| Element | Register | Surface |
| --- | --- | --- |
| `` | `defineAudioLevelAnalyzerElement(tag?)` | Sampled dynamics inspector. |
| `` | `defineAudioMeterElement(tag?)` | Compact normalized level or spectrum monitor. |
| `` | `defineAudioOscilloscopeElement(tag?)` | Triggered time-domain inspection. |
| `` | `defineAudioSpectrumAnalyzerElement(tag?)` | Probeable live spectrum. |
| `` | `defineAudioTransientAnalyzerElement(tag?)` | Approximate streaming attack inspection. |
| — | `defineAllAudioElements()` | Register all five default tags. |
## View
The **visualization** elements of [`@webmusic/audio/view`](). `` provides three drawings of the same audio under one `type` attribute, including waveform regions. `` shows a scrolling strip of a live signal.
| Element | Purpose |
| --- | --- |
| [``]() | The projection stage — waveform, spectrogram or meter, chosen with `type`; follows a bound player, seeks, zooms, pans and annotates. |
| [``]() | A scrolling projection of a live `AnalyserNode` — waveform or spectrogram. The only live *time-axis* surface here. |
Inputs are component-specific. `` binds to playback through `player`;
`` borrows an analyser through `source` or `.analyser`.
Use ``'s `.regions` and `annotate` support to display or create regions.
Waveform views can use clip or peaks data, load clips through `src`, or draw
precomputed `peaks-src` data without decoding audio.
Analysis stays caller-owned: `` requires
`.spectrogram` data from [`@webmusic/audio/analyze`]() or another
compatible producer. Binding a player supplies playback state, not that analysis.
These Elements compose published UI Kit surfaces. `` uses stage + status;
`` uses animated `CanvasStage`. Domain geometry and pixels remain
in Audio Headless/render adapters; the presenters own generic DOM, canvas
sizing/DPR, accessible interaction, status and cleanup.
Point `` at an [``]() with `player="#id"` and the waveform and spectrogram **follow the playhead** as it plays; its `meter` type taps that player's analyser. This binding is event-based and uses no player-package imports. For browser renderers, code-only view models, colormaps and layout math behind the tags, see the [View API](). Existing `@webmusic/audio/view/element` and `/view/auto` imports still expose/register `` for compatibility; the Analyze entry above is its canonical home.
| Element | Register | Renders |
| --- | --- | --- |
| [``]() | `defineAudioViewElement(tag?)` | Min/max waveform with a playhead (default). |
| [``]() | — | STFT magnitude heatmap (time × frequency). |
| [``]() | — | Live RMS / spectrum level meter. |
| `` | `defineAudioLiveViewElement(tag?)` | A scrolling projection of a live `AnalyserNode`. |
| `AudioViewElement`, `AudioLiveViewElement` | `defineAllAudioElements()` | The classes behind the tags — extend one to build a custom surface, or register every element at its default tag. |
| [`AudioViewDragMode`]() | type from `@webmusic/audio/view/element` | `seek`, `scrub`, `pan` or `none` for the AudioView Element's `dragMode` property. |
## Theming
Every visible element owns the same neutral outer component surface: `#fff`, a
`1px solid #d8d8d8` border, `.6rem` padding and square corners. Override those defaults with
`--wm-component-background`, `--wm-component-border`, `--wm-component-padding` and
`--wm-component-radius`. The `--wap-*`, `--wapl-*`, `--wam-*`, `--war-*` and `--wameter-*`
families remain compatibility and inner-control palettes, so the transport, meter track or record
accent can be retuned independently of the outer surface:
```css
/* Keep the neutral surface, add rounding, and retune the transport controls. */
audio-player {
--wm-component-radius: 12px;
--wap-fg: #111;
--wap-accent: #e23b3b;
--wap-track: #ececec;
--wap-height: 3rem;
}
```
Each play element prefixes its variables distinctly — `--wap-*` (player), `--wapl-*` (playlist), `--wam-*` (mixer), `--war-*` (recorder), `--wameter-*` (meter) — and lists them under its own **Styling** heading. [``]() carries a live re-skin of the whole transport from page CSS.
### Analysis variables
Analyze hosts use the same neutral outer surface as Play: `--wm-component-background`,
`--wm-component-border`, `--wm-component-padding` and `--wm-component-radius`.
The level, oscilloscope, spectrum and transient tools use their respective
[UI Kit presenters](). Each tag's Styling section names its relevant
public hooks.
---
#
> A mixer companion for audio-player, with master/member volume, mute and solo.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/element/play/audio-mixer/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`` supplies a mixing desk to a linked
[`audio-player`](). The player handles transport;
the mixer provides master/member volume, mute and solo. Its Headless `AudioMixer`
coordinates simultaneous sources using their supported scheduling capabilities.
Each member retains its own clock; linking does not create a new shared clock.
Assign `.members` to let the component own the backend, or `.mixer` to borrow an
existing backend. A borrowed mixer is never disposed by the Element.
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/audio/element/play/audio-mixer/)
## Import
```html
```
API
```ts
import { loadClipFromUrl } from '@webmusic/audio/play';
import { createAudioMixer } from '@webmusic/audio/play/headless';
import type { AudioMixerElement } from '@webmusic/audio/play/element';
const clip = await loadClipFromUrl('/wav/Arabesque%20No.1.wav');
// Drive a shared mixer (never disposed by the element):
const mixer = createAudioMixer();
mixer.add({ id: 'layer-a', clip });
mixer.add({ id: 'layer-b', clip, volume: 0.7 });
document.querySelector('audio-mixer')!.mixer = mixer;
// …or let the element build (and own) its own mixer from specs:
document.querySelector('audio-mixer')!.members = [
{ id: 'layer-a', clip },
{ id: 'layer-b', clip, volume: 0.7 },
];
```
| Member | Kind | Description |
| --- | --- | --- |
| `player` | attribute | Unique selector for the central player; observes late insertion, upgrade, replacement and removal. |
| `.player` | property | Assign/read the central `AudioPlayer` or its Element; explicit assignment wins over the selector. |
| `master-volume` | attribute | Master fader level, applied live. |
| `autoplay` | attribute | Start the ensemble on mount. Browsers still gate audio until the user interacts. |
| `solo` | attribute | Id of the member to solo, applied live. Solo is exclusive across strips. |
| `.mixer` | property | An existing (shared) `AudioMixer` to drive — never disposed by the element. |
| `.members` | property | `AudioMixerMemberSpec[]` — each `{ id, clip?, player?, effect?, volume? }`. `clip` is optional when you pass an existing `player` (any structural `PlayerLike`). Reads back the applied specs. |
| `.audioContext` / `.destination` | property | Share the page's context and output node instead of letting the mixer mint its own. Re-mints only a mixer the element owns; a borrowed one is left alone. |
| `.masterVolume` | property | Reads the attached mixer snapshot and writes its master level. Without a mixer, retains the next owned mixer's construction input. |
| `.seek(seconds)` | method | Seek members to the same requested position. During a running group, eligible naturally ended stems rejoin with the running members; independently paused members stay paused. |
| `.add(spec)` / `.remove(id)` | method | Add or remove a member live. `remove()` with no argument is still the DOM's own detach. |
| `webaudio:end` | event | The whole ensemble finished (every member ended). |
| `webaudio:memberend` | event | `detail = { id }` when one member's clip finishes. |
| `webaudio:error` | event | An engine failure — a failed seek-restart, or a member spec carrying neither `clip` nor `player`. |
The Headless mixer owns the mix state. The desk reads its frozen `snapshot()` and subscribes to `change`, so initial borrowed settings, direct mixer commands, membership edits and multiple desks sharing one mixer stay consistent. Assigning `.mixer` preserves that mix unless explicit `master-volume` or `solo` attributes override it. Removing the solo member through either API clears solo and restores other members' requested gains and mute flags.
The linked player calls `play()` / `pause()` / `stop()`; the master and per-strip sliders drive `setMasterVolume` / `setVolume`, and **M** / **S** toggle `mute` / `solo` live (solo is exclusive across strips).
A linked desk omits its transport bar. Removing it releases only its own backend
connection; it never disposes the central player. Without a connection, the mixer
shows its own transport. The latest explicit player source selection wins.
A mix has no single clip or aggregate analyser: clip views/meter need a specific
member or an explicitly routed signal.
Styling
The host fills its container width by default, can shrink inside flex/grid
layouts, and respects `hidden`. Set host width in your application for a narrower
control; spacing between components belongs to their composition.
The default surface uses the same neutral UI tokens as Score. These optional legacy aliases
demonstrate an explicit dark customization; they do not define the default palette.
```css
audio-mixer {
--wam-bg: #15171c; /* desk background */
--wam-fg: #eef; /* labels + glyphs */
--wam-accent: #4ea1ff; /* fader accent + engaged mute / solo */
--wam-track: #2a2e37; /* fader track */
}
```
`--wam-track-border` controls the fader outline and, when omitted, follows
`--wm-control-border` → `--wm-border` → `#d8d8d8`.
---
#
> Central play/pause, seek and time controls for a selected audio clip, playlist or mix.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/element/play/audio-player/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`` is the central Audio transport. It owns a stable
[`AudioPlayer`]() and controls one selected clip,
playlist or mix. Connect playlist, mixer and recorder components using
`player="#session"` or a `.player` object reference. The player supplies the shared
play/pause, seek and time controls; companions supply their own tasks.
A `src` URL or `.clip` selects one clip. Linked playlist/mixer components select
their backend, preserving its state. A linked recorder auditions its take through
this player. Views and meters follow the same owner.
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/audio/element/play/audio-player/)
## Import
```html
```
## Linked composition
```html
```
Register Play tags from `@webmusic/audio/play/element` and the meter from
`@webmusic/audio/analyze/element`. The former `view/element` meter import still
works for compatibility. Use a mixer instead of the playlist for simultaneous
stems. One player selects one backend: assigning a clip replaces the queue/mix
connection. Separate sessions use separate player IDs.
Use `AudioPlayerElement` and `defineAudioPlayerElement()` from the Play Element
entry. Its `.player` property accepts the central `AudioPlayer`.
[`AudioClipPlayer`]() remains the low-level
Headless engine for one clip.
## Re-skinning
Every visual knob is a CSS custom property, so the whole transport re-skins from
the page's own stylesheet — no provider, no theme runtime, and no rebuild of the
audio graph. The `--wap-*` names are listed under **Styling** below; setting the
shared `--wm-*` tokens instead re-skins this element along with the rest of the
capability. This one is restyled entirely from page CSS:
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/audio/element/play/audio-player/)
API
```ts
import { loadClipFromUrl } from '@webmusic/audio/play';
import { gain } from '@webmusic/audio/play/headless';
import type { AudioPlayerElement } from '@webmusic/audio/play/element';
const el = document.querySelector('audio-player')!;
el.clip = await loadClipFromUrl('/wav/Arabesque%20No.1.wav'); // drive a loaded AudioClip (skips src)
el.effect = gain({ value: 0.8 }); // insert an effect into the graph
el.setAttribute('volume', '0.8'); // master volume (live attribute)
el.setAttribute('rate', '1.25'); // playback speed (live attribute)
await el.load(arrayBuffer); // load a non-URL buffer / blob
await el.player?.play(); // stable AudioPlayer facade
```
| Member | Kind | Description |
| --- | --- | --- |
| `src` | attribute | URL of an audio file (format inferred from the extension). Optional if you assign `.clip` / `.player`. |
| `format` | attribute | Force the decode format (e.g. `wav` / `mp3`) when the extension is missing or wrong. |
| `controls` | attribute | Shown by default; `controls="false"` (also `0`/`no`/`off`) hides the transport, leaving a headless element. Toggling it never remounts the transport, so the playhead survives. |
| `loop` | attribute | `loop` (whole clip) or `loop="a,b"` (an A–B region in seconds). Applied **live** — changing it keeps the playhead instead of rebuilding the player. |
| `engine` | attribute | `auto` (default) / `buffer` (decoded) / `media` (streamed `Styling
The host fills its container width by default, can shrink inside flex/grid
layouts, and respects `hidden`. Set host width in your application for a narrower
control; spacing between components belongs to their composition. The default
transport gap is `0.75rem`, matching Score; override `--wm-transport-gap` or the
shared `--wm-control-gap` token to change it.
The default surface uses the same neutral UI tokens as Score. These optional legacy aliases
demonstrate an explicit dark customization; they do not define the default palette.
```css
audio-player {
--wap-bg: #15171c; /* transport background */
--wap-fg: #f2f4f8; /* text + button glyphs */
--wap-accent: #4ea1ff; /* played fill */
--wap-track: #2a2e37; /* seek track */
--wap-radius: 8px;
--wap-height: 44px;
}
```
---
#
> A queue: play an ordered set of clips one after another, with auto-advance, previous/next/select, list loop and prefetch of the next entry — all through one shared AudioContext.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/element/play/audio-playlist/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`` supplies queue selection and sequencing to a linked
[`audio-player`](). The player displays transport;
the list displays entries and their loading/current state. The queue keeps one
context across entries and reuses its tested auto-advance/prefetch behavior.
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/audio/element/play/audio-playlist/)
## Import
```html
Arabesque WAV · pass 1
Arabesque WAV · pass 2
Arabesque MP3 · streamed
```
The entries live in light DOM, so the markup degrades to a plain list without JavaScript. A comma-separated `src` works for the simple case, and `.entries` takes over from code:
```html
```
```ts
import {loadClipFromUrl} from '@webmusic/audio/play';
import type {AudioPlaylistElement} from '@webmusic/audio/play/element';
const list = document.querySelector('audio-playlist');
if (!list) throw new Error('Audio playlist not found');
const clip = await loadClipFromUrl('/wav/Arabesque%20No.1.wav');
list.entries = [
{ id: 'a', label: 'Arabesque WAV · loaded', clip },
{ id: 'b', label: 'Arabesque WAV · queued', src: '/wav/Arabesque%20No.1.wav' },
{ id: 'c', label: 'Arabesque MP3 · streamed', src: '/mp3/Arabesque%20No.1.mp3', streaming: true },
];
await list.play();
```
API
| Member | Kind | Description |
| --- | --- | --- |
| `player` | attribute | Unique selector for the central `audio-player`; late insertion, upgrade and replacement are observed. |
| `src` | attribute | A comma or whitespace separated list of URLs. Ignored when `.entries` is assigned. |
| `loop` | attribute | Live list-loop policy: restart after the last entry and let previous/next wrap, without resetting the current entry. |
| `autoplay` | attribute | Start on mount or when enabled, using the current selection. Disabling does not pause active playback. Browsers may still block playback. |
| `volume` / `rate` | attribute | Applied live to the entry that is playing — changing them never interrupts it. |
| `prefetch` | attribute | Live policy for resolving the next entry. On by default; disabling prevents future prefetches while an in-flight load may still finish and cache. |
| `skip-failed` | attribute | Live policy for the next load failure, including pending loads. On by default; false stops at the failed entry. |
| `engine` | attribute | `auto` (default) / `buffer` / `media`, forwarded to every entry's player. |
| `.entries` | property | `AudioPlaylistEntry[]` — `{id, label?, clip?, src?, streaming?, format?}`. Overrides markup. |
| `.playlist` | property | The underlying `AudioPlaylist` controller, for the full queue API. Borrowed — do not dispose it. |
| `.player` | property | Assign/read the linked central `AudioPlayer` (or its Element). Explicit assignment takes precedence over the selector. |
| `.activePlayer` | readonly property | The current entry's low-level `AudioClipPlayer`, borrowed from the queue and replaced on advance. |
| `.index` | property | Index of the current entry, or `-1` when the queue is empty. |
| `.play()` / `.pause()` / `.stop()` | method | Transport for the queue as a whole. |
| `.next()` / `.previous()` / `.select(idOrIndex)` | method | Move between entries, retaining playback intent. Invalid select ids or non-integer/out-of-range indices reject with RangeError and preserve the old entry. |
| `.seek(seconds)` | method | Seek within the current entry. |
| `.clip` / `.seconds` / `.currentTime` / `.duration` / `.analyser` | property | The `PlayerLike` facade, so [``]() can visualize the queue itself rather than one entry. |
| `webaudio:trackchange` | event | `detail = { id, index }` — a different entry became current, including the first. |
| `webaudio:trackend` | event | `detail = { id, index }` — one entry played to its natural end. The queue advances next. |
| `webaudio:playlistend` | event | The last entry ended and the list is not looping. |
| `webaudio:timeupdate` | event | `detail = { seconds, duration, progress }` for the current entry. |
| `webaudio:seek` | event | `detail = { seconds, progress }` when the user drags the seek bar. |
| `webaudio:error` | event | A load or playback failure. A failed entry is marked in the list and skipped. |
**The queue uses track-specific completion events.** That name already means "one clip finished" on ``; reusing it here would make the two impossible to tell apart on a page carrying both. A queue reports `trackend` per entry and `playlistend` once at the bottom.
**Connection and ownership.** A linked list installs its queue as the central
player's borrowed backend. Disconnect removes only that exact attachment and
releases its owned queue; it never disposes the central player. Source replacement
or a newer companion selection must not be undone by an older cleanup. An
explicit unresolved selector remains detached. Without a connection, the
playlist provides its own transport controls.
**Loop is a list-level concept.** The queue never sets `loop` on the entry's player: the buffer engine suppresses its end callback while looping, so a looping track player would never let the queue advance. `loop` on this element loops the *list*.
If every entry fails to load, the queue stops after one pass even with `loop`
enabled. Press Play again to retry after resolving the error; it does not
continuously retry failed entries in the background.
**Live configuration.** Loop, prefetch and skip-failed changes retain the current player, position and cache. Autoplay does not rebuild the queue. Changing the engine still rebuilds it because it selects a different backend. Source-list changes reconcile entries by id/source; an explicit `.entries` assignment continues to take precedence.
**What prefetch covers.** It fetches and decodes the next entry's clip — the dominant cost — while the current one plays. The per-entry audio graph is still built when that entry starts. A prefetch failure is not fatal: the entry is marked and retried when it actually becomes current.
For the queue without a custom element, use `createAudioPlaylist(options)` from [`@webmusic/audio/play/headless`](). The element is that controller plus attribute plumbing and the shared UI-kit presenter — and it is the element that supplies the controller's `loadEntry`, since the headless layer may not reach the loading API itself.
Styling
The host fills its container width by default, can shrink inside flex/grid
layouts, and respects `hidden`. Set host width in your application for a narrower
control; spacing between components belongs to their composition.
The shared playlist presenter supplies the neutral `--wm-component-*` outer surface. The legacy
`--wapl-*` aliases remain available for a playlist-specific skin:
```css
audio-playlist {
--wapl-bg: #fff;
--wapl-fg: #111;
--wapl-accent: #e23b3b;
--wapl-active: rgba(226, 59, 59, .12);
--wapl-radius: 12px;
}
```
In linked mode, the transport bar is omitted. An independent playlist shows its
transport, using `--wapl-accent` and `--wm-playlist-button-*`.
Rows carry `aria-current="true"` for the entry playing and `data-status="loading" | "error"` while an entry is being fetched or after it failed, so a stylesheet can mark both. The presenter patches those in place rather than rebuilding the list, so a keyboard user's focus survives playback ticks.
---
#
> A record button with a live level bar — opens the microphone, captures the take into an AudioClip, and dispatches it on stop via webaudio:recorded.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/element/play/audio-recorder/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`` is a **record button with a live level bar**. Press it to open the microphone through an [`AudioRecorder`]() (the button turns square while armed and the bar tracks the input level); press again to stop, at which point it assembles the captured PCM into an `AudioClip` and dispatches `webaudio:recorded` with it. It needs **microphone permission** — the browser prompts on first record — and never auto-starts. Opening remains busy until the capture context is running. The current [ScriptProcessor backend]() captures fixed stereo blocks; stop has no final partial-block flush.
For symbolic note performances, use the corresponding
[``](), which produces a `Score`
and exports MIDI / MusicXML.
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/audio/element/play/audio-recorder/)
## Import
```html
```
```ts
import { defineAudioPlayerElement } from '@webmusic/audio/play/element';
import type {
AudioPlayerElement,
AudioRecorderElement,
AudioRecorderRecordedDetail,
} from '@webmusic/audio/play/element';
defineAudioPlayerElement();
const recorder = document.querySelector('audio-recorder')!;
const player = document.querySelector('audio-player')!;
recorder.addEventListener('webaudio:recorded', (e) => {
const { clip } = (e as CustomEvent).detail;
player.clip = clip; // play the take straight back
});
```
API
| Member | Kind | Description |
| --- | --- | --- |
| `player` | attribute | Unique selector for the central player used for take audition. Late insertion/upgrade and replacement are observed. |
| `.player` | property | Assign/read the borrowed central `AudioPlayer` or its Element; explicit assignment overrides the selector. |
| `device-id` | attribute | Capture from a specific input device (a `MediaDeviceInfo.deviceId`). |
| `echo-cancellation` | attribute | Boolean constraint passed to `getUserMedia`. Absent leaves the engine default. |
| `sample-rate` | attribute | Requested capture rate. |
| `max-seconds` | attribute | A **memory** ceiling, converted to a frame limit using — in order — the rate the last take actually resolved to, the `sample-rate` you asked for, or the recorder's own `44100` fallback. Breaching it discards the unfinished take and surfaces as `webaudio:error`; it is not a timed stop that returns a clip. |
| `auto-export` | attribute | Export each take to WAV on stop and emit `webaudio:exported`. |
| `.recording` / `.playing` / `.level` | property (get) | Live state: capturing, playing the take back, and the raw input level. |
| `.recorder` / `.take` | property (get) | The underlying `AudioRecorder` and the last captured `AudioClip`. |
| `.start()` / `.stop()` / `.toggle()` | method | Drive capture from code instead of the button. `stop()` resolves with the take. |
| `.playTake()` / `.stopPlayback()` / `.togglePlayback()` | method | Select and audition the take through the linked player. Stop affects that audition only while the player still selects this take. |
| `.exportTake(options?)` / `.downloadTake(fileName?)` | method | Encode the take as WAV, and hand it to the browser as a download. |
| `webaudio:recordingstart` | event | Capture actually began — the microphone is open and running. |
| `webaudio:recorded` | event | `detail = { clip }` — the captured `AudioClip` — after a successful stop. |
| `webaudio:level` | event | `detail = { level }` — the live input level — while recording. |
| `webaudio:exported` | event | `detail = { blob, format, clip }` after an export. |
| `webaudio:error` | event | `detail` is the `Error` if the microphone can't be opened, or a limit is breached. |
The element's class is `AudioRecorderElement` and its factory is
`defineAudioRecorderElement()`. The `webaudio:recorded` event carries
`AudioRecorderRecordedDetail`.
A failure releases the failed engine and commits idle state before `webaudio:error` is dispatched, so an error listener may call `start()` to retry. Live `level` is linear RMS over all retained channels, not a left-channel-only reading or a loudness measurement.
With `player="#session"`, take audition delegates to the central player instead
of creating an independent playback engine. Capture remains recorder-owned.
Disconnecting does not dispose the borrowed player; a take already handed to it
remains player-owned. Without a connection, the recorder owns its audition player.
Wire `webaudio:recorded` to do something else with the take — feed it to an [``](), queue it in an [``](), export it via [`clipToBlob`](), or add it to an [``]().
Styling
The host fills its container width by default, can shrink inside flex/grid
layouts, and respects `hidden`. Set host width in your application for a narrower
control; spacing between components belongs to their composition.
The default surface uses the same neutral UI tokens as Score. These optional legacy aliases
demonstrate an explicit dark customization; they do not define the default palette.
```css
audio-recorder {
--war-bg: #15171c; /* surface background */
--war-fg: #eef; /* text */
--war-accent: #e0445b; /* record button + level fill */
--war-track: #2a2e37; /* level-bar track */
}
```
`--war-button-border` controls the idle action-button line and, when omitted, follows
`--wm-control-border` → `--wm-border` → `#d8d8d8`.
---
#
> A scrolling projection of a live AnalyserNode — waveform or spectrogram — with the newest column pinned to the right edge and history sliding left.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/element/view/audio-live-view/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
Draw what is being heard **right now**. The newest column sits at the right edge and history slides left, so the strip reads as "now" scrolling away.
This is the capability's missing cardinality. Every other surface here draws data that already exists — a decoded clip, a peaks pyramid, a precomputed spectrogram. The only live path was [``](), which is **instantaneous**: it has no time axis at all, so nothing could show what the microphone did a second ago. This element is the only live time-axis surface in the package.
```html
```
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/audio/element/view/audio-live-view/)
Two drawings live under one `type`, exactly as `` keeps waveform and spectrogram under one attribute: both are "append one column per frame and scroll", and they differ only in how a column is painted. `waveform` reduces each frame to a min/max band; `spectrogram` paints one bin per row, low frequencies at the bottom, coloured through `color-map`. An unknown `type` falls back to `waveform`.
The Element itself owns none of the live canvas machinery. Headless `LiveViewController` owns the
borrowed-analyser sampling, scratch arrays, clock and bounded history; `paintLiveProjection()` owns
only waveform/spectrogram pixels; published `@webmusic/ui` `CanvasStage` owns canvas creation,
DPR/resize, RAF, accessible label, status overlay and teardown. The Element resolves attributes and
the analyser source, then composes those three ports.
## The analyser is borrowed
`source="#id"` looks for a tap on the target in this order: `.analyser`, `.inputAnalyser`, `.recorder?.inputAnalyser`, `.player?.analyser` — so a player, a meter and a recorder all resolve without demanding one spelling. You can also assign `.analyser` directly, which overrides `source`.
The node is **borrowed**. This element never creates, mutates or disposes it, so several views can read the same tap and disconnecting one leaves the others (and the recording) untouched.
API
| Member | Kind | Default | Description |
| --- | --- | --- | --- |
| `type` | attribute / property | `waveform` | `waveform` or `spectrogram`; unknown values fall back to `waveform`. Changing it starts a fresh history. |
| `source` | attribute | — | CSS selector of an element exposing an `AnalyserNode`. Changing it starts a fresh history. |
| `window-seconds` | attribute / `.windowSeconds` | `5` | Seconds of history the strip holds, minimum `0.5`. Changing it starts a fresh history. |
| `height` | attribute | `4rem` | Strip height — a bare number is `px`, or any CSS length. |
| `wave-color` | attribute | `currentColor` | Column fill for `type="waveform"`. |
| `color-map` | attribute | `viridis` | Palette for `type="spectrogram"` (`viridis` · `magma` · `grayscale` · `gray`; an unknown name falls back to `viridis`). |
| `.analyser` | property | — | Borrow an `AnalyserNode` directly; overrides `source`. Never disposed here. |
| `.live` | property (getter) | — | Whether an analyser is currently resolved. |
| `webaudio:error` | event | — | `detail = { error }` if presenter sizing/animation or the draw adapter fails. `{ bubbles, composed }`. |
The `CanvasStage` requests an initial draw and, when `requestAnimationFrame` is available, captures
one column per frame while connected. It cancels its animation and resize observer on disconnect;
hosts without RAF get the initial draw but no Element-owned timer fallback.
With nothing to show, the shared status overlay says `No live source — set source or .analyser`
until an analyser resolves, then `Waiting for signal…` until the first column arrives. Status text
is accessible DOM rather than pixels painted into the canvas.
Headless
The model behind the drawing is `LiveViewController` from `@webmusic/audio/view/headless`. It borrows
the analyser and owns type/window configuration, capture clock, reusable byte/float scratch arrays
and a [`LiveScrollBuffer`]() — a fixed-capacity ring covering the most recent
`windowSeconds`. Capacity never grows, so a view that runs for hours has bounded history.
```ts
import {createLiveViewController} from '@webmusic/audio/view/headless';
import {paintLiveProjection} from '@webmusic/audio/view/render';
const live = createLiveViewController({analyser, type: 'waveform', windowSeconds: 4});
function draw(frame) {
live.capture(frame.time);
paintLiveProjection(frame, live.buffer, live.type, {waveColor: '#4a9eff'});
}
```
Changing analyser, type or window clears incompatible scratch/history state. The controller never
schedules, mutates or disconnects the borrowed node. It is DOM-free and the painter accepts a
structural canvas frame, so both halves are independently reusable without the Element.
Styling
The `CanvasStage` root is the labelled image; its internal canvas is presentational and its status
overlay remains accessible. Its outer box uses the shared neutral `--wm-component-*` surface (or
`--wm-stage-surface-*` for this presenter alone). Set the host height through the attribute, the
waveform colour through `wave-color` or inherited `color`, and the full-bleed drawing viewport and
status through `--wm-stage-*` / `--wm-status-*`:
```css
audio-live-view {
color: #4a9eff; /* waveform columns, via currentColor */
}
```
```html
```
Column width is the strip width divided by the ring's capacity, so a shorter `window-seconds` gives
wider columns rather than a stretched history. `CanvasStage` owns device-pixel sizing and redraws
after host resize.
---
#
> The Audio projection stage — one element and three drawings under a type attribute: a min/max waveform, an STFT spectrogram heatmap and a live level meter, on one geometry model.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/element/view/audio-view/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`` is the Audio **projection stage** — one element that draws decoded audio onto a canvas and follows a player. The three drawings are the *same* tag with a different `type` attribute (and the matching data property). Switching `type` at runtime disposes the old visualizer and re-renders; unknown values fall back to `waveform`.
| `type` | Draws | Needs |
| --- | --- | --- |
| **`waveform`** (default) | Min/max waveform with a playhead. | `.peaks` — an `AudioPeaks` pyramid from [`computePeaks`](), a `peaks-src` JSON file, a `.clip` or a bound player. |
| **`spectrogram`** | STFT magnitude heatmap (time × frequency). | `.spectrogram` — `SpectrogramData` from [`computeSpectrogram`](). |
| **`meter`** | Live RMS / spectrum level bars, with no time axis. | An `AnalyserNode`, tapped from the bound player. |
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/audio/element/view/audio-view/)
The examples on this page share one decoded clip and peaks pyramid. The first
waveform does not wait for a spectrogram: spectra are prepared through the
analysis worker when a spectral mode is selected. The lower examples activate
when they approach the viewport or receive keyboard/pointer interaction, and
reuse their prepared data. Loading and failures appear beside the relevant demo.
The demos start in `drag-mode="scrub"`: the playhead stays centered while the
track moves underneath. Drag left toward later audio, or right toward earlier
audio. Their decoded player sounds the movement at your gesture speed, then
coasts in both position and sound before restoring its previous playing/paused
state. Parameters can switch
to viewport-only `pan`.
The main demo authors `height="96"` and `amplitude="1.6"` for a compact, fuller
waveform. This fixed visual gain can crop tall peaks; it changes neither PCM nor
playback volume. Parameters, copied markup and Reset use those authored values.
Switching drawing type retains the explicit height; the library defaults remain
128 px for waveform, 256 px for spectrogram and amplitude 1.
## Import
```html
```
`defineAudioViewElement(tag?)` registers the one element; `defineAllAudioElements()` (also from `@webmusic/audio/view/element`) registers all View tags at their default names. Import `@webmusic/audio/view/auto` to register on module evaluation, or `@webmusic/audio/view/global` for a prebuilt `
```
These markers are a view overlay, not a second transport or clock. Bind the
view to an [``]() if playback
following or seeking is needed.
## Which tasks reuse, and which do not
| Task/path | Current `update` behavior |
| --- | --- |
| Sample-identical input | Reuses the cached result. Sample comparison itself can scan the channels. |
| Changed sample rate or channel count | Runs full analysis: cached time/frequency axes and peaks layout cannot be reused. |
| `peaks` | Reuses unchanged base blocks for suitable length-preserving edits; rebuilds other levels or the pyramid as needed. |
| `onsets` | Reuses unaffected ranges and recomputes around the edit window. |
| `spectrogram`, `pitch`, `features` | Recomputed in full when requested. |
| `loudness`, `tempo`, `key` | Whole-signal recomputation; current result always includes loudness. |
| No cache / unavailable samples | Falls back to full analysis. |
`analyze()` is memoized, so calling it twice without an edit returns the same
result rather than repeating the work.
## Off the main thread
The separate `@webmusic/audio/analyze/worker-client` API supports worker analysis
and registered clip/session operations. Its transfer and disposal contracts belong
to the [Analyze API reference](). Do not infer zero-copy behavior
or automatic background execution from this Headless session alone.
## Time, ownership and contract source
This is a result-cache owner over static clip samples, not a playback clock.
Positions in its outputs are clip-relative seconds or sample/frame indices as
declared by each algorithm. It owns no context, DOM, stream or scheduler; release
retained data by dropping the session after pending work settles.
The public barrel and `packages/audio/src/analyze/headless/session.ts and analyze/core/analyze-clip.ts` define the value, option, state and event
contracts described below. The reference does not turn private implementation
fields into public API.
API
**Construction**
| Member | Kind | Description |
| --- | --- | --- |
| `createAudioAnalysisSession(clip, options?)` | AudioAnalysisSession | Retains an immutable clip and memoized result. Options are AudioAnalysisSessionOptions, an alias of AudioAnalysisOptions. |
**Options**
| Member | Kind | Default | Description |
| --- | --- | --- | --- |
| `tasks` | option | `['peaks', 'loudness']` | peaks, loudness, tempo, key, onsets, pitch, spectrogram or features. |
| `fftSize` | option | `2048` | STFT window size. |
| `hopSize` | option | `fftSize / 4` | STFT hop in samples. |
| `peaks` | option | algorithm default | ComputePeaksOptions, including baseSamplesPerPeak. |
| `tempo` | option | algorithm default | TempoOptions for the full-analysis path; current incremental recomputation does not forward this option. |
| `onsets` | option | algorithm default | OnsetOptions for full analysis; incremental onset recomputation uses its own window configuration. |
| `pitch` | option | algorithm default | PitchOptions passed to tracking. |
| `features` | option | algorithm default | ExtractFeaturesOptions plus optional names; forwarded to feature extraction. |
**Commands and state**
| Member | Kind | Description |
| --- | --- | --- |
| `analyze()` | `Promise` | Memoized full result, serialized with updates; resolution may reuse object identity. |
| `update(editedClip)` | `Promise` | Commits the new clip/cache only after successful task completion; reuse is described above. |
| `clip` | readonly AudioClip | Last successfully committed input. |
**Events, lifetime and types**
| Member | Kind | Description |
| --- | --- | --- |
| `AudioAnalysisSession` | type | analyze, update and clip only; no events, timer, stop or dispose. |
| `AudioAnalysisSessionOptions` | type | Alias of AudioAnalysisOptions; detailed nested algorithm options and result/task types belong to the Analyze API. |
| `AudioAnalysisResult` | result type | Peaks/loudness and requested analysis outputs; use their named units and optional fields. |
| `AudioAnalysisTask` | related API type | Task union listed above; chromagram and summary are not task literals. |
**Composed by.** The worker runtime can construct this session for registered
offline analysis work. The [live Analyze Elements]()
inspect an audio graph; they do not expose this session's cache or worker
protocol.
## Related
[Analyze API and workers]() · [Realtime analyzer]() · [View models]()
---
# createRealtimeAnalyzer
> Sample a borrowed AnalyserNode for level, spectrum or time-domain frames, and derive approximate live transient cues.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/headless/analyze/realtime-analyzer/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`createRealtimeAnalyzer(source, options?)` polls an `AnalyserNode` and reports
what is passing through it — level, brightness, and optionally pitch — as a
frame per interval. It is the live counterpart of everything on
[`@webmusic/audio/analyze`](): those functions measure a
clip you have, this one measures a signal you are hearing.
`source` is either an `AnalyserNode` or anything exposing one as `.analyser`,
which is why an `AudioClipPlayer` can be passed directly.
It borrows the analyser and never disconnects or disposes it. However, an explicit
`fftSize` currently changes that borrowed analyser on start; omit the option when
its configuration belongs to another owner.
`stop()` ends the poll loop and is idempotent; there is nothing else to
release.
## Import
```ts
import {createRealtimeAnalyzer} from '@webmusic/audio/analyze/headless';
const analyzer = createRealtimeAnalyzer(player, {pitch: true});
analyzer.start((frame) => {
paint(frame.rms, frame.peak, frame.centroid, frame.pitch);
});
// … later
analyzer.stop();
```
## Frames, and what may be missing
Every frame carries `time`, `rms`, `peak` and `centroid`. Three payloads are
conditional, and a UI has to handle their absence rather than treat it as an
error:
- `pitch` and `clarity` need `pitch: true` and the package's `pitchy`
dependency, which is imported dynamically. The first frames after `start()` can
arrive before that import settles — that is the resting state, not a
failure, so distinguish pending/unavailable pitch from a measured silent signal.
- `frequencyData` is present only with `includeSpectrum: true`, since copying
the byte spectrum every interval is a real cost when nothing draws it.
- `timeDomainData` is present only with `includeTimeDomain: true`. It is a fresh
`Float32Array` copy of the sampled window, so retaining one frame does not
change when the next interval reuses the analyser's scratch buffer. An
oscilloscope can inspect this window without retuning the borrowed FFT size.
`fftSize` is applied to the resolved analyser when explicitly supplied. This
implementation creates no analyser of its own and does not restore the previous
FFT size on stop. With the option omitted, the borrowed node keeps its setting.
## Live transient cues
`createTransientDetector()` is a separate Headless state machine for the
relative attack strength of successive realtime frames. Feed it the
sampler's optional `frequencyData`, `rms` and monotonic `time`; it compares
positive FFT-bin rises with RMS growth. The first observation establishes a
baseline and cannot be a hit. `observe(input, sensitivity?, minIntervalMs?)`
returns `{strength, threshold, hit, intervalMs?}`. `intervalMs` appears on a
hit after an earlier hit, not on every frame. Call `reset()` when playback
seeks, the source changes, or a frozen inspection resumes.
```ts
import {createRealtimeAnalyzer, createTransientDetector} from '@webmusic/audio/analyze/headless';
declare const analyser: AnalyserNode;
const detector = createTransientDetector();
const sampler = createRealtimeAnalyzer(analyser, {includeSpectrum: true});
sampler.start((frame) => {
if (!frame.frequencyData) return;
const cue = detector.observe({
time: frame.time,
rms: frame.rms,
frequencyData: frame.frequencyData,
}, 0.55, 160);
if (cue.hit) console.log(cue.strength, cue.intervalMs);
});
// On teardown:
sampler.stop();
detector.reset();
```
Sensitivity is clamped to `0..1` (default `0.55`); minimum hit spacing is
clamped to `50..1000` ms (default `160`). The detector rejects nonfinite time
or RMS and an empty FFT frame. Its output is a **polled attack cue**, not a
beat/BPM estimate, clip-relative timestamp or sample-accurate onset. The
borrowed analyser's smoothing and the poll interval can blur or miss attacks.
## Time, ownership and contract source
This analyzer observes live signal windows. Its interval timestamp is observation
time, not musical position or a session authority. It does not schedule sound,
seek the source or attach to a worker. A failed pitchy import omits pitch fields; source
resolution/setup can throw, while per-frame failures are swallowed.
The public barrel and `packages/audio/src/analyze/headless/realtime.ts` define the value, option, state and event
contracts described below. The reference does not turn private implementation
fields into public API.
API
**Construction**
| Member | Kind | Description |
| --- | --- | --- |
| `createRealtimeAnalyzer(source, options?)` | RealtimeAnalyzer | Accepts an AnalyserNode or RealtimeSource; source is resolved when start is called. |
| `RealtimeSource.analyser` | optional AnalyserNode | Borrowed tap; start throws if no analyser can be resolved. |
**RealtimeAnalyzerOptions**
| Member | Kind | Default | Description |
| --- | --- | --- | --- |
| `intervalMs` | option | `50` | Floored and lower-bounded at 1 ms for normal finite input. |
| `pitch` | option | `false` | Dynamically load the package's pitchy dependency and attach pitch/clarity if available. |
| `includeSpectrum` | option | `false` | Copy the raw byte frequency array into each frame. |
| `includeTimeDomain` | option | `false` | Copy the float time-domain sample window into each frame. |
| `fftSize` | option | unchanged | When supplied, assigns the borrowed analyser. No owned-analyser creation path exists. |
**RealtimeAnalyzer commands and state**
| Member | Kind | Description |
| --- | --- | --- |
| `start(onFrame)` | void | Starts a main-thread interval; repeated start while running is a no-op, retaining the existing callback. |
| `stop()` | void | Idempotently stops the interval; may later start again. |
| `running` | readonly boolean | Whether polling is active. |
**RealtimeFrame callback payload**
| Member | Kind | Description |
| --- | --- | --- |
| `time` | number | Milliseconds from performance.now(), falling back to Date.now(); not AudioContext time. |
| `rms` | number | RMS of float time-domain samples; no explicit output clamp. |
| `peak` | number | Maximum absolute float sample; no explicit output clamp. |
| `centroid` | number | Byte-spectrum-weighted centroid in Hz; 0 for no spectral energy. |
| `pitch` | optional number | Fundamental Hz when pitchy is available. |
| `clarity` | optional number | Pitch detector confidence paired with pitch. |
| `frequencyData` | optional Uint8Array | Owned copy of the byte spectrum when includeSpectrum is true. |
| `timeDomainData` | optional Float32Array | Owned copy of the float sample window when includeTimeDomain is true. |
**Transient detector**
| Member | Kind | Description |
| --- | --- | --- |
| `createTransientDetector()` | factory | Creates a detector with no graph, timer or DOM. |
| `observe({time, rms, frequencyData}, sensitivity?, minIntervalMs?)` | command | Returns relative `strength` in `0..1`, the active `threshold`, `hit`, and an optional elapsed `intervalMs` between hits. |
| `reset()` | command | Drops prior FFT/RMS evidence and the last-hit timestamp. |
| `TransientDetector`, `TransientInput`, `TransientObservation` | types | Detector contract, sample input and observation result. |
**Errors, lifecycle and types**
| Member | Kind | Description |
| --- | --- | --- |
| `onFrame(frame)` | callback | Only delivery interface; there is no on/off emitter or error event. Per-frame reads and callback exceptions are contained. |
| `stop()` | cleanup | Stops polling but does not change the borrowed graph. Drop the analyzer reference to release retained scratch state; no dispose method. |
| `RealtimeAnalyzer`, `RealtimeAnalyzerOptions`, `RealtimeSource`, `RealtimeFrame` | types | Object, construction source/options and callback shape owned by this page. |
**Composed by.** The [live Analyze Elements]() use this
sampler with the selected player's borrowed tap while playback is active, or
with a caller-supplied `AnalyserNode`. The transient tool also uses the
detector. Use these Headless contracts directly for application-owned live UI.
## Related
[Metering]() · [Live projection]() · [Clip analysis]()
---
# Audio Headless
> UI-free Audio transports, analysis sessions, projections and helpers — the purpose of each /headless entry, the reference page that owns every exported value, and explicit resource and time roles.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/headless/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
Headless components are the code-only domain objects behind the elements:
engines, runners, and renderers imported from
`@webmusic/audio//headless`. They never touch the DOM.
| Capability | Contents |
| --- | --- |
| [Play](#play) | `AudioPlayer`, clip/queue/mix backends, `AudioRecorder` and engines. |
| [Analyze](#analyze) | Cached analysis sessions, a main-thread realtime sampler and a streaming transient detector; worker clients use their own entry. |
| [View](#view) | Timeline/view models and pure projection helpers; renderers use the separate /render entry. |
## Roles and ownership
A player may drive clip time, a playlist owns queue selection, a timeline projects
another object's seconds, and a pure helper returns data without running anything.
Do not infer a shared session clock, emitter or disposal method from the Headless
label. Each owning page states its contract and related public types.
The export tables below are a manual export-ownership map, checked against the
public barrel. The Headless documentation gate searches recognized **value
names** on this page and across the capability's pages; it does not prove that
types, signatures, defaults, unique explanation ownership or lifecycle coverage
are complete. Those require source review. Browser renderers belong to
[API / View](), and application composition can use
[UI Kit]() without changing domain ownership.
## Play
`@webmusic/audio/play/headless` is everything that makes or captures sound
without drawing any of it: the transports, the two engines beneath them, the
recorder and the effect builders.
No DOM, no styles. Import from `@webmusic/audio/play/headless`; the elements
that wrap these objects are under
[Web Components › Play](), and the loaders and decoders
that produce a clip in the first place are on
[`@webmusic/audio/play`]().
| Export | Kind | Page |
| --- | --- | --- |
| `AudioPlayer` · `createAudioPlayer` | class · factory | [AudioPlayer]() |
| `AudioClipPlayer` · `createAudioClipPlayer` | class · factory | [AudioClipPlayer]() |
| `AudioPlaylist` · `createAudioPlaylist` | class · factory | [AudioPlaylist]() |
| `AudioMixer` · `createAudioMixer` | class · factory | [AudioMixer]() |
| `AudioRecorder` · `createAudioRecorder` | class · factory | [AudioRecorder]() |
| `BufferEngine` · `MediaEngine` | classes | [Engines]() |
| `gain` · `filter` · `delay` · `compressor` · `customEffect` · `chainEffects` · `insertEffect` | effect builders and graph helpers | [Effects]() |
**Type ownership**
| Exported types | Explanation owner |
| --- | --- |
| `AudioPlayerOptions`, `AudioPlayerTransport`, `AudioPlayerEvents` | [AudioPlayer]() |
| `AudioClipPlayerOptions`, `AudioClipPlayerEvents`, `AudioScratchSession`, `PlayerTimeUpdate`, `PlayerEngineKind`, `PlayerLike` | [AudioClipPlayer]() |
| `AudioPlaylistOptions`, `AudioPlaylistEvents`, `AudioPlaylistEntry`, `AudioPlaylistEntryStatus` | [AudioPlaylist]() |
| `AudioMixerOptions`, `AudioMixerEvents`, `MixerMemberInit`, `AudioMixerSnapshot`, `AudioMixerMemberSnapshot` | [AudioMixer]() |
| `AudioRecorderOptions`, `AudioRecorderEvents` | [AudioRecorder]() |
| `PlaybackEngine`, `BufferEngineOptions`, `MediaEngineOptions`, `MediaPlaybackAdapter`, `MediaPlaybackAdapterFactory` | [Engines and metering]() |
| `Effect`, `EffectNodes`, `GainOptions`, `FilterOptions`, `DelayOptions`, `CompressorOptions` | [Effect]() |
The source barrel is `packages/audio/src/play/headless/index.ts`. Stateful
transports own commands and lifetimes; lazy effect recipes and PCM helpers do not
acquire an event emitter or dispose method merely by sharing this entry.
### One clip, N at once, N in sequence
The three transports are the three cardinalities, and choosing between them is
usually the first decision:
| You have | Reach for |
| --- | --- |
| one clip | [`AudioClipPlayer`]() |
| several clips whose start/seek commands should be coordinated | [`AudioMixer`]() |
| several clips that should play **in order** | [`AudioPlaylist`]() |
Contexts can be created or explicitly borrowed. Created mixer/playlist members
reuse their owner's reference context, but independent players retain their own
clocks. Borrowed arbitrary PlayerLike objects require compatible time and resource
contracts; there is no fixed browser context-count promise.
## Analyze
`@webmusic/audio/analyze/headless` is the **stateful** half of audio analysis.
The capability root measures a clip once and returns the numbers; these two
objects keep something between calls — a cache of frames that survives an
edit, a poll loop over a live signal, or state across successive attack observations.
No DOM, no styles, no drawing: analysis produces facts, and
[audio/view](#view) projects signals. Import from
`@webmusic/audio/analyze/headless`.
| Export | Kind | Page |
| --- | --- | --- |
| `createAudioAnalysisSession` | session | [createAudioAnalysisSession]() |
| `createRealtimeAnalyzer` | analyzer | [createRealtimeAnalyzer]() |
| `createTransientDetector` | detector | [Live transient cues]() |
**Type ownership**
| Exported types | Explanation owner |
| --- | --- |
| `AudioAnalysisSession`, `AudioAnalysisSessionOptions` | [createAudioAnalysisSession]() |
| `RealtimeAnalyzer`, `RealtimeAnalyzerOptions`, `RealtimeFrame`, `RealtimeSource` | [createRealtimeAnalyzer]() |
| `TransientDetector`, `TransientInput`, `TransientObservation` | [Live transient cues]() |
The source barrel is `packages/audio/src/analyze/headless/index.ts`. Algorithm
option/result types referenced by a session are owned by the [Analyze API]().
The session owns cached results without a clock or event emitter; the realtime
analyzer owns an interval and has callback delivery plus `stop()`. The transient
detector retains preceding FFT/RMS evidence and provides `reset()`, but owns no
timer or graph. None exposes a generic dispose method.
The session analyses a **decoded clip** you already hold, and re-analyses it
with task-dependent reuse after an edit. The realtime analyzer never sees a clip: it borrows an
`AnalyserNode` and reports what is passing through it right now. Reach for the
session when you have the recording and the analyzer when you have the signal.
Pair the realtime analyzer's optional byte spectrum with the detector for
approximate live attack cues, not a beat grid or sample-accurate onset track.
## View
`@webmusic/audio/view/headless` is the **geometry** half of drawing audio:
where the viewport is, which columns fall inside it, what a click at *x*
means. None of it paints. The canvas renderers that do are `/render` adapters
on [`@webmusic/audio/view`](), by the same rule that keeps
every imperative surface out of a `/headless` entry.
Import from `@webmusic/audio/view/headless`. The models retain input data
and geometry; a live controller borrows an analyser. A player binding owns
subscriptions and returns unsubscribe(). None draws or creates a playback clock.
| Export | Kind | Page |
| --- | --- | --- |
| `AudioMeterController` · `createAudioMeterController` · `calculateLevelMeter` · `calculateSpectrumBars` | controller · factory · helpers | [Metering]() |
| `AudioTimeline` · `createAudioTimeline` | class · factory | [AudioTimeline]() |
| `bindPlayerToAudioTimeline` | binding | [AudioTimeline]() |
| `WaveformViewModel` · `createWaveformViewModel` · `fitWaveformColumns` | class · factory · helper | [View models]() |
| `SpectrogramViewModel` · `createSpectrogramViewModel` | class · factory | [View models]() |
| `computeClipPeaks` · `peaksDuration` | functions | [Peaks]() |
| `LiveViewController` · `createLiveViewController` · `LiveScrollBuffer` | classes · factory | [Live projection]() |
| `frequencyColumn` · `timeDomainColumn` | column builders | [Live projection]() |
**Type ownership**
| Exported types | Explanation owner |
| --- | --- |
| `AudioMeterControllerOptions`, `AudioMeterConfiguration`, `AudioMeterLevelFrame`, `LevelMeterFrame`, `LevelMeterOptions`, `SpectrumBarsOptions` | [Metering]() |
| `AudioTimelineOptions`, `AudioTimelineSnapshot`, `AudioTimelineHit`, `AudioTimelineGeometryUpdate`, `AudioTimelineSubscriber`, `AudioTimelineBinding`, `BindAudioTimelineOptions`, `PlayheadTarget` | [AudioTimeline]() |
| `WaveformViewModelOptions`, `WaveformColumn`, `WaveformChannelPeak`, `SpectrogramViewModelOptions`, `SpectrogramFrameView` | [View models]() |
| `LiveColumn`, `LiveProjectionType`, `LiveScrollOptions`, `LiveViewControllerOptions` | [Live projection]() |
The source barrel is `packages/audio/src/view/headless/index.ts`. AudioPeaks and
Region input types belong to the [Audio core API](); SpectrogramData
belongs to the [View API](). Pure functions such as computeClipPeaks
and fitWaveformColumns have no lifecycle/events. Model query results do not imply
an internal refresh timer.
`AudioTimeline` and the view models describe a **clip you already have**;
`LiveViewController` describes a signal arriving now. The split is the same
one the analyze capability makes, for the same reason: a clip has a length and
a live signal does not.
---
# AudioClipPlayer
> Single-clip transport over a buffer or a media element, with regions, a beat grid, looping and a metering tap.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/headless/play/audio-clip-player/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`AudioClipPlayer` plays one `AudioClip`. It picks an engine, owns the audio
graph between the source and the destination, and emits the events a transport
UI needs — `timeupdate` while playing, `end` at the finish, `regionenter` and
`regionleave` as the playhead crosses the clip's own annotations.
The input clip is bound at construction and exposed as the readonly `clip`
property. Reading it allocates no playback resources; followers borrow this same
immutable data alongside the player's position. There is no clip-replacement
command, so a new clip means a new player. That is why [`AudioPlaylist`]()
builds one player per entry rather than swapping a clip into a shared one.
It **owns** the `AudioContext` only when it created one. Pass `audioContext`
and the player borrows it — `dispose()` then tears down its own graph and
leaves the context open. Applications can share a context deliberately without
assuming a fixed browser-wide context limit.
## Import
```ts
import {AudioClipPlayer} from '@webmusic/audio/play/headless';
const player = new AudioClipPlayer(clip, {volume: 0.8});
await player.play();
player.seek(12.5);
// Retain the player until the owning session/view is removed.
const dispose = () => player.dispose();
```
## Two engines, one contract
`engine: 'auto'` selects BufferEngine for decoded clips up to ten minutes.
Otherwise it selects MediaEngine when a source URL is available, and falls back to
BufferEngine without one. Force an engine with `engine: 'buffer'` or `'media'`.
Only the buffer path honors absolute future starts; media playback delegates
buffering and memory policy to its host adapter rather than promising flat memory.
`preservesPitch` is a media-engine property. The buffer engine changes pitch
whenever it changes speed, because that is what varying a buffer's playback
rate does; the option is a documented no-op there rather than a silent one.
## Ownership and the tap
`analyser` exposes an `AnalyserNode` in the player's own graph. Meters and
realtime analyzers **borrow** it: a borrower must not disconnect or
reconfigure it, since every other reader of the same tap would see the change.
`clock` is present only once the buffer engine has built its transport clock,
which can happen during `play()`, `preload()` or access to `analyser`. The bridge
reads it live rather than snapshotting it. Reading `analyser` materializes the graph
and throws after disposal; it is not a resource-free inspection.
## Scratch sessions
`beginScratch()` optionally borrows a scratch session over the player's decoded
PCM. It captures the current playing/paused state and suspends ordinary playback.
It returns `undefined` without changing playback for a disposed player, a zero-duration clip or unavailable decoded samples. Callers must check that result; a legacy structural player may not
provide the method at all. A nonempty decoded clip supports this independently
of its normal buffer/media engine selection; media still needs its existing
`mediaAdapterFactory`.
An `AudioScratchSession` (type exported from `/play/headless` and `/play`) exposes:
| Member | Contract |
| --- | --- |
| `active` | Whether this session owns scratching, including release inertia. It stays active during the coast; a superseded, canceled or completed session is inactive. |
| `moveToSeconds(seconds, audible = true)` | Move to a finite absolute clip-second position, clamped/wrapped by the active engine. Successive signed position changes determine scratch direction/speed through the player's existing volume, pan and effect route. `false` moves silently. Calling this during a coast settles that completion and retakes the same session. |
| `end(resume = true)` | `Promise`; normal end coasts from the signed release speed toward the configured playback rate if previously playing, or zero if previously paused, then restores that state. It resolves after completion or cancellation, and rejects on coast/restoration failure. `false` cancels immediately without resume. |
The caller owns ending the session. Repeated `end(true)` during a coast awaits
its existing completion without restarting it; ending a stale/completed session
is a no-op. Retain the handle until completion so `end(false)` can still cancel.
The player owns its AudioContext, playback engine and shared route; ending a
gesture never disposes the player. External `play`, `pause`, `stop`, `seek`
(including `seekFraction` and legacy `scrub`) and disposal invalidate the session
and settle its pending coast. A newer session replaces it and inherits the
original playing intent, including when re-grabbing during release inertia.
Late readiness/end cannot resume obsolete work.
The current release transition lasts about **280 ms on the AudioContext clock**.
Both accepted position and scratch sound follow that signed velocity transition;
a previously paused source decelerates to rest, while a playing source converges
to its configured forward rate before ordinary playback resumes. A silent click
or release after motion has stopped restores directly, without a stale fling.
Context suspension cancels a pending coast. Volume/pan changes retain the session.
Rate/loop changes retain a held gesture, but finish an active coast at its current
accepted position and restore the prior state with the new configuration.
The backend retains native playback phase through bounded local PCM windows,
with smooth rate changes and short crossfades for direction changes, refills or
large position corrections. Pointer updates do not restart a separate audio grain.
Signed scratch speed is capped at 8 times sample speed; the audio-clock watchdog
brakes and silences stationary input. Reverse windows copy only local PCM and
never modify the original clip. Speed also changes pitch; this is not
pitch-preserving time stretching. Cancellation, external transport commands and
disposal stop scratch output immediately. With a range loop, pre-loop lead-in
remains available and windows respect the active loop edges; seamless scratch
loop wrapping is not promised. The existing `scrub(deltaSeconds)` is a separate
relative-seek convenience and does not start an audible scratch session.
[``]()
composes this capability with centered geometry and pointer lifecycle.
## Time, ownership and contract source
The player owns playback state on the clip-seconds axis. Its context is the audio
reference; it is not automatically the authority for every player on the page.
Views follow its notifications or reader. Under native looping, `seconds` wraps
while `clock.positionAt(t)` remains unwrapped. A seek before a buffer's scheduled
start retains that absolute start time and relocates the held position.
A `load` listener can pause or stop the pending start. Seeking from that listener
changes the start position without cancelling the play intent. Region events use
the engine's actual clamped/wrapped position. A transport command from a
`timeupdate`, region or beat listener invalidates the remaining notifications
from the interrupted observation batch; committed region membership prevents
seeking within the same region from recursively entering it again.
Range-loop options must have finite bounds and a positive span after clamping a
negative start to zero. Invalid ranges throw `RangeError`; rejected live edits
preserve the accepted loop. Buffer ranges must overlap the known clip duration.
For media, later metadata can leave no playable span: the watcher stops and the
stream finishes normally. See [engine range rules]().
The public barrel and `packages/audio/src/play/headless/player.ts` define the value, option, state and event
contracts described below. The reference does not turn private implementation
fields into public API.
API
**Construction**
| Member | Kind | Description |
| --- | --- | --- |
| `new AudioClipPlayer(clip, options?)` | constructor | Binds one immutable AudioClip; exposes it as readonly `.clip`. |
| `createAudioClipPlayer(clip, options?)` | factory | Returns a new AudioClipPlayer with the same ownership rules. |
**AudioClipPlayerOptions**
| Member | Kind | Default | Description |
| --- | --- | --- | --- |
| `audioContext` | option | created lazily | Borrowed if supplied; never closed by player disposal. |
| `destination` | option | context destination | Must belong to the selected context. |
| `engine` | option | `'auto'` | PlayerEngineKind: auto, buffer or media; selection described above. |
| `effect` | option | none | Lazy Effect recipe inserted in the player graph. |
| `mediaAdapterFactory` | option | none | Required host adapter when the media engine needs one. |
| `preservesPitch` | option | engine default | Media behavior; BufferEngine does not preserve pitch on rate changes. |
| `loop` | option | `false` | Boolean or finite `{start, end}` in clip seconds; ranges require a positive playable span. |
| `cursorIntervalMs` | option | `50` | Polling cadence in milliseconds; not audio scheduling precision. |
| `volume` | option | `1` | Initial gain. |
| `pan` | option | `0` | Initial stereo position. |
| `rate` | option | `1` | Initial playback multiplier. |
**Commands**
| Member | Kind | Description |
| --- | --- | --- |
| `play(when?)` | `Promise` | Starts/resumes; when is absolute AudioContext seconds for BufferEngine. Resolution means start preparation completed, not clip completion. |
| `pause()` | void | Holds position and invalidates a pending start. |
| `stop()` | void | Stops and returns to the start. |
| `seek(seconds)` | void | Clip-second position; clamped/wrapped by the selected engine. Buffer pre-roll retains its scheduled start. |
| `seekFraction(fraction)` | void | Clamps the fraction to 0–1 and seeks within duration. |
| `scrub(deltaSeconds)` | void | Seeks relative to current clip seconds; does not start audible scratching. |
| `beginScratch()` | AudioScratchSession or undefined | Begin the optional decoded-PCM gesture session described [above](#scratch-sessions); unsupported clips leave playback untouched. |
| `setRate(rate)` | void | Clamps to 0.25–4; pass finite numbers. |
| `setVolume(volume)` | void | Clamps below at zero; pass finite gain. |
| `setPan(pan)` | void | Clamps to −1…1; pass a finite value. |
| `setLoop(loop)` | void | Validates/stores before readiness and updates an existing engine; rejected ranges leave the accepted option intact. |
| `setPreservesPitch(flag)` | void | Stores before readiness; changes the media engine when active. |
| `preload()` | `Promise` | Builds/prepares the graph without starting sound or bypassing later resume/readiness requirements. |
**State**
| Member | Kind | Description |
| --- | --- | --- |
| `clip` | readonly AudioClip | Immutable constructor input; resource-free to read, remains available after disposal. |
| `seconds` | number | Engine clip position; 0 before an engine exists. |
| `duration` | number | Engine duration or input clip duration, in seconds. |
| `progress` | number | seconds / duration, or 0 for zero duration. |
| `playing` | boolean | Ordinary engine running state; false before readiness and while a scratch session suspends it. |
| `scratching` | readonly boolean | Whether an exclusive scratch session currently owns audible playback, including readiness and release inertia. |
| `clock` | TransportClockReader or undefined | Buffer engine only, after graph creation; unwrapped under native looping. |
| `analyser` | AnalyserNode | Materializes and exposes the player-owned tap; borrowers must not disconnect it. |
**Events and subscriptions**
| Member | Kind | Description |
| --- | --- | --- |
| `on(event, listener)` | unsubscribe function | Typed AudioClipPlayerEvents subscription. |
| `once(event, listener)` | unsubscribe function | One delivery. |
| `off(event, listener)` | void | Remove the matching listener. |
| `timeupdate` | PlayerTimeUpdate | `{seconds, duration, progress}` at cursor polling cadence and applicable transport changes. |
| `end` | AudioClip | Natural end of the input clip. |
| `regionenter` | Region | Entered a clip annotation. |
| `regionleave` | Region | Left a clip annotation. |
| `beat` | object | `{index, seconds}` from BeatGrid crossings. |
| `load` | object | `{duration}` when graph setup completes. |
| `error` | Error | Playback failure; await operations to handle their Promise failures too. |
**Lifecycle and related types**
| Member | Kind | Description |
| --- | --- | --- |
| `dispose()` | void | Idempotent; invalidates pending work, stops timers, releases graph/listeners, and closes only its own context. Release failures may throw after cleanup attempts. |
| `PlayerEngineKind` | type | `'auto'`, `'buffer'` or `'media'`. |
| `PlayerTimeUpdate` | type | Numeric seconds, duration and progress fields. |
| `AudioClipPlayerEvents` | type | Payload map listed above. |
| `PlayerLike` | type | Shared structural control contract; optional capabilities must be checked before use. |
| `AudioScratchSession` | type | Optional scratch-session active state, movement and asynchronous end contract; see [Scratch sessions](#scratch-sessions). |
**Composed by.** [``]()
— Audio / Play.
## Related
[Load/decode API]() · [Engines and metering]() · [Synchronized playback]()
---
# AudioMixer
> Coordinates clip/player starts and per-member volume, solo, mute and effect construction.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/headless/play/audio-mixer/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`AudioMixer` coordinates several clips or structural players. Players it creates
share a context and receive a common future start after preparation; they still
retain their own engine clocks. Borrowed players must use a compatible reference
and support scheduled starts for that alignment to hold. The mixer does not
publish a TransportClock or run a Bridge-style drift monitor.
A member is either a clip the mixer wraps in a player, or a `PlayerLike` you
already own. The mixer **owns what it created and borrows what you passed**:
`dispose()` disposes the players it built and leaves yours alone.
Use `.transport` with the central [AudioPlayer]():
`player.setTransport(mixer.transport)`. The adapter exposes group commands,
seconds/duration/playing and subscriptions without claiming a single clip or
aggregate analyser. The facade borrows it; the caller still disposes the mixer.
## Import
```ts
import {AudioMixer} from '@webmusic/audio/play/headless';
const mixer = new AudioMixer({masterVolume: 0.9});
mixer.add({id: 'drums', clip: drums});
mixer.add({id: 'bass', clip: bass, volume: 0.7});
await mixer.play();
mixer.solo('drums');
// Keep the mixer while the session is active.
const dispose = () => mixer.dispose();
```
## Solo is exclusive
`solo(id)` mutes every other member, so an A/B comparison needs no second
object. `solo(null)` clears the solo selection. Clearing the solo restores each
member's own mute state. Removing the selected solo member also clears solo and
recomputes the remaining gains.
## One authoritative mix state
`snapshot()` returns a detached, deeply frozen `AudioMixerSnapshot`: master gain,
solo selection and each member's requested gain, mute, solo and effective gain.
The mixer owns this state; Elements and custom presenters read it instead of
maintaining separate channel values. `on('change', listener)` invalidates a
presenter's view after membership or mix changes; call `snapshot()` inside the
listener to read the latest state, including after a reentrant command. Removing
the subscription releases only that observer.
`effectiveVolume` is the requested linear multiplier sent to a member's optional
`setVolume`; it is not a measurement of device output. A structural player
without `setVolume` cannot apply this mix. Commands directly on a borrowed player
also bypass the mixer's requested state; use mixer commands for shared controls.
## Errors from two levels
Members emit their own failures, and the mixer emits `error` for its own
transport work — notably a seek-restart that fails partway. An element that
forwards only `end` and `memberEnd` reports a mixer stuck mid-seek as silence;
forward `error` as well.
If one member fails during a group start, the mixer pauses every participant
that started and retracts any later asynchronous start before rejecting the
group command. A later `play()` call can retry the group.
## Time, ownership and contract source
The mixer issues commands; it is not a sampled position model. Start/seek
alignment is best-effort for arbitrary PlayerLike implementations, and borrowing a
player does not reroute that player into another context or audio graph.
During a running group, seeking before a naturally ended member's end position
rejoins that member with the still-playing members. Independently paused members
stay paused. Duration is the end boundary when available, with the member's
reported position as fallback; a structural player exposing neither cannot
provide an end bound. Pausing/stopping the group or natural group completion
clears its running intent, so a later seek alone does not start it. A restarted
member can report completion again for the new pass.
The public barrel and `packages/audio/src/play/headless/mixer.ts` define the value, option, state and event
contracts described below. The reference does not turn private implementation
fields into public API.
API
**Construction**
| Member | Kind | Description |
| --- | --- | --- |
| `new AudioMixer(options?)` | constructor | Creates a coordinator; context is allocated lazily unless supplied. |
| `createAudioMixer(options?)` | factory | Returns an AudioMixer. |
**AudioMixerOptions**
| Member | Kind | Default | Description |
| --- | --- | --- | --- |
| `audioContext` | option | lazy creation | Borrowed when supplied; used by players constructed from clips. |
| `masterVolume` | option | `1` | Applied with each member gain through its optional setVolume. |
| `destination` | option | context destination | Destination for players created by the mixer. |
| `mediaAdapterFactory` | option | none | Host streaming adapter for created players. |
**MixerMemberInit**
| Member | Kind | Description |
| --- | --- | --- |
| `id` | string | Unique member key; a valid new member replaces the old one after its player and end subscription are prepared. Validation or subscription failure leaves the old member active; after a successful commit, cleanup failure does not restore the old member. |
| `clip` | AudioClip, optional | Construct an owned AudioClipPlayer when no player is supplied. |
| `player` | PlayerLike, optional | Borrow an existing player; takes precedence over clip. |
| `effect` | Effect, optional | Used only when constructing a player from a clip. |
| `volume` | number, default 1 | Member gain. There is no muted construction option. |
**Commands**
| Member | Kind | Description |
| --- | --- | --- |
| `add(init)` | this | Requires clip or player; subscribes to member end. |
| `remove(id)` | void | Detaches the member, clears its solo if selected, reapplies gains and releases owned resources. Attempts all cleanup before rethrowing the first synchronous failure. |
| `play()` | `Promise` | Prepares built-in members and proposes a shared start; does not await track completion. |
| `pause()` | void | Pauses members and invalidates pending start intent. |
| `stop()` | void | Stops members and clears ended tracking. |
| `seek(seconds)` | void | Seeks in clip seconds; running members and eligible naturally ended stems rejoin on a future origin. Independently paused members stay paused. Async restart failures emit error. |
| `setVolume(id, volume)` | void | Sets one member linear gain; finite values clamp below at zero, non-finite values use 1. |
| `setMasterVolume(volume)` | void | Sets the multiplier applied to all members, using the same normalization as member gain. |
| `mute(id)` | void | Mutes a member. |
| `unmute(id)` | void | Clears that member mute. |
| `solo(id)` | void | Exclusive solo; null clears it without clearing individual mute flags. |
**State**
| Member | Kind | Description |
| --- | --- | --- |
| `ids()` | string[] | Snapshot of current member identifiers. |
| `snapshot()` | AudioMixerSnapshot | Detached frozen mix state; it does not expose player objects or a group clock. Retain member references for playback readouts. |
**Snapshot fields**
| Member | Owner | Description |
| --- | --- | --- |
| `masterVolume` | AudioMixerSnapshot | Requested master linear gain. |
| `soloId` | AudioMixerSnapshot | Selected exclusive solo id or null. |
| `members` | AudioMixerSnapshot | Frozen array of frozen AudioMixerMemberSnapshot entries. |
| `id` / `volume` | AudioMixerMemberSnapshot | Member key and requested channel linear gain. |
| `muted` / `solo` | AudioMixerMemberSnapshot | Member mute flag and whether it is the selected solo. |
| `effectiveVolume` | AudioMixerMemberSnapshot | Zero for mute/solo exclusion; otherwise volume × masterVolume. |
**Events**
| Member | Kind | Description |
| --- | --- | --- |
| `on(event, listener)` | unsubscribe function | AudioMixerEvents subscription. |
| `once(event, listener)` | unsubscribe function | One delivery. |
| `off(event, listener)` | void | Remove listener. |
| `change` | void payload | Membership or mix changed; reread snapshot. Listener failures are routed to error without blocking later readers. |
| `end` | void payload | All tracked members have ended. |
| `memberEnd` | object | `{id}` for one completed member. |
| `error` | Error | Mixer-level failure, including asynchronous seek restart. Member errors are separate subscriptions. |
**Lifecycle and types**
| Member | Kind | Description |
| --- | --- | --- |
| `dispose()` | void | Idempotent; detaches ownership first, attempts every subscription/player cleanup and owned-context close even after a failure, then rethrows the first synchronous cleanup failure. Borrowed players/context remain caller-owned. |
| `AudioMixerOptions` | type | Coordinator options above. |
| `MixerMemberInit` | type | Member construction inputs above. |
| `AudioMixerEvents` | type | change, end, memberEnd and error payload map. |
| `AudioMixerSnapshot` / `AudioMixerMemberSnapshot` | types | Readonly mix and member snapshots described above. |
**Composed by.** [``]() — Audio /
Play, over [`mountMixer`]().
## Related
[AudioClipPlayer]() · [Effects]() · [Bridge synchronization]()
---
# AudioPlayer
> A stable playback owner over a replaceable clip or borrowed queue/mix transport.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/headless/play/audio-player/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`AudioPlayer` is the central Audio command surface. It retains one identity while
its clip changes, or delegates to an existing queue/mix backend. It creates no
second transport clock. Use [`AudioClipPlayer`]()
when an immutable single-clip engine is all the application needs.
The demo constructs one nonvisual player and controls it with plain inputs. Load
the Arabesque No.1 WAV, switch between a clip and a two-pass queue, or clear and reselect the
source. Its readouts follow the object's events; the demo owns decoding and queue
cleanup, while the facade owns its context and clip engine.
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/audio/headless/play/audio-player/)
## Import
```ts
import {AudioPlayer, AudioPlaylist, AudioMixer} from '@webmusic/audio/play/headless';
import {loadClipFromUrl} from '@webmusic/audio/play';
const player = new AudioPlayer();
const clip = await loadClipFromUrl('/wav/Arabesque%20No.1.wav', {context: player.audioContext});
player.setClip(clip);
await player.play();
player.pause();
const queue = new AudioPlaylist({
audioContext: player.audioContext, entries: [{id: 'take', clip}],
});
player.setTransport(queue); // borrow the queue without disposing it
await player.play();
const mix = new AudioMixer({audioContext: player.audioContext});
mix.add({id: 'stem', clip});
player.setTransport(mix.transport);
await player.play();
const dispose = () => {
player.setTransport(undefined); // detach the borrowed backend
queue.dispose();
mix.dispose();
player.dispose(); // release its owned context after the borrowers
};
```
## Ownership and source selection
`setClip` selects an owned low-level clip engine, stopped at zero. The engine is
created lazily. Replacing it releases only that owned engine, preserving facade
identity and its output tap/context. Assigning the same selected clip is a no-op.
`setTransport` selects a borrowed backend and preserves its state. Selecting a
new nonempty clip or transport pauses the previous backend first, preserving its
position and preventing hidden overlap. Clearing a connection only detaches it. The facade
never stops or disposes a borrowed backend merely because it is detached.
The backend supplies all position/state readback. A queue uses seconds within its
active entry and exposes that clip. A mix uses its group axis and has no single
clip or aggregate analyser. Unsupported analyser/clock/scratch capabilities are absent; unsupported live control setters throw rather than silently succeeding.
Sharing this facade does not complete shared-clock injection across mix members.
API
### Construction and selection
| Member | Contract |
| --- | --- |
| `new AudioPlayer(clip?, options?)` / `createAudioPlayer(clip?, options?)` | Empty by default. `AudioPlayerOptions` uses the [clip engine options](): context/destination, engine, media adapter, rate, volume, pan, loop, pitch preservation and effects. |
| `setClip(clip \| undefined)` | Replace the selected source; undefined clears it. Source replacement invalidates the old owned playback and scratch work. |
| `setTransport(transport \| undefined)` | Borrow an `AudioPlayerTransport`; undefined detaches the selected borrowed backend. Direct and indirect facade borrowing cycles are rejected. |
| `clip` / `transport` | Readonly selected clip or borrowed transport; a mix returns no clip. |
### Commands and state
| Member | Contract |
| --- | --- |
| `play(when?)` | Promise resolves after start/preparation, not at natural completion. `when` is absolute AudioContext seconds, supported according to the selected backend. Empty/disposed instances do nothing. |
| `pause()` / `stop()` | Delegate to the current backend. Pause retains position; stop resets according to that backend. |
| `seek(seconds)` / `seekFraction(fraction)` / `scrub(deltaSeconds)` | Delegate seeks on the selected content axis. Fraction clamps to 0–1; scrub is a relative seek, not scratch audio. |
| `preload()` | Prepare the selected backend without starting sound, when supported. |
| `setVolume(value)` / `setRate(value)` / `setPan(value)` | Set clip controls; rate clamps to 0.25–4, pan to −1–1, volume is nonnegative. Nonfinite input throws. A selected borrowed backend must expose the requested setter or the command throws without committing a new retained setting. |
| `setLoop(loop)` / `setPreservesPitch(value)` | Clip configuration. Queue loop belongs to the queue's list policy; do not infer group loop/pan/pitch support. |
| `beginScratch()` / `scratching` | Borrow the selected backend's optional scratch session. See [scratch sessions](). |
| `seconds` / `currentTime` / `duration` / `progress` | Live position, duration and fraction; zero for empty input. |
| `playing` | Selected backend's playback state. |
| `volume` / `rate` / `pan` | Facade's retained clip settings; a backend can expose narrower control support. |
| `audioContext` | Lazily creates or returns the borrowed AudioContext for shared decoding/owned playback. Throws after disposal. A borrowed group may own a different context; sharing must be configured explicitly. |
| `clock` | Selected engine's optional read-only transport clock; the facade owns no clock. |
| `analyser` | Stable owned clip-output analyser or a borrowed backend's optional analyser. Empty input does not create a graph. |
### Events and lifecycle
`on`, `once` and `off` use `AudioPlayerEvents`; `on`/`once` return an unsubscribe.
`sourcechange` carries `{clip, revision}`, `statechange` carries `{playing}`, and
`timeupdate` carries `{seconds, duration, progress}`. Notifications are synchronous;
read current state again after reentrant callbacks. Source observation does not
start playback. `load` carries `{duration}`; `end` carries the clip or undefined
for a backend without one. `regionenter`, `regionleave` and `beat` forward clip
annotations. `error` carries an Error from the selected backend.
`dispose()` is terminal and idempotent. It releases subscriptions, observation,
owned engine/output and only an internally created context. A supplied context
and borrowed transport remain caller-owned. Dispose owners separately in their
application lifetime. Cleanup must not reclaim a source selected by a callback.
`AudioPlayerTransport` requires seconds/duration/playing, play/pause/stop/seek.
Optional clip/currentClip, analyser, clock, configuration, preload/scratch and
subscribe/onEnd/onError ports expose backend capabilities without DOM. An
`AudioPlaylist` implements this contract; use `AudioMixer.transport` for a mix.
## Related
[``]() ·
[AudioClipPlayer]() ·
[AudioPlaylist]() ·
[AudioMixer]() ·
[AudioMeterController]()
---
# AudioPlaylist
> Ordered entries played in sequence on one shared context, prefetching the next while the current one plays.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/headless/play/audio-playlist/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`AudioPlaylist` is the third cardinality: one clip is a
[player](), N at once is a
[mixer](), N in order is this. It owns an
ordered list of entries, advances when one ends, and resolves the next
entry's clip while the current one is still playing.
It creates or borrows one context and constructs an owned player for the selected
entry, releasing the previous player on replacement. Each player keeps its input
clip privately; the queue does not swap clips into one shared player. A supplied
context remains caller-owned.
An AudioPlaylist can be borrowed by the central
[AudioPlayer]() through
`player.setTransport(queue)`. `subscribe`, `onEnd` and `onError` provide the
structural connection; `clip` aliases the current resolved clip, and `clock`
reads the active entry's clock when available. The queue keeps ownership of its
entry engines and context; detaching the facade does not dispose it.
## Import
```ts
import {AudioPlaylist} from '@webmusic/audio/play/headless';
const playlist = new AudioPlaylist({
entries: [
{id: 'a', label: 'Take 1', clip: firstTake},
{id: 'b', label: 'Take 2', clip: decoded},
],
loop: true,
});
await playlist.play();
// Keep the playlist until the owning view/session is removed.
const dispose = () => playlist.dispose();
```
## Loading is injected
`headless` may not import `play/api`, so the playlist does not know how to
fetch or decode: the caller supplies a loader, exactly as the media engine
takes its adapter factory. The element passes `loadClipFromUrl`. An entry with
a ready `clip` needs no loader at all.
`prefetch` (default on) covers **fetch and decode** for the next entry — the
dominant cost — but not its audio graph, which is still built when that entry
starts.
## List loop, not track loop
When failed entries are skipped, one play request attempts at most one pass
through the queue. If no entry loads, playback stops even with `loop: true`;
call `play()` again to retry after resolving the load error. Concurrent play
requests share the same pending start.
`loop` restarts the list after the last entry, and it must live here rather
than on the players: a looping *player* never emits `end`, so forwarding the
setting downward would stall the queue forever. This is also why the events
are distinct names — `trackchange` and `playlistend` rather than an overloaded
`end`, which already means "one clip finished".
## Live queue policies and selection
`setLoop`, `setPrefetch` and `setSkipFailed` update queue policy in place. They
preserve the current selection, player, position, playback intent and resolved
clips. Enabling prefetch while playing starts the next missing load; disabling it
prevents future prefetches but does not cancel a load already in flight. Its result
may still enter the cache. The latest skip-failed policy applies when a pending
load fails; with skipping disabled the queue stops at that entry.
`select(idOrIndex)` requires an existing id or an integer index in range. Invalid
selection rejects with `RangeError` before changing the active entry, position or
pending work. Previous/next keep their separate endpoint and loop behavior.
## Time, ownership and contract source
The playlist owns selection and list progression. Clip positions reset on entry
changes; it does not expose a continuous session clock across the entire queue.
If the selected entry has no player yet, `seek` and `seekFraction` retain the
requested position and apply it when that entry starts. A later selection or
`stop()` clears that pending position.
Prefetching uses the injected loader and does not imply sample-accurate gapless
transitions.
The public barrel and `packages/audio/src/play/headless/playlist.ts` define the value, option, state and event
contracts described below. The reference does not turn private implementation
fields into public API.
API
**Construction**
| Member | Kind | Description |
| --- | --- | --- |
| `new AudioPlaylist(options?)` | constructor | Constructs an ordered queue. |
| `createAudioPlaylist(options?)` | factory | Returns an AudioPlaylist with identical lifetime semantics. |
**AudioPlaylistOptions**
| Member | Kind | Default | Description |
| --- | --- | --- | --- |
| `entries` | option | `[]` | Readonly AudioPlaylistEntry inputs. |
| `audioContext` | option | lazy creation | Shared by created entry players and supplied to loadEntry. |
| `destination` | option | context destination | Routing for created players. |
| `loop` | option | `false` | Repeat the list, not each clip. |
| `prefetch` | option | `true` | Resolve the next clip early. |
| `skipFailed` | option | `true` | Attempt later entries after failure; one pass per start request. |
| `volume` | option | `1` | Initial entry-player volume. |
| `rate` | option | `1` | Initial entry-player rate. |
| `engine` | option | `'auto'` | PlayerEngineKind for created players. |
| `mediaAdapterFactory` | option | none | Host adapter for streaming entries. |
| `loadEntry(entry, context)` | option | none | `Promise`; required to resolve src entries, not called for ready clips. |
**AudioPlaylistEntry**
| Member | Kind | Description |
| --- | --- | --- |
| `id` | string | Required unique identifier. |
| `label` | optional string | Display label. |
| `clip` | optional AudioClip | Ready material; avoids the loader. |
| `src` | optional string | Source understood by the injected loader. |
| `streaming` | optional boolean | Loader hint to retain URL-based media. |
| `format` | optional AudioFormat | Explicit format hint passed to the loader. |
**Commands**
| Member | Kind | Description |
| --- | --- | --- |
| `setEntries(entries)` | void | Reconciles by id/source; changed current material replaces its player, removal stops it. |
| `play()` | `Promise` | Starts/resumes selected entry; concurrent starts share pending work, resolution is not playlist completion. |
| `pause()` | void | Holds current playback. |
| `stop()` | void | Stops current playback. |
| `toggle()` | `Promise` | Pause or play according to current state. |
| `next()` | `Promise` | Select next entry. |
| `previous()` | `Promise` | Select previous entry. |
| `select(idOrIndex)` | `Promise` | Select by existing id or integer index; invalid inputs reject with RangeError without changing state. |
| `seek(seconds)` | void | Seek within current entry, in seconds. |
| `seekFraction(fraction)` | void | Seek within current entry by normalized fraction. |
| `setVolume(volume)` | void | Retain/apply entry-player volume. |
| `setRate(rate)` | void | Retain/apply entry-player rate. |
| `setLoop(loop)` | void | Change list-loop policy without resetting selection or playback. |
| `setPrefetch(prefetch)` | void | Change future prefetch policy; enabling while playing may begin the next load. In-flight loads can still finish and cache. |
| `setSkipFailed(skipFailed)` | void | Choose whether the next load failure advances; applies to pending loads too. |
**State**
| Member | Kind | Description |
| --- | --- | --- |
| `entries` | readonly AudioPlaylistEntry[] | Current queue. |
| `index` | number | Selected entry index; may be −1 without selection. |
| `current` | entry or undefined | Selected queue entry. |
| `statusOf(id)` | AudioPlaylistEntryStatus | idle, loading, ready or error. |
| `clipOf(id)` | AudioClip or undefined | Resolved clip for an entry. |
| `currentClip` | AudioClip or undefined | Resolved current material. |
| `activePlayer` | AudioClipPlayer or undefined | Current player; caller may inspect but must not dispose it. |
| `playing` | boolean | Current playback state. |
| `seconds` | number | Position in current clip, not total queue elapsed time. |
| `duration` | number | Current clip duration. |
| `progress` | number | Current clip progress. |
**Events**
| Member | Kind | Description |
| --- | --- | --- |
| `on(event, listener)` | unsubscribe function | Typed subscription; this class has no once method. |
| `off(event, listener)` | void | Remove the matching listener. |
| `trackchange` | object | `{id, index, entry}` when current selection changes. |
| `trackend` | object | `{id, index}` at natural end before advancement. |
| `playlistend` | void payload | Final entry ends without list looping. |
| `timeupdate` | object | `{seconds, duration, progress}` for the active entry. |
| `statuschange` | object | `{id, status}` when load state changes. |
| `error` | Error | Entry loading or playback failure. |
**Lifecycle and types**
| Member | Kind | Description |
| --- | --- | --- |
| `dispose()` | void | Idempotently invalidates work, disposes its current player/listeners and closes only its own context. |
| `AudioPlaylistOptions` | type | Construction options above. |
| `AudioPlaylistEntry` | type | Queue input above. |
| `AudioPlaylistEntryStatus` | type | Four load states above. |
| `AudioPlaylistEvents` | type | Event payload map above. |
**Composed by.** [``]() —
Audio / Play, which supplies `loadClipFromUrl` as the loader and composes
[`mountPlaylist`]().
## Related
[AudioClipPlayer]() · [Loader API]() · [Playlist presenter]()
---
# AudioRecorder
> Microphone capture into an AudioClip, with a live level, a device selection and hard retention caps.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/headless/play/audio-recorder/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`AudioRecorder` captures from an input device and hands back an `AudioClip` —
the same immutable type every other object in the family consumes, so a take
can be played, analysed or exported without conversion.
It **owns** the media stream it opens and the buffers it accumulates;
`dispose()` releases both and stops the device. A take that has already been
emitted through `stop` belongs to the caller and survives disposal.
## Import
```ts
import {AudioRecorder} from '@webmusic/audio/play/headless';
const recorder = new AudioRecorder({echoCancellation: false});
recorder.on('level', (rms) => paintMeter(rms));
recorder.on('stop', (clip) => keep(clip));
await recorder.start();
// … later
const clip = await recorder.stop(); // Promise resolves to the captured take
recorder.dispose();
```
## The sample rate is not known until start
`maxRecordedFrames` is a frame count, so converting a duration into one needs
the rate — and the real context's rate is not known until `start()` opens the
device. A supplied `sampleRate` requests the context rate; retained frames and the emitted
clip use the context's actual sample rate. Compute duration from that rate rather
than assuming a requested rate was honored.
Both caps are hard: capture fails before retaining a block that exceeds
`maxRecordedFrames` per channel or `maxRecordedBytes` across channels, whichever
comes first. The unfinished take is discarded, `error` is emitted, and a later
`stop()` rejects; a cap is not a timed stop that returns a shorter clip. The byte
cap counts retained Float32 PCM, including channel padding, rather than the
transient allocations used to assemble a completed clip.
## Capture backend and measurement
The current backend uses a `ScriptProcessorNode` with 4096-frame callbacks and
fixed two-channel input/output. The browser upmixes mono input to stereo or
mixes additional device channels into that stereo input; the returned clip does
not preserve the device's original channel layout. No AudioWorklet backend is
implemented here.
`stop()` returns only completed callback blocks. It has no final partial-block
flush and does not promise a sample-accurate stop boundary; 4096 frames span
about 85 ms at 48 kHz, before main-thread scheduling delay. There is no pause
command. `dispose()` cancels opening/capture and discards unfinished buffers.
The live `level` is linear RMS over retained channels:
`sqrt(sum(sample²) / (frames × channels))`. Channel energy is combined after
squaring, so opposite phases do not cancel. It is not dBFS, LUFS or true peak.
A context must reach `running` before `start()` resolves and emits `start`;
permission and resume failures release the acquired device and context.
## Selecting a device
`deviceId` takes a
`MediaDeviceInfo.deviceId`. Enumerating devices is the caller's job — the
recorder does not open a picker, and there is no element for that yet.
## Time, ownership and contract source
This is a capture owner, not a session TransportClock driver. Its stream and
capture context are created by the recorder; the options do not accept a borrowed
context. A completed clip survives teardown. Input permission and device support
remain browser responsibilities.
The public barrel and `packages/audio/src/play/headless/recorder.ts` define the value, option, state and event
contracts described below. The reference does not turn private implementation
fields into public API.
API
**Construction**
| Member | Kind | Description |
| --- | --- | --- |
| `new AudioRecorder(options?)` | constructor | No permission request until start. |
| `createAudioRecorder(options?)` | factory | Returns an AudioRecorder. |
**AudioRecorderOptions**
| Member | Kind | Default | Description |
| --- | --- | --- | --- |
| `deviceId` | option | system default | Input device identifier for getUserMedia. |
| `echoCancellation` | option | `true` | Requested audio constraint. |
| `sampleRate` | option | context-selected | Requested AudioContext rate; output uses actual context.sampleRate. |
| `maxRecordedFrames` | option | `30_000_000` | Maximum retained frames per channel. |
| `maxRecordedBytes` | option | 256 MiB | Maximum retained Float32 PCM bytes across channels. |
**Commands**
| Member | Kind | Description |
| --- | --- | --- |
| `start()` | `Promise` | Opens device/context, resumes it when needed and resolves once capture is ready. Rejects unavailable-device, permission, setup, resume or cancellation failures. Concurrent pending calls share one request. |
| `stop()` | `Promise` | Assembles and returns the take, emits stop and releases capture resources. Rejects when not recording or after a capture failure. |
**State**
| Member | Kind | Description |
| --- | --- | --- |
| `isRecording` | boolean | Whether recording is active; there is no recording getter. |
| `level` | number | Latest linear RMS across retained channels, initially zero. |
| `inputAnalyser` | AnalyserNode or undefined | Borrowed read access to the live tap during capture; unavailable before start/after cleanup. |
**Events**
| Member | Kind | Description |
| --- | --- | --- |
| `on(event, listener)` | unsubscribe function | Typed AudioRecorderEvents listener. |
| `once(event, listener)` | unsubscribe function | One delivery. |
| `off(event, listener)` | void | Remove matching listener. |
| `start` | void payload | Capture became active. |
| `stop` | AudioClip | Completed take, also returned by stop(). |
| `level` | number | Latest input RMS across retained channels. |
| `error` | Error | Capture processing, retention-cap or clip assembly failure. Device/permission/setup/resume failures reject `start()` without emitting this event; handle rejected commands as well. |
**Lifecycle and types**
| Member | Kind | Description |
| --- | --- | --- |
| `dispose()` | void | Idempotently cancels pending setup, stops owned tracks/context, removes listeners and discards unfinished buffers; does not emit a completed take. |
| `AudioRecorderOptions` | type | Construction options above. |
| `AudioRecorderEvents` | type | Payload map above. |
**Composed by.** [``]()
— Audio / Play, over [`mountRecorder`]().
## Related
[AudioClipPlayer]() · [Export API]() · [Live projection]()
---
# Effect
> The effect builders and the two graph helpers: describe a chain as data, then let a player or the exporter wire it.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/headless/play/effects/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
An `Effect` here is a **description**, not a node: `gain(0.5)` or
`filter({type: 'lowpass', frequency: 800})` returns a plain value that knows
how to build itself against a context. The player, the mixer and the offline
exporter all take one, which is what lets the same chain apply live and during
a render.
The builders own nothing. `chainEffects` only composes descriptions into one
`Effect`; nodes exist once something builds it, and `insertEffect` — the one
helper that does build — returns the disposer to whoever called it.
## Import
```ts
import {AudioClipPlayer, chainEffects, delay, filter, gain} from '@webmusic/audio/play/headless';
const player = new AudioClipPlayer(clip, {
effect: chainEffects(
filter({type: 'lowpass', frequency: 1200}),
delay({delaySeconds: 0.25, feedback: 0.3}),
gain(0.8),
),
});
const dispose = () => player.dispose(); // when the owning session is removed
```
## Live parameters
An individual built effect exposes `EffectNodes`, whose optional `params` map carries the
live `AudioParam`s — `frequency`, `wet`, `feedback` — for a knob to drive.
A built chain deliberately exposes only `{input, output, dispose}`: it does
**not** forward the params of the effects it composed. A UI that needs them
must capture each `createAudioNodes` result itself and recapture after a
rebuild, rather than expecting the chain to surface them.
`customEffect` accepts a factory or an existing EffectNodes pair. Prefer a factory
for reusable recipes; an existing pair belongs to one context/graph and should not
be concurrently installed elsewhere.
## Where a chain attaches
`effect` is an AudioClipPlayer construction option and a per-member MixerMemberInit
option; AudioMixer has no master effect option or live setEffect command. Replacing
these requires an explicit player/member replacement with caller-managed position
and ownership. No effect recipe owns or moves a transport clock.
`insertEffect(context, source, destination, effect)` is the low-level form:
it wires `source → [effect] → destination` and returns the effective output
node with an optional disposer. Both the
player graph and offline exporter use the same structural contract. Custom effects
remain responsible for supporting the supplied realtime/offline context and for
releasing any internal nodes they create.
## Time, ownership and contract source
Recipes are reusable construction values. Realized nodes have context-specific
identity; parameter references must be reacquired after rebuilding. Low-level
chain/insert helpers do not provide a general transactional graph-replacement
protocol, so callers must handle construction failures and their owned routing.
The public barrel and `packages/audio/src/play/headless/effects.ts, play/core/effect.ts and platform/kernel/src/effect.ts` define the value, option, state and event
contracts described below. The reference does not turn private implementation
fields into public API.
API
**Lazy builders**
| Member | Kind | Description |
| --- | --- | --- |
| `gain(valueOrOptions?)` | Effect | Number or GainOptions; gain defaults to 1. Built params: gain. |
| `filter(options?)` | Effect | FilterOptions; params: frequency, Q and gain. |
| `delay(options?)` | Effect | DelayOptions; params: time, feedback and wet. |
| `compressor(options?)` | Effect | CompressorOptions; params: threshold and ratio. |
| `customEffect(factoryOrNodes, label?)` | Effect | Factory `(BaseAudioContext) => EffectNodes` or existing nodes; optional label. |
**Builder option types**
| Member | Kind | Default | Description |
| --- | --- | --- | --- |
| `GainOptions.value` | number | `1` | Gain multiplier. |
| `FilterOptions.type` | BiquadFilterType | `'lowpass'` | Biquad type. |
| `FilterOptions.frequency` | number | `1000` | Hz. |
| `FilterOptions.Q` | number | `1` | Filter Q. |
| `FilterOptions.gain` | number | node default | dB for filter types using gain. |
| `DelayOptions.delaySeconds` | number | `0.25` | Delay seconds. |
| `DelayOptions.feedback` | number | `0.3` | Feedback gain. |
| `DelayOptions.wet` | number | `0.3` | Wet gain. |
| `DelayOptions.dry` | number | `1` | Dry gain. |
| `CompressorOptions.threshold` | number | node default | Threshold dB. |
| `CompressorOptions.knee` | number | node default | Knee dB. |
| `CompressorOptions.ratio` | number | node default | Compression ratio. |
| `CompressorOptions.attack` | number | node default | Attack seconds. |
| `CompressorOptions.release` | number | node default | Release seconds. |
**Graph helpers**
| Member | Kind | Description |
| --- | --- | --- |
| `chainEffects(...effects)` | Effect | Skips null/undefined; lazily connects stages. Empty chain creates a gain pass-through. It does not merge params maps. |
| `insertEffect(context, source, destination, effect)` | object | Connects source → effect → destination, or direct when effect is undefined; returns `{output, dispose?}`. |
**Effect / EffectNodes contract and lifetime**
| Member | Kind | Description |
| --- | --- | --- |
| `Effect.label` | optional string | Recipe label. |
| `Effect.createAudioNodes(context)` | EffectNodes | Builds on a BaseAudioContext; each call normally creates a fresh graph. |
| `EffectNodes.input` | readonly AudioNode | Signal input. |
| `EffectNodes.output` | readonly AudioNode | Signal output. |
| `EffectNodes.params` | optional `Record` | Only explicitly exposed live parameters. |
| `EffectNodes.dispose()` | optional void method | Custom release of internal resources. Callers must also manage the edges they establish. |
| `GainOptions`, `FilterOptions`, `DelayOptions`, `CompressorOptions` | types | Own every builder setting listed above; builders install no timer, events or transport state. |
**Composed by.** No element exposes an effect rack yet; the chain reaches
elements through the player effect property and mixer member effect definitions used by
[``]() and
[``]().
## Related
[AudioClipPlayer]() · [AudioMixer]() · [Offline export]()
---
# BufferEngine and MediaEngine
> Buffer and host-adapted media engines for low-level audio transport.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/headless/play/engines/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
Audio Play uses two low-level engines for decoded-buffer and streaming playback.
Monitoring is owned separately by [AudioMeterController]()
in View.
Both engines implement `PlaybackEngine`.
[`AudioClipPlayer`]() picks between
them and owns the one it built, so constructing an engine yourself is for
hosting a transport of your own.
## BufferEngine — sample-accurate, resident
Plays a decoded `AudioBuffer` through an `AudioBufferSourceNode`. It schedules
against the audio clock through `play(offsetSeconds, when)` and what the
bridge's synchronized playback is built on; it also exposes a
`TransportClockReader`, so a follower can read its position without sampling.
Its source can loop natively; reported currentTime wraps while its clock remains
unwrapped. Native looping is distinct from the Bridge group loop contract.
A seek during pre-roll replaces the pending source at the original absolute
start time. Once sound has begun, seeking replaces it immediately. A failed
replacement leaves the engine paused at the requested position.
The cost is memory: the whole clip is resident. `play(offset)` positions a
**start** and folds an offset past the loop end back into the window — before
that fold, a native `start` would run to the buffer's end and never re-enter
the loop, leaving the engine reporting "playing" over silence.
## MediaEngine — host-backed streaming
Plays through a media element via a host adapter. It avoids requiring a fully decoded PCM clip; buffering and memory policy belong
to the host adapter/browser. `preservesPitch` is supported through that adapter. It polls rather than
scheduling, so there is no sample-accurate join and no transport clock; it
guards a loop range by watching position instead of looping natively.
On either engine, `play(offsetSeconds, when)` is a no-op while already playing,
including its offset argument. Use `seek(seconds)` to relocate running playback.
MediaEngine accepts and ignores `when`; it has no Kernel clock reader.
Range bounds must be finite. Negative starts clamp to zero; an empty or reversed
range throws `RangeError` before replacing an accepted option. Engines copy the
range, so mutating the caller's object does not change a running loop. BufferEngine
also rejects ranges with no span inside its known clip duration. MediaEngine
clamps the end when metadata arrives; if the start is then beyond the media,
it stops watching that unusable range and reports the ordinary natural end.
A later explicit `setLoop` or `play` re-evaluates the available duration.
Disposal is terminal for both engines: it releases their sources/adapters and
output connections, and later transport/preparation calls cannot recreate them.
The borrowed AudioContext remains open.
## Import
```ts
import {BufferEngine} from '@webmusic/audio/play/headless';
// context is caller-owned; clip already contains decoded PCM.
const engine = new BufferEngine(context, clip);
engine.output.connect(context.destination);
engine.prepare();
await engine.play(0, context.currentTime + 0.06);
const dispose = () => engine.dispose();
// The context owner retains its eventual cleanup path.
```
## Time, ownership and contract source
Engine position is audio media time. Only a buffer reader maps it against the
audio reference. Retain and dispose the engine while separately respecting
context ownership.
The public barrel and `packages/audio/src/play/headless/engines/{buffer-engine,media-engine}.ts` define the value, option, state and event
contracts described below. The reference does not turn private implementation
fields into public API.
API
**Engine construction and options**
| Member | Kind | Description |
| --- | --- | --- |
| `new BufferEngine(context, clip, options?)` | constructor | BaseAudioContext and decoded AudioClip are required; the context is borrowed. |
| `BufferEngineOptions.loop` | option | Boolean or finite `{start, end}` clip seconds; default false. Ranges must overlap the clip with positive span. |
| `BufferEngineOptions.rate` | option | Playback multiplier, default 1. |
| `BufferEngineOptions.preservesPitch` | option | Accepted but not implemented by buffer rate changes. |
| `new MediaEngine(context, clip, options?)` | constructor | Requires clip.sourceUrl and a borrowed BaseAudioContext. |
| `MediaEngineOptions.adapterFactory` | option | MediaPlaybackAdapterFactory required before preparation, seeking or playback. |
| `MediaEngineOptions.loop` | option | Default false; full-stream flag or finite positive watcher-controlled range. Actual metadata may later make a range unusable. |
| `MediaEngineOptions.rate` | option | Default 1. |
| `MediaEngineOptions.preservesPitch` | option | Default true, forwarded to the host adapter. |
**PlaybackEngine commands and state**
| Member | Kind | Description |
| --- | --- | --- |
| `output` | AudioNode | Owned engine output for caller routing; no engine setVolume method. |
| `currentTime` | number | Clip position in seconds; 0 before media readiness. |
| `duration` | number | Clip duration, or valid media metadata once available. |
| `playing` | boolean | Engine running state. |
| `play(offsetSeconds?, when?)` | void or `Promise` | Start offset in clip seconds, optional absolute reference time; no-op while running. |
| `pause()` | void | Hold position and stop sounding. |
| `stop()` | void | Stop and return to start. |
| `seek(seconds)` | void | Relocate a paused or running playhead; BufferEngine preserves an armed future start. |
| `setRate(rate)` | void | Change playback multiplier; implementations clamp normal finite rates to 0.25–4. |
| `setLoop(loop)` | void | Change full/range looping live; invalid range edits throw before replacing the accepted range. |
| `onEnded(callback)` | void | Register the natural-end callback; this is not an EventEmitter subscription API. |
| `prepare()` | void or `Promise` | Optional in PlaybackEngine, implemented by both engines; creates needed resources without playing. |
| `dispose()` | void | Terminal and idempotent; releases owned source/adapter/output and loop work without closing the borrowed context. |
| `clock` | optional TransportClockReader | BufferEngine provides it; MediaEngine omits it. |
| `MediaEngine.setPreservesPitch(flag)` | void | Changes the streaming adapter; not a BufferEngine method. |
**MediaPlaybackAdapter and factory**
| Member | Kind | Description |
| --- | --- | --- |
| `MediaPlaybackAdapterFactory(context, sourceUrl)` | function type | Returns the host adapter; host code owns DOM integration outside Headless. |
| `duration` | readonly number | Media duration in seconds. |
| `paused` | readonly boolean | Paused state. |
| `ended` | readonly boolean | Natural-end state. |
| `currentTime` | number | Readable/writable media seconds. |
| `playbackRate` | number | Readable/writable multiplier. |
| `preservesPitch` | optional boolean | Host pitch-preservation switch. |
| `loop` | boolean | Native full-stream loop switch. |
| `onended` | callback or null | End callback slot assigned by engine. |
| `play()` | void or `Promise` | Start the host media. |
| `pause()` | void | Pause host media. |
| `connect(destination)` | void | Route host audio into the engine output. |
| `disconnect()` | optional void method | Detach routing during engine cleanup. |
| `dispose()` | optional void method | Release adapter-owned host resources. |
**Public types**
| Member | Kind | Description |
| --- | --- | --- |
| `PlaybackEngine` | type | Common engine protocol above. |
| `BufferEngineOptions` | type | Buffer settings above. |
| `MediaEngineOptions` | type | Streaming settings above. |
| `MediaPlaybackAdapter` | type | Host contract above. |
**Composed by.** The engines are what
[``]() and
[``]() drive.
## Related
[AudioClipPlayer]() · [AudioMeterController]() · [Realtime analysis]() · [View models]()
---
# AudioMeterController
> Read live level and spectrum frames from a borrowed analyser or an owned transparent tap.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/headless/view/audio-meter/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`AudioMeterController` remains in the **View Headless** entry: it provides
measurements for a caller-owned presentation loop and owns no playback state,
transport or timer. The standalone [``]()
is now cataloged as a compact live Analyze tool and reuses this controller;
the original View entry remains available for code-only consumers.
## Import
```ts
import {createAudioMeterController} from '@webmusic/audio/view/headless';
const meter = createAudioMeterController({analyser});
const level = meter.readLevel();
const spectrum = meter.readSpectrum(28);
meter.dispose(); // the borrowed analyser stays connected
```
## API
### Options and state
| Member | Contract |
| --- | --- |
| `context?: BaseAudioContext` | Creates an owned input → analyser → output tap. Mutually exclusive with `analyser`. |
| `analyser?: AnalyserNode` | Borrows a node without configuring or disconnecting it. Omit both source options for a zero-valued meter. |
| `fftSize?: number` | Owned FFT size; power of two 32–32768, default 1024. |
| `smoothingTimeConstant?: number` | Owned FFT smoothing, 0–1; default 0.8. |
| `levelScale?: number` | Nonnegative display gain; default 1.8. |
| `peakDecay?: number` | Nonnegative scaled-RMS peak-hold decrement per read; default 0.012. |
| `analyser`, `input`, `output` | Read-only active analyser and owned splice ports. Borrowed mode has no input/output ports. |
| `ownsGraph` | Whether this controller owns a transparent tap. |
`AudioMeterControllerOptions`, `AudioMeterConfiguration` and
`AudioMeterLevelFrame` are exported from the same entry. Configuration excludes
source options: replacement means constructing a new controller.
### Commands and returned frames
| Method | Result |
| --- | --- |
| `configure(options)` | Changes tuning in place. Omitted values persist; invalid settings leave previous tuning and graph intact. Borrowed nodes retain their own FFT/smoothing configuration. |
| `readLevel()` | Returns `{rms, peak, peakHold, level}`. `level` is clamped scaled RMS; the live held marker retains scaled RMS with the per-read decrement. |
| `readSpectrum(bars = 28)` | Returns normalized FFT bars as a `Float32Array`. |
| `dispose()` | Releases every owned node, even if one disconnect fails, then rethrows the first error. Repeated disposal is safe. |
The pure helpers `calculateLevelMeter(samples, {previousPeak?, peakDecay?})` and
`calculateSpectrumBars(spectrum, {bars?, normalizeBytes?})` share this entry.
The PCM helper returns unscaled `{rms, peak, peakHold}`, ignores nonfinite
samples as zero, and retains the previous sample peak by a multiplicative
`peakDecay` in 0–1 (default 0.96). It differs intentionally from the live scaled
RMS hold. Spectrum byte normalization defaults to true. Types
`LevelMeterFrame`, `LevelMeterOptions` and `SpectrumBarsOptions` describe these
pure calls.
### Lifecycle and measurement limits
The controller emits no events and schedules no frames. A caller chooses the
read cadence and rebuilds on source replacement; several monitors may borrow the
same analyser. A context is borrowed even when its tap nodes are owned: disposal
never closes it. The values are normalized display readings, not dBFS, LUFS or
true-peak measurements. Peak decay depends on reads, not elapsed time.
### Migration
Import the controller and pure helpers from `@webmusic/audio/view/headless`.
Their former `play/headless` exports have been removed so Play does not depend on
View. The Element's Analyze catalog placement does not move these Headless exports
or rename the tag; `view/element` and `view/auto` retain compatibility registration.
## Related
- [Audio Meter Element]()
- [Audio Player]()
- [View API]()
- [Headless inventory]()
---
# AudioTimeline
> Zoom, offset, playhead and hit testing for an audio view — the geometry every waveform and spectrogram surface shares, with nothing drawn.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/headless/view/audio-timeline/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`AudioTimeline` owns where a view is looking. It converts between seconds and
pixels, keeps the playhead, holds the clip's regions for hit testing, and
answers the one question every renderer asks each frame: **which time range is
visible**.
It draws nothing and knows no canvas. That is what lets the same geometry back
a waveform, a spectrogram or a caller-owned overview — and what makes it
testable without a DOM.
Copies the region list while retaining the immutable Region values. It owns
geometry and playhead state, not a playback clock. It has no timer, events or
dispose method; a separate player binding owns subscriptions.
## Import
```ts
import {AudioTimeline, bindPlayerToAudioTimeline} from '@webmusic/audio/view/headless';
const timeline = new AudioTimeline({
durationSeconds: clip.duration,
viewportWidth: 800,
regions: clip.regions,
});
const binding = bindPlayerToAudioTimeline(player, timeline, {followPlayhead: true});
const {startSeconds, endSeconds} = timeline.visibleRange();
// Keep the binding while the owning view exists.
const dispose = () => binding.unsubscribe();
```
## Zoom anchors, it does not jump
`setZoom(pixelsPerSecond, anchorSeconds)` keeps the anchor where it is on
screen, defaulting to the playhead. Zooming without an anchor is the classic
way to lose the listener's place: the number changes and the content leaps.
`visibleRange(bufferScreens)` widens the answer by that many extra screens on
each side, which is how a virtualizing renderer prepares columns just outside
the viewport without drawing the whole clip.
## Following, and clicking
`followPlayhead(anchor)` scrolls so the playhead sits at that fraction of the
viewport — `0.5` centres it. `bindPlayerToAudioTimeline` wires a player's
`timeupdate` to `setPlayhead` and optionally to the follow, returning an
object whose `unsubscribe()` you own.
`hitTest(x)` turns a pixel into `{seconds, region?}` — the region is present
when the point falls inside one, which is what lets a click seek *and* report
which annotation it landed on.
## Time, ownership and contract source
A timeline is a view-state owner. The player/session remains authoritative for
transport seconds; the binding projects those seconds at the player event cadence.
Redraw after your own geometry commands because AudioTimeline publishes no change
event. Bindings subscribe but do not own their player or target.
The public barrel and `packages/audio/src/view/headless/timeline.ts and binding.ts` define the value, option, state and event
contracts described below. The reference does not turn private implementation
fields into public API.
API
**Construction**
| Member | Kind | Description |
| --- | --- | --- |
| `new AudioTimeline(options)` | constructor | Requires AudioTimelineOptions. |
| `createAudioTimeline(options)` | factory | Returns an AudioTimeline with the same geometry behavior. |
**AudioTimelineOptions**
| Member | Kind | Default | Description |
| --- | --- | --- | --- |
| `durationSeconds` | option | required | Nonnegative timeline length; invalid values normalize to zero. |
| `pixelsPerSecond` | option | `100` | Positive horizontal scale. |
| `viewportWidth` | option | `0` | Visible width in CSS pixels. |
| `offsetPixels` | option | `0` | Scroll offset, clamped to content bounds. |
| `playheadSeconds` | option | `0` | Initial position, clamped to duration. |
| `regions` | option | `[]` | Readonly Region list; its array is copied. |
**Commands**
| Member | Kind | Description |
| --- | --- | --- |
| `setDuration(seconds)` | void | Normalizes duration and clamps dependent state. |
| `setZoom(pixelsPerSecond, anchorSeconds?)` | void | Preserves the anchor screen position where bounds allow; default or nonfinite anchor uses the playhead. |
| `setViewport(width)` | void | Changes visible CSS-pixel width. |
| `setOffset(pixels)` | void | Sets/clamps scroll offset. |
| `setPlayhead(seconds)` | void | Moves the displayed playhead only; does not seek a player. |
| `setRegions(regions)` | void | Copies a replacement hit-test list. |
| `followPlayhead(anchor = 0.5)` | void | Scrolls the playhead to a viewport fraction, clamped to 0–1 and content bounds. |
**State and query results**
| Member | Kind | Description |
| --- | --- | --- |
| `snapshot` | AudioTimelineSnapshot | Fresh object with durationSeconds, pixelsPerSecond, viewportWidth, offsetPixels, playheadSeconds and contentWidth. |
| `regions` | readonly Region[] | Current hit-test list. |
| `visibleRange(bufferScreens = 0)` | object | `{startSeconds, endSeconds}` within duration, optionally padded on each side. |
| `hitTest(x)` | AudioTimelineHit | Viewport-relative CSS x maps to `{seconds, region?}`. |
**Player binding**
| Member | Kind | Description |
| --- | --- | --- |
| `bindPlayerToAudioTimeline(player, target, options?)` | AudioTimelineBinding | Reads timeupdate payload.seconds or player.seconds; updates a PlayheadTarget. It does not issue playback commands or perform an initial synchronization. |
| `followPlayhead` | option | false by default; calls target.followPlayhead when updates arrive. |
| `followAnchor` | option | undefined; target default applies (0.5 on AudioTimeline). |
| `resetOnEnd` | option | true; subscribes to end and writes zero. |
| `AudioTimelineBinding.unsubscribe()` | void | Idempotent; removes subscriptions without disposing player/target. |
| `PlayheadTarget.setPlayhead(seconds)` | required method | Receives the projected position. |
| `PlayheadTarget.followPlayhead(anchor?)` | optional method | Receives the requested scroll behavior. |
**Types and lifecycle**
| Member | Kind | Description |
| --- | --- | --- |
| `AudioTimelineOptions` | type | Construction geometry above. |
| `AudioTimelineSnapshot` | type | Six numeric fields listed under snapshot. |
| `AudioTimelineHit` | type | seconds with optional Region. |
| `BindAudioTimelineOptions` | type | followPlayhead, followAnchor and resetOnEnd. |
| `AudioTimelineBinding` | type | Owns only unsubscribe(). |
| `PlayheadTarget` | type | Structural write target; no event or dispose requirement. |
**Composed by.** [``]() — Audio / View.
## Related
[View models]() · [Render adapters]() · [AudioClipPlayer]()
---
# LiveViewController and LiveScrollBuffer
> Caller-paced analyser projection and fixed-capacity timestamped column storage, without a renderer or scheduler.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/headless/view/live-projection/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`LiveViewController` turns a borrowed `AnalyserNode` into a scrolling
projection: it samples frames, converts each to a column, and appends it to a
`LiveScrollBuffer` whose capacity is sized from the expected window and sampling
rate. It retains a fixed number of columns, not an exact time-pruned interval. The renderer
draws whatever the ring holds.
It **borrows** the analyser and never mutates or disconnects it — the same
rule everywhere in this repository, because a tap usually has other readers.
Sampling is caller-paced: the controller never schedules a frame, so each
`capture(nowMs)` you make reads exactly one column. `clear()` drops the
history; the buffer itself is yours to drop.
## Import
```ts
import {createLiveViewController} from '@webmusic/audio/view/headless';
const live = createLiveViewController({
analyser: player.analyser,
type: 'spectrogram',
windowSeconds: 8,
});
let frameId = 0;
const frame = (now: number) => {
const buffer = live.capture(now);
if (buffer) console.log(buffer.length, buffer.latestSeconds);
frameId = requestAnimationFrame(frame);
};
frameId = requestAnimationFrame(frame);
const dispose = () => {
cancelAnimationFrame(frameId); // application owns the rendering cadence
live.clear(); // release history/scratch, not the borrowed tap
};
```
## Capacity is fixed on purpose
`LiveScrollBuffer` derives its capacity once, from `windowSeconds ×
columnsPerSecond`, and never grows. Irregular capture can make the actual time
span differ from windowSeconds. The source comment says why better than a
paraphrase would: a live view runs for as long as the page is open, so an
unbounded history is a leak with a pretty name. When the ring is full the
oldest column is overwritten.
Each column carries **its own timestamp** rather than relying on its index,
so a renderer draws correct spacing even when frames arrive irregularly —
which they do, since sampling competes with everything else on the main
thread.
## Two frame shapes
The analyser gives two different kinds of data, and the two column builders
are how one buffer takes both:
- `timeDomainColumn(frame)` reduces a time-domain window to a `[min, max]`
pair — two rows, a waveform band in normalized −1…1 units.
- `frequencyColumn(frame, out)` writes a byte spectrum into an existing
`Float32Array` — N rows, a spectrogram slice. It takes the output array so
the hot path allocates nothing.
`LiveViewController` uses two rows for the waveform pair and frequencyBinCount
rows for a spectrum. LiveScrollBuffer itself accepts any positive row count.
Time-domain frames have **no dB mapping** — that is a frequency-domain notion
— so a renderer must not apply one uniformly across both projections.
## Time, ownership and contract source
Capture timestamps locate observations relative to the first capture; they are
not an AudioContext or musical transport authority. The controller and buffer have
no events or dispose methods. The application owns frame scheduling and must stop
it before dropping the projection.
The public barrel and `packages/audio/src/view/headless/live.ts` define the value, option, state and event
contracts described below. The reference does not turn private implementation
fields into public API.
API
**LiveViewController construction**
| Member | Kind | Description |
| --- | --- | --- |
| `new LiveViewController(options?)` | constructor | No graph or timer is created. |
| `createLiveViewController(options?)` | factory | Returns the same controller type. |
| `analyser` | option | Optional borrowed tap; no reconfiguration/disconnection. |
| `type` | option | 'waveform' by default; LiveProjectionType also permits 'spectrogram'. |
| `windowSeconds` | option | 5 by default, minimum 0.5 for controller history sizing. |
| `columnsPerSecond` | option | 60 by default, minimum 1; sizes capacity without scheduling captures. |
**Controller commands and state**
| Member | Kind | Description |
| --- | --- | --- |
| `capture(nowMs)` | LiveScrollBuffer or undefined | One sample at caller-supplied monotonic milliseconds; first capture sets local time zero. No analyser returns undefined. |
| `setAnalyser(analyserOrUndefined)` | void | Changes borrowed source and clears history when changed. |
| `setType(type)` | void | Changes projection and clears history when changed. |
| `setWindowSeconds(seconds)` | void | Changes capacity policy and clears history when changed. |
| `clear()` | void | Drops buffer/scratch/local origin; does not disconnect analyser. |
| `analyser` | AnalyserNode or undefined | Borrowed source. |
| `type` | LiveProjectionType | Current waveform or spectrogram mode. |
| `windowSeconds` | number | Requested history window. |
| `buffer` | LiveScrollBuffer or undefined | Created on capture; undefined before capture/after clear. |
**LiveScrollBuffer construction**
| Member | Kind | Description |
| --- | --- | --- |
| `new LiveScrollBuffer(options)` | constructor | Allocates fixed storage once. |
| `rows` | option | Required values per column; floored with minimum 1. |
| `windowSeconds` | option | 5 by default; minimum 0.1 for the standalone buffer. |
| `columnsPerSecond` | option | 60 by default; minimum 1. |
| `capacity` | readonly number | max(2, ceil(windowSeconds × columnsPerSecond)); fixed after construction. |
| `rows` | readonly number | Stored values per column. |
| `windowSeconds` | readonly number | History sizing value. |
| `length` | readonly number | Held columns, initially 0. |
| `latestSeconds` | readonly number | Most recent column timestamp, initially 0. |
**Buffer commands and reads**
| Member | Kind | Description |
| --- | --- | --- |
| `push(seconds, values)` | void | Copies up to rows values, pads missing values with zero, overwrites oldest at capacity. |
| `forEach(visit)` | void | Visits oldest first. One scratch values array is reused across callbacks; copy it if retaining a column. The walk is not allocation-free. |
| `at(index)` | LiveColumn or undefined | Age index 0 is oldest; returned values are copied. |
| `clear()` | void | Resets count/cursor; allocated storage remains reusable. |
**Pure helpers and types**
| Member | Kind | Description |
| --- | --- | --- |
| `timeDomainColumn(frame)` | [min, max] | Byte time-domain samples mapped around 128 into normalized −1…1; empty input yields [0,0]. |
| `frequencyColumn(frame, out)` | Float32Array | Writes byte spectrum normalized to 0–1 into caller output and returns it. |
| `LiveColumn` | type | `{seconds, values}`; units of values depend on the projection. |
| `LiveProjectionType` | type | waveform or spectrogram. |
| `LiveScrollOptions` | type | rows, windowSeconds, columnsPerSecond. |
| `LiveViewControllerOptions` | type | analyser, type, windowSeconds, columnsPerSecond. |
**Composed by.** [``]() —
Audio / View, whose `type` picks between the two projections over one
controller.
## Related
[AudioTimeline]() · [Realtime analyzer]() · [Metering]()
---
# computeClipPeaks and peaksDuration
> computeClipPeaks and peaksDuration: the compact min/max pyramid a waveform reads instead of the samples.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/headless/view/peaks/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`computeClipPeaks(clip, samplesPerPeak?)` reduces a clip's channel data to a
**pyramid** of min/max pairs — a base level at the requested resolution, then
progressively coarser levels above it. A waveform renderer picks the level
that matches the current zoom, so rendering can use a suitable resolution without scanning raw samples on every
frame. Computing the pyramid still scans the input and allocates output.
`peaksDuration(peaks)` computes the represented block length from a pyramid, which
is what lets a surface draw peaks it was handed without also being told the
duration.
Both are synchronous helpers with no events, clock or disposal. computeClipPeaks
reads the clip through channels(), which supplies defensive PCM copies, and returns
a new pyramid. peaksDuration reads an existing pyramid.
## Import
```ts
import {computeClipPeaks, peaksDuration} from '@webmusic/audio/view/headless';
const peaks = computeClipPeaks(clip, 256);
if (peaks) console.log(peaksDuration(peaks));
```
`computeClipPeaks` returns `undefined` for a clip with no channel data — a
streaming clip that was never decoded — rather than an empty pyramid, so a
caller must handle the absence instead of drawing a flat line. A decoded clip
with zero samples returns a pyramid with an empty base level; its represented
duration is zero, and waveform projections return no columns.
## Intentionally local
This is a second implementation of peaks: the analyze capability computes them
too. It is duplicated on purpose, because `view` may not import `analyze` —
the projection surface would otherwise acquire an analysis dependency to draw
a waveform. The same reasoning covers the BBC-compatible
`peaksFromWaveformData` on the capability root, which hydrates a pyramid from
`audiowaveform` JSON **without decoding audio at all**.
## Time, ownership and contract source
This page owns pure projection helpers, not a Headless lifecycle object. A peaks
pyramid summarizes sample amplitude at a resolution; it is not a TransportClock or
a reversible copy of PCM. Retain/drop the result as data.
The public barrel and `packages/audio/src/view/headless/peaks.ts` define the value, option, state and event
contracts described below. The reference does not turn private implementation
fields into public API.
API
**API helpers**
| Member | Kind | Description |
| --- | --- | --- |
| `computeClipPeaks(clip, samplesPerPeak = 256)` | AudioPeaks or undefined | Positive finite samplesPerPeak is floored/lower-bounded at 1. Builds quantized Int8 min/max blocks and coarser levels; undefined without channel data. |
| `peaksDuration(peaks)` | number | Base block count × samplesPerPeak / sampleRate, in seconds; 0 for no base level or nonpositive sample rate. The final block can pad beyond exact clip duration. |
**Related data contract**
| Member | Kind | Description |
| --- | --- | --- |
| `AudioPeaks` | @webmusic/audio type | sampleRate, channels, baseSamplesPerPeak and levels. |
| `PeaksLevel` | @webmusic/audio type | samplesPerPeak and packed Int8 min/max data; owned by the core API reference. |
| `clip.duration` | authoritative source length | Use the clip duration when an exact playback/viewport boundary is required instead of assuming peaksDuration is exact. |
**Composed by.** [``]() — Audio / View.
## Related
[View models]() · [Core audio data]() · [View API]()
---
# WaveformViewModel and SpectrogramViewModel
> Project AudioPeaks or SpectrogramData through a caller-controlled timeline into visible columns and frames.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/headless/view/view-models/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
Two view models, one job: given a clip and a viewport, produce the **columns**
a renderer paints — and only the columns in view. They share a page because a
reader picks between them by asking what they want to see, not by learning two
different objects.
WaveformViewModel retains an AudioPeaks value; SpectrogramViewModel retains
structured SpectrogramData. Each owns an AudioTimeline and calculates arrays on
query. They do not read an AudioClip directly, own audio resources, schedule frames
or publish events. No disposal method is needed.
## Import
```ts
import {
createWaveformViewModel,
computeClipPeaks,
} from '@webmusic/audio/view/headless';
const peaks = computeClipPeaks(clip);
if (!peaks) throw new Error("Decoded samples are required");
const waveform = createWaveformViewModel(peaks, {durationSeconds: clip.duration});
waveform.timeline.setViewport(800); // pixels
waveform.timeline.setZoom(100); // pixels per second
const columns = waveform.columns(); // one column per visible timeline pixel
```
## WaveformViewModel
Reads the peaks pyramid and emits one `{min, max}` per screen column, picking
the pyramid level whose resolution matches the zoom. That level choice is why
a waveform stays cheap at every zoom: each draw reads one coarse layer rather
than the samples.
`fitWaveformColumns(peaks, width)` is the whole-clip shortcut: one merged
`{min, max}` column per pixel, fitting the entire pyramid into `width` with no
timeline at all. A caller-owned compact preview can use that fit-to-box
geometry and recompute it on resize.
## SpectrogramViewModel
Reads precomputed STFT frames and windows them to the visible range, with frequency-bin values retained for the renderer to map. The data contract is explicit: it
takes **structured** `SpectrogramData`, because `view` may not import
`analyze` — that boundary is what keeps a projection surface from acquiring an
analysis dependency.
Supply sorted frame times and frequencies, with frame-major magnitudes matching
binsPerFrame. The Headless model selects bins; it does not map frequency to pixels. The
packaged renderer uses its own frequency-axis policy. A custom renderer must map
its labels and bins consistently rather than infer a mel scale from these values.
## Time, ownership and contract source
These models project existing data; they never advance a clock. A separate
player binding updates their timeline and the caller chooses when to query and
paint. Input changes require new models; view-model results are not an analysis
cache or a sample-editing API.
The public barrel and `packages/audio/src/view/headless/waveform.ts and spectrogram.ts` define the value, option, state and event
contracts described below. The reference does not turn private implementation
fields into public API.
API
**Construction**
| Member | Kind | Description |
| --- | --- | --- |
| `new WaveformViewModel(peaks, options?)` | constructor | Retains AudioPeaks; default duration is peaksDuration(peaks), which may include final block padding. |
| `createWaveformViewModel(peaks, options?)` | factory | Returns WaveformViewModel. |
| `new SpectrogramViewModel(data, options?)` | constructor | Retains SpectrogramData from /view API; default duration is inferred from the last time and final interval. |
| `createSpectrogramViewModel(data, options?)` | factory | Returns SpectrogramViewModel. |
**Shared timeline options**
| Member | Kind | Default | Description |
| --- | --- | --- | --- |
| `durationSeconds` | option | inferred | Override material duration in seconds. |
| `pixelsPerSecond` | option | `100` | Initial positive scale. |
| `viewportWidth` | option | `0` | Visible CSS-pixel width; set before requesting useful columns. |
| `offsetPixels` | option | `0` | Initial scroll offset. |
| `playheadSeconds` | option | `0` | Initial displayed playhead. |
| `regions` | option | `[]` | Annotations retained by the model timeline. |
| `WaveformViewModelOptions.mergeChannels` | option | `false` | Merge channel ranges into one output peak. |
| `SpectrogramViewModelOptions.minFrequency` | option | no lower limit | Inclusive lower Hz bound on selected bins. |
| `SpectrogramViewModelOptions.maxFrequency` | option | no upper limit | Inclusive upper Hz bound on selected bins. |
**State and queries**
| Member | Kind | Description |
| --- | --- | --- |
| `WaveformViewModel.timeline` | readonly AudioTimeline | Mutable geometry owner reached through a readonly reference. |
| `WaveformViewModel.columns(bufferScreens = 0)` | readonly WaveformColumn[] | Visible columns, optionally buffered; empty for no visible width/range. |
| `SpectrogramViewModel.timeline` | readonly AudioTimeline | Controls time viewport, not playback. |
| `SpectrogramViewModel.frames(bufferScreens = 0)` | readonly SpectrogramFrameView[] | Visible frames and selected frequency bins. Magnitudes may be subarray views of input data; treat them as borrowed. |
| `fitWaveformColumns(peaks, width)` | readonly WaveformColumn[] | Pure whole-clip projection, merging channels into width columns; recompute after resizing. |
**Public result and option types**
| Member | Kind | Description |
| --- | --- | --- |
| `WaveformViewModelOptions` | type | AudioTimelineOptions with optional durationSeconds, plus mergeChannels. |
| `SpectrogramViewModelOptions` | type | AudioTimelineOptions with optional durationSeconds, plus min/maxFrequency. |
| `WaveformChannelPeak` | type | `{min, max}` sample range. |
| `WaveformColumn` | type | `{x, seconds, peaks, region?}`; x is absolute timeline pixels, so account for scroll offset when drawing. |
| `SpectrogramFrameView` | type | `{frameIndex, seconds, frequencies, magnitudes}`. |
| `SpectrogramData` | related /view API type | times, frequencies, magnitudes and binsPerFrame; no Analyze import is required. |
**Composed by.** Applications can consume either model with a custom renderer.
The shared `fitWaveformColumns` helper prepares overview columns for a
caller-owned canvas without instantiating both model classes.
## Related
[AudioTimeline]() · [Peaks]() · [Analyze API]() · [Render adapters]()
---
# Audio
> Digital audio — decoding, playback, projection and MIR analysis: what each Audio capability covers, how to install it, and a walkthrough from file to sound to screen.
Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/audio@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
Audio is one package, `@webmusic/audio`, for **recorded sound**: decoded
samples in an immutable `AudioClip`, played, drawn and measured. The
play, analyze, and view capabilities support Web Components, Headless objects,
and APIs. Combine the APIs with [UI presenters]() or your own interface;
integration entries such as React build on the same contracts.
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/audio/)
## Capabilities
| Capability | What it covers | Forms |
| --- | --- | --- |
| **play** | Get sound in and out: decoding and loading, buffer and media playback, mixing, microphone recording, effects, WAV export. | [Web Components]() · [Headless]() · [API]() |
| **analyze** | Derive facts from a signal: peaks, loudness, tempo and beats, key, onsets, pitch, chromagram, spectrogram, Meyda features, transcription. | [Web Components]() · [Headless]() · [API]() |
| **view** | Project a signal onto a surface: waveforms, spectrograms, regions and live scrolling views; build custom overviews from the Headless data helpers. | [Web Components]() · [Headless]() · [API]() |
| **react** | The same capabilities as React components and hooks. | [API]() |
The package root is the model itself — `AudioClip`, `Region`, `BeatGrid`,
`ClipEditSession`, `AudioPeaks` and JSON. Importing it creates no audio
context and is safe in Node, workers and SSR; explicit conversion methods may
use an AudioContext supplied by the caller. See
the [package subpath table]() for every published
entry.
Audio has no `io` capability: decoding is part of **play**, because what a
decoder produces is exactly what a player consumes.
## What Audio is not
Audio never reads notation: MIDI, MusicXML, MXL and ABC are
[Score](), and the two packages never depend on each other.
`transcribe()` stops at family-neutral note events on purpose — assembling
them into a `Score`, like anything else needing **both** families, lives in
[Bridge]().
## Install
:::note[Registry or local workspace]
Use an available published `@webmusic/audio` version, or consume this repository's workspace locally with `npm install` at the root and a workspace link. CDN examples require the requested version and its dependencies to resolve; verify availability before deploying them.
:::
```bash
npm install @webmusic/audio @webmusic/kernel @webmusic/ui
```
`play`, `analyze`, `view`, and `react` are export subpaths of that one installed package; import them as shown throughout this page rather than installing them separately.
`@webmusic/kernel` is required. The heavy, format-specific engines are **optional peer dependencies** — installed only when you opt in, and selected by entry or feature. UI composition entries import the presenter peer statically: `@webmusic/ui` for the `/element` forms, and `react` for `/react`.
- The [`@webmusic/audio/play`]() capability uses optional decoder peers (`mpg123-decoder` for mp3, `@wasm-audio-decoders/flac`, `@wasm-audio-decoders/ogg-vorbis`, `ogg-opus-decoder`, and `mediabunny` for AAC/containers), `music-metadata` for tags, and the time-stretch worklets (`signalsmith-stretch`, `@soundtouchjs/audio-worklet`). Each is needed only for the format or feature that uses it — WAV needs none of them.
- The [`@webmusic/audio/analyze`]() capability uses the package's bundled DSP dependencies (`fft.js`, `meyda`, `music-tempo`, `pitchy`) and the optional MIR and transcription peers (`essentia.js`, `aubiojs`, `pitchfinder`, `realtime-bpm-analyzer`, `webfft`, plus `@spotify/basic-pitch`, `@magenta/music`, `@tensorflow/tfjs` behind the `./transcribe` entry).
## Drop-in Web Components
The fastest way to get audio on a page — no build step. `@webmusic/audio/play` ships ready-to-use custom elements (central player, playlist, mixer and recorder); drop a tag in and it works. `@webmusic/audio/view` adds `` and ``. `@webmusic/audio/analyze` adds five live tools, including ``. Every tag and its owning page is on the [Web Components page]().
### From a CDN
The fastest path: load a prebuilt capability IIFE from a CDN and **every element in that capability registers itself**. No `npm`, no bundler, no `define*` calls — just put the tags on the page.
```html
```
Add the view and analysis elements the same way — each capability has its own bundle under the package's `dist` directory:
```html
```
Each bundle is the capability's `/global` entry and exposes one window
global carrying its exports plus a `defineAllAudioElements()`:
**`WebMusicAudioPlay`** (`dist/play/auto.global.js`),
**`WebMusicAudioView`** (`dist/view/auto.global.js`), and
**`WebMusicAudioAnalyze`** (`dist/analyze/auto.global.js`).
`` is deliberately bring-your-own-data: registering it does not decode
the player's `src` or compute a waveform. Assign its `.peaks` property as shown in
[Visualize](#visualize) before expecting a waveform.
For ES modules, use the [bundler setup](#with-a-bundler) below. The ESM output
retains package imports that a plain browser module URL cannot resolve by itself.
### With a bundler
After the [install](#install) above, import each capability's **`auto`** entry
once and its elements register on load:
```ts
import "@webmusic/audio/play/auto"; // , , ,
import "@webmusic/audio/view/auto"; // ,
import "@webmusic/audio/analyze/auto"; // , , , ,
```
### Custom registration
For **tree-shaking** or **custom tag names**, skip `auto` and call the individual `define*` functions yourself from the `/element` subpath. Each is idempotent and one-time, so you ship only what you reference:
```ts
import {
defineAudioPlayerElement,
} from "@webmusic/audio/play/element";
import {defineAudioMeterElement} from "@webmusic/audio/analyze/element";
defineAudioPlayerElement(); // registers
defineAudioMeterElement("vu-meter"); // …or register under a custom tag
```
The original `view/element` and `view/auto` meter entries remain compatibility
paths to the same `` implementation; using both auto entries does
not register a second class.
Every capability also exports a `defineAllAudioElements()` aggregate from its `/element` subpath — exactly what each `/auto` entry calls for you:
```ts
import { defineAllAudioElements } from "@webmusic/audio/play/element";
defineAllAudioElements(); // every element in @webmusic/audio/play, at default tags
```
Elements ship from the `/element` subpath, so each capability root tree-shakes free of DOM code. Every tag, its owning page and the shared theming variables are on the **[Web Components page]()**.
## Load a clip
```ts
import { loadClipFromUrl } from "@webmusic/audio/play";
const clip = await loadClipFromUrl("/wav/Arabesque%20No.1.wav");
console.log(clip.sampleRate, clip.duration);
```
WAV follows the dependency-free decoder path. For compressed files, provide
`{ context: new AudioContext() }` for native browser decoding or install the
matching optional WASM decoder. To stream an MP3 without decoding it into an
`AudioClip`, use ``.
## Play
The same clip can power your own buttons through `AudioClipPlayer`. Put this
markup on a page and call `mountAudioButtons(host, '/wav/Arabesque%20No.1.wav')`.
Use the [Arabesque No.1 WAV]() from the demos or your own recording.
```html
```
```ts
import { loadClipFromUrl } from "@webmusic/audio/play";
import { AudioClipPlayer } from "@webmusic/audio/play/headless";
export function mountAudioButtons(host: HTMLElement, src: string) {
const play = host.querySelector("[data-play]")!;
const pause = host.querySelector("[data-pause]")!;
const status = host.querySelector("[data-status]")!;
const events = new AbortController();
let player: AudioClipPlayer | undefined;
const subscriptions: Array<() => void> = [];
let disposed = false;
const report = (error: unknown) => {
if (!disposed)
status.textContent =
error instanceof Error ? error.message : String(error);
};
void loadClipFromUrl(src)
.then((clip) => {
if (disposed) return;
player = new AudioClipPlayer(clip, { volume: 0.5 });
subscriptions.push(
player.on("end", () => {
if (!disposed) status.textContent = "Ended";
}),
player.on("error", report),
);
play.disabled = pause.disabled = false;
status.textContent = "Ready. Click Play to enable sound.";
})
.catch(report);
play.addEventListener(
"click",
async () => {
if (!player) return;
play.disabled = true;
try {
await player.play(); // creates/resumes its context inside the gesture
if (!disposed)
status.textContent = player.playing ? "Playing" : "Paused";
} catch (error) {
report(error);
} finally {
if (!disposed) play.disabled = false;
}
},
{ signal: events.signal },
);
pause.addEventListener(
"click",
() => {
player?.pause();
status.textContent = "Paused";
},
{ signal: events.signal },
);
const dispose = () => {
if (disposed) return;
disposed = true;
events.abort();
for (const unsubscribe of subscriptions) unsubscribe();
player?.dispose();
play.disabled = pause.disabled = true;
status.textContent = "Disposed";
};
window.addEventListener("pagehide", dispose, { signal: events.signal });
return dispose; // also call when your framework removes the host
}
const disposeControls = mountAudioButtons(
document.querySelector("#audio-controls")!,
"/wav/Arabesque%20No.1.wav",
);
// Call disposeControls() when this page section is removed.
```
This example loads a WAV before enabling playback, reports loading/start errors,
and ignores a load that completes after disposal. The player owns its lazily
created context. Retry a failed source by disposing and mounting again. See
[AudioClipPlayer]() for other engines,
looping, effects and borrowed-context behavior.
## API + UI
The UI presenter can instead borrow an already created player. Its binding uses
clip seconds and returns a cleanup that releases only the presentation resources.
The application still owns and eventually disposes the player.
```ts
import type { AudioClipPlayer } from "@webmusic/audio/play/headless";
import { mountTransport } from "@webmusic/ui/transport";
export function mountAudioTransport(
host: HTMLElement,
player: AudioClipPlayer,
reportError: (error: unknown) => void,
) {
const listeners = new Set<() => void>();
const notify = () => {
for (const listener of listeners) listener();
};
const ui = mountTransport(
host,
{
snapshot: () => ({
playing: player.playing,
seconds: player.seconds,
duration: player.duration,
progress: player.progress,
}),
play: async () => {
await player.play();
notify();
},
pause: () => {
player.pause();
notify();
},
seekFraction: (fraction) => {
player.seekFraction(fraction);
notify();
},
subscribe: (listener) => {
const offTime = player.on("timeupdate", listener);
const offEnd = player.on("end", listener);
listeners.add(listener);
return () => {
listeners.delete(listener);
offTime();
offEnd();
};
},
},
{ label: "Audio", onError: reportError },
);
return () => {
ui.destroy();
listeners.clear();
};
}
```
Commands issued through this binding refresh paused state immediately. If other
application controls mutate this Audio player while paused, route those commands
through the same application notification function. The renderer does not own a
second transport. See [Transport]() and the
[Score/Audio composition]() for a coordinated group.
## Analyze
```ts
import { createAudioAnalysisSession } from "@webmusic/audio/analyze/headless";
const session = createAudioAnalysisSession(clip, {
tasks: ["loudness", "tempo", "key"],
});
const result = await session.analyze();
console.log(result.tempo?.bpm); // beats per minute (undefined unless 'tempo' ran)
console.log(result.key?.tonic, result.key?.mode); // e.g. "C" "major"
console.log(result.loudness.integratedLufs); // ITU-R BS.1770 LUFS
```
## Visualize
Compute a peaks pyramid with `@webmusic/audio/analyze`, then render it into a container with `@webmusic/audio/view`:
```ts
import { computePeaks } from "@webmusic/audio/analyze";
import { renderWaveformVisualizer } from "@webmusic/audio/view/render";
const peaks = computePeaks(clip.channels()!, clip.sampleRate);
const viz = renderWaveformVisualizer(document.querySelector("#wave")!, peaks, {
height: 96,
pixelsPerSecond: 100,
});
```
Or hand the same peaks to the `` element through its `.peaks` property:
```ts
const view = document.querySelector("audio-view")!;
view.peaks = computePeaks(clip.channels()!, clip.sampleRate);
```
## Next Steps
- [Web Components]() for every drop-in tag, its owning page and shared theming.
- [Headless]() for the code-only transports, sessions and their export ownership.
- [Core API]() for the `AudioClip` model, `Region`, `BeatGrid`, `AudioPeaks`, and events.
- [Play API]() for decoding/loading, `AudioClipPlayer`, `AudioMixer`, `AudioRecorder`, effects, and export.
- [Analyze API]() for peaks, loudness, tempo, key, onsets, pitch, and the incremental analysis session.
- [View API]() for canvas waveform / spectrogram / meter renderers and player binding.
- [React API]() for hooks and the `` component.
---
# @webmusic/bridge
> Every root export of the bridge: timeline conversion, offline score rendering, the role adapters, transcription assembly, and the sync object documented under Headless.
Canonical documentation: https://koperative-lab.github.io/WebMusic/bridge/api/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/bridge@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`@webmusic/bridge` is a **single-entry package**: the root is the whole API.
It is the only package allowed to import both families, and both are peer
dependencies alongside `@webmusic/kernel` — the bridge borrows the score and
audio instances your application already has.
It does not decode audio, parse notation, or draw anything. Those live in
[`@webmusic/audio`]() and [`@webmusic/score`](); what is
here is only the work that needs *both*.
## Install
`npm install @webmusic/bridge` — use an available published version or the local workspace. It has no optional peers of its own; it inherits whatever the two families need.
## Entry map
| Subpath | Form | What is there |
| --- | --- | --- |
| `@webmusic/bridge` | root · the whole API | Everything below. |
| — | [Headless]() | `ScoreAudioSync` and its two factories are root exports, documented under the headless contract because they are constructed and disposed rather than called. |
There is no `/headless` subpath, no `/element`, and no `/auto`: the bridge
ships no Web Components. It gets none until it has one to ship.
## Synchronized playback
The stateful half of the package. `ScoreAudioSync` drives a master and a
follower transport as one unit, and the two factories build both players on a
shared `AudioContext` first. All three are documented on
[its page]() rather than here, because
construction, commands, state and disposal are what a reader needs and a
function list cannot carry them.
`clipAsMaster` and `scoreAsFollower` are the role adapters underneath: they
wrap a clip player and a score player in the `SyncMasterTransport` /
`SyncFollowerTransport` shapes the sync consumes. Reach for them when you are
assembling a pairing by hand instead of through a factory.
## Timeline conversions
The two families deliberately keep different musical time models — Score's
exact `TimeMap` (rational quarters → seconds) and Audio's `BeatGrid`
(beats[] in seconds). Both project onto the same seconds ruler, so they
convert:
```ts
import {beatGridFromTimeMap, timeMapFromBeatGrid} from '@webmusic/bridge';
// Exact at the sampled positions; intervening tempo changes can be lost.
const grid = beatGridFromTimeMap(score.timeMap, {durationQuarters: 64});
// Approximate: each inter-beat interval becomes one tempo segment.
const map = timeMapFromBeatGrid(gridFromTempoDetection);
```
Use `beatGridFromTimeMap` to hand a score's tempo curve to audio-side tools
(beat-synced effects, waveform beat markers); use `timeMapFromBeatGrid` to
carry a detected tempo grid (e.g. from `@webmusic/audio/analyze`'s tempo detection)
into score-side quantization. `DEFAULT_MAX_MAPPED_BEATS` is the ceiling that
keeps a pathological tempo map from generating an unbounded grid.
`timeMapFromBeatGrid` maps each grid beat to one quarter note, sets quarter
zero to second zero, and supplies a default 4/4 meter. Retain the original
`grid.beats[0]` offset separately when aligning a recording. It does not recover
the original meter or the subdivision factor used to create a grid. These
functions therefore do not form a lossless round trip.
### Mapping contract
| Member | Default | Contract |
| --- | --- | --- |
| `beatGridFromTimeMap(map, options): BeatGrid` | — | Samples a Score `TimeMap` at uniform quarter subdivisions, returning Audio beat positions in seconds. The final sample rounds up to the next subdivision if needed. |
| `options.durationQuarters` | Required | Finite number greater than zero; requested span on the quarter axis. |
| `options.beatsPerQuarter` | `1` | Positive safe integer; samples per quarter. The resulting grid's beat index is a subdivision index. |
| `options.maxBeats` | `DEFAULT_MAX_MAPPED_BEATS` (`1_000_000`) | Safe integer at least 2. The allocation includes the zero-position sample; excessive spans throw `RangeError`. |
| `timeMapFromBeatGrid(grid): TimeMap` | — | Each positive inter-beat interval becomes a tempo segment; zero/nonpositive intervals throw `RangeError`. With fewer than two beats, uses `grid.bpm`. |
`BeatGridFromTimeMapOptions` is the options shape above. Both operations return
data, own no running transport or audio resources, and need no disposal.
## Offline rendering
`renderScoreToClip(score, options?)` plays a score into an `OfflineAudioContext`
and returns the result as an `AudioClip` — a matched backing track, without a
speaker or a user gesture. This is what lets the sync demo make the clip the
sounding source while a silent score transport drives the visuals.
The renderer includes an effect/release tail by default. Use the
audio-mastered pairing to play that tail through the clip's end. The live
Score-mastered demo renders with `tailSeconds: 0`; passing a Bridge-rendered
clip with a positive tail back to `createSyncedPlayback` is rejected before
creating players. Both pair factories require the original Score instance used
for that render. A `tempo` override that differs from the Score's base tempo
changes the clip's time axis; neither pair factory accepts that rendered clip.
Render at the Score's base tempo for synchronized playback and change speed with
`sync.setRate()`. These guards recognize the original AudioClip object from
one render call; copying or serializing it loses that in-memory provenance.
For repeated notation, call `expandRepeats(score)` first, then render and pair
the expanded Score instance. Passing `scoreOptions.expandRepeats` with a
Bridge-rendered clip would expand only the live player, so the factories reject
it before allocating playback resources.
Its signature is `renderScoreToClip(score: Score, options?: RenderOptions):
Promise`. Options and synth ownership follow
[Score's offline render helpers]().
An `OfflineAudioContext` implementation and an offline-compatible synth are
required; self-routing Tone/Spessa synths do not route into that capture graph.
Rendering rejects on preparation/render failures. The returned clip is data,
with the renderer's sample rate and channel count, and has no playback lifetime
until supplied to a player. Timbre, effects, tempo, and rendering tails are
choices made during rendering, not information recoverable from the clip's
original notation.
## Audio → Score assembly
`@webmusic/audio/analyze/transcribe` emits family-neutral note events; the bridge
turns them into a real `Score`:
```ts
import {transcribe} from '@webmusic/audio/analyze/transcribe';
import {scoreFromTranscription} from '@webmusic/bridge';
const {notes, bpm} = await transcribe(clip, {quantize: true});
const score = scoreFromTranscription(notes, {bpm, quantizeGrid: 4});
```
Notation snaps to the grid (default sixteenths; `quantizeGrid: 0` still rounds
to a 1/480-quarter tick). Accepted notes retain their supplied onset and
duration in `performed.onsetSec` / `durationSec`; velocity is converted to
integer MIDI units. Pitches are rounded and out-of-range results are dropped.
This preserves supplied timing alongside notation but cannot recover material
the transcriber did not represent. Note that
`transcribe` itself needs its optional peers installed
(`@spotify/basic-pitch`, `@tensorflow/tfjs`); `scoreFromTranscription` is
pure and always available. `DEFAULT_MAX_TRANSCRIPTION_MEASURES` and
`DEFAULT_MAX_TRANSCRIPTION_NOTES` bound what one call will assemble.
### Assembly contract
`scoreFromTranscription(notes: readonly TranscribedNote[], options?:
ScoreFromTranscriptionOptions): Score` is synchronous. It creates one part,
one voice, a constant tempo, and enough measures to cover the accepted notes
(at least one measure for empty input). It does not detect instruments,
voices, meter, or a variable tempo map.
| Option | Default | Contract |
| --- | --- | --- |
| `bpm` | `120` | Finite, positive output tempo. Pass the desired interpretation explicitly. |
| `timeSignature` | `{numerator: 4, denominator: 4}` | Positive safe-integer numerator and denominator; defines measure duration. |
| `quantizeGrid` | `4` | Nonnegative safe integer subdivisions per quarter. Zero uses 480 ticks per quarter; rounded durations have at least one grid unit. |
| `title` | `'Transcription'` | Score title. |
| `partName` | `'Transcribed'` | The single part's name. |
| `maxNotes` | `DEFAULT_MAX_TRANSCRIPTION_NOTES` (`100_000`) | Positive safe integer limiting input count, before pitch filtering. |
| `maxMeasures` | `DEFAULT_MAX_TRANSCRIPTION_MEASURES` (`4_096`) | Positive safe integer limiting materialized measures. |
For each input note, `midi` must be finite; its rounded pitch outside 0–127
is skipped. For an accepted pitch, seconds must be finite, nonnegative, and
ordered, and velocity must be finite. Velocity is multiplied by 127, rounded,
and clamped to 1–127. Invalid options, accepted-note values, unsafe timing
arithmetic, or resource-limit violations throw `RangeError`. Assembly owns
no context, subscription, or running clock and has no disposal step.
## API Reference
Every export of the root, exactly once.
| Export | Kind | Use it for |
| --- | --- | --- |
| `ScoreAudioSync` | class | [Synchronized playback]() — the transport pair as one unit. |
| `createSyncedPlayback` | factory | [Score as master]() — builds both players and the sync. |
| `createAudioMasteredPlayback` | factory | [Audio as master]() — the same, with the recording driving. |
| `clipAsMaster` | adapter | Wrap a clip player as the sync's master role. |
| `scoreAsFollower` | adapter | Wrap a score player as the sync's follower role. |
| `beatGridFromTimeMap` | function | Sample a score's exact tempo map onto an audio `BeatGrid`. |
| `timeMapFromBeatGrid` | function | Carry a detected beat grid into a score `TimeMap`. |
| `renderScoreToClip` | function | Render a score offline into an `AudioClip`. |
| `scoreFromTranscription` | function | Assemble transcribed note events into a `Score`. |
| `DEFAULT_MAX_MAPPED_BEATS` | constant | The beat ceiling `beatGridFromTimeMap` will not exceed. |
| `DEFAULT_MAX_TRANSCRIPTION_MEASURES` | constant | Measure ceiling for one assembly. |
| `DEFAULT_MAX_TRANSCRIPTION_NOTES` | constant | Note ceiling for one assembly. |
| `TransportCommand`, `TransportCommit`, `TransportEvent`, `TransportSnapshot` | types | Re-exported Kernel command/observation contracts used by `ScoreAudioSync`; see [session commands](). |
| `ScoreAudioSyncOptions` | type | Construction options for the sync. |
| `CreateSyncedPlaybackOptions` | type | Options for the score-as-master factory. |
| `CreateAudioMasteredPlaybackOptions` | type | Options for the audio-as-master factory. |
| `AudioMasterAdapterOptions` | type | Offset options for `scoreAsFollower`. |
| `BeatGridFromTimeMapOptions` | type | Options for `beatGridFromTimeMap`. |
| `ScoreFromTranscriptionOptions` | type | Options for `scoreFromTranscription`. |
| `SyncScoreTransport` | type | The score side of a pairing. |
| `SyncClipTransport` | type | The clip side of a pairing. |
| `SyncMasterTransport` | type | The clock-owning role. |
| `SyncFollowerTransport` | type | The following role. |
| `SyncTransportClockView` | type | Read-only view of the master clock. |
| `SyncLoopRegion` | type | A loop region in master seconds. |
| `SyncTransportContract` | type | The structural contract both roles satisfy. |
---
# Playback composition
> Load once, coordinate Score and Audio, and attach views without creating another transport.
Canonical documentation: https://koperative-lab.github.io/WebMusic/bridge/composition/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/bridge@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
A complete composition has one command authority and several borrowers. This
example builds a short score, renders a matching recording, and gives both to
Bridge. Audio supplies the sound; the muted Score player supplies musical data,
position and note activity. The waveform, piano roll and chord analysis each use
that existing session.
This Score-mastered example renders with `tailSeconds: 0` so its clip ends on
the Score timeline. For an offline render with an effect or release tail, use
`createAudioMasteredPlayback` so the clip can continue through that tail.
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/bridge/composition/)
## Try the transitions
1. Click **Load example**, then **Play**. Loading resumes an application-owned
AudioContext from your click and renders the backing track offline. No network
asset, SoundFont or optional decoder is required.
2. Click **Attach Score views** during playback. The piano roll and analysis
borrow the already loaded score; they do not fetch a second source. Detach
them, pause, and attach again to inspect the paused snapshot.
3. Choose **2×**, pause, and seek with the transport slider. The position stays
on the original score-seconds axis: the midpoint of this eight-second phrase
is four score seconds at either rate. Resume to hear the changed speed.
4. Enable **Repeat the group** to exercise coordinated seeking at the end.
This is a tick-detected group loop, with scheduling lead-in; it is not a
claim of an exact or gapless audio loop boundary.
5. Click **Replace source**. The old group pauses while a new score and recording
are prepared. Successful replacement disposes the previous group and replaces
its views. A preparation failure retains the previous source, visibly paused.
6. Click **Dispose session**, including while loading. Pending work cannot install
a new group after disposal. Load again to create a fresh session.
The chord panel presents score-derived chord analysis. It does not implement the
proposed `score-chord` playback-preview mode. Two equal-looking positions on this
page are UI observations, not measurements of audible timing. The
[ScoreAudioSync reference]() owns the supported
coordination and error semantics.
## Setup in your application
Install the five packages used by this composition:
```bash
npm install @webmusic/score @webmusic/audio @webmusic/bridge @webmusic/kernel @webmusic/ui
```
Use published versions that are available, or this repository's local workspaces.
The implementation below is compiled by the documentation site against its
workspace packages. Browser audio requires a user gesture and Web Audio support;
this example also requires OfflineAudioContext. Unsupported environments and
loading/command failures appear in the status output.
The composition is ordinary application code: `ScoreBuilder` produces the score,
`renderScoreToClip` produces its audio, `createSyncedPlayback` creates the paired
players, and a UI transport dispatches group commands. The application closes
its context when the session is disposed. Bridge releases the players it creates;
removing a view releases only that view's borrowed connection and own resources.
## Three integration choices in one session
| Choice | Used here | State and ownership |
| -------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------- |
| Web Components | `score-view` and `score-chord-analysis` | Borrow a public playback source; no repeated `src` and no new player. |
| Headless | `ScorePlayer`, `AudioClipPlayer`, `ScoreAudioSync` | Own playback behavior and participate in the selected group authority. |
| API + UI | Offline rendering, peaks, waveform renderer and `mountTransport` | Produce data or display state supplied by the application. |
A player selector is a browser convenience around the same nonvisual source
protocol. The owner host in this demo exposes a public `playback` value. Its
snapshot and subscription delegate to the native Score player, while its seek
command delegates to the **group**, so an interactive follower cannot move only
one participant:
```ts
import type { ScorePlaybackSource } from "@webmusic/score";
import { createSyncedPlayback } from "@webmusic/bridge";
export function groupPlaybackSource(
pair: ReturnType,
): ScorePlaybackSource {
return {
snapshot: () => pair.scorePlayer.playback.snapshot(),
subscribe: (listener) => pair.scorePlayer.playback.subscribe(listener),
seekNominal: async (position) => {
const result = await pair.sync.dispatch({ type: "seek", position });
if (result.status !== "committed") throw new Error("Seek superseded.");
},
};
}
```
New companions receive their initial data and state through that protocol.
Application drawing uses animation frames to read current positions; it does not
advance musical time. Command completion comes from `dispatch`, not from an
optimistic button label. The last six command results are bounded diagnostics,
not an unbounded event history.
Complete Score/Audio demo implementation
This is the actual client module used by the live demo. `DemoScope` is a small
site lifecycle helper; in an application, register its cleanup with your framework
and release it on host removal or navigation. It is not a package API.
```ts
import {
Duration,
MeasureId,
PartId,
Pitch,
Rational,
ScoreBuilder,
VoiceId,
type ScorePlaybackSource,
} from "@webmusic/score";
import { defineScoreViewElement } from "@webmusic/score/view/element";
import { defineChordAnalysisElement } from "@webmusic/score/analyze/element";
import { computePeaks } from "@webmusic/audio/analyze";
import { renderWaveformVisualizer } from "@webmusic/audio/view/render";
import {
createSyncedPlayback,
renderScoreToClip,
type TransportCommand,
} from "@webmusic/bridge";
import { mountTransport } from "@webmusic/ui/transport";
import type { DemoScope } from "../demo-lifecycle";
let nextOwner = 0;
type Pair = ReturnType;
type Waveform = ReturnType;
type PlaybackHost = HTMLElement & { playback?: ScorePlaybackSource };
/** Two small sources with identical duration and different musical content. */
function sampleScore(descending: boolean) {
const builder = new ScoreBuilder();
const partId = PartId("demo-piano");
const voice = VoiceId("demo-voice");
const meter = { numerator: 4, denominator: 4 };
builder
.setMetadata({
title: descending ? "Descending phrase" : "Ascending phrase",
})
.addTempo({ atQuarters: Rational.ZERO, bpm: 120 })
.addMeter({
atQuarters: Rational.ZERO,
measureNumber: 1,
timeSignature: meter,
});
builder.addPart({ id: partId, name: "Piano", staves: 1 });
const pitches = Array.from({ length: 4 }, () => [
"C4",
"E4",
"G4",
"B4",
"C5",
"G4",
"E4",
"C4",
]).flat();
if (descending) pitches.reverse();
pitches.forEach((pitch, index) => {
if (index % 8 === 0)
builder.addMeasure({
id: MeasureId(`demo-measure-${index / 8}`),
number: index / 8 + 1,
onsetQuarters: new Rational(index, 2),
durationQuarters: new Rational(4),
timeSignature: meter,
});
builder.addNote(partId, {
id: builder.newNoteId(),
pitch: Pitch.parse(pitch),
onsetQuarters: new Rational(index, 2),
duration: Duration.eighth(),
voice,
});
});
return builder.build();
}
/** Application composition only: public package entries, explicit resources and commands. */
export function mountBridgeComposition(
root: HTMLElement,
scope: DemoScope,
): void {
defineScoreViewElement();
defineChordAnalysisElement();
const query = (selector: string): T =>
root.querySelector(selector)!;
const load = query("[data-load]");
const replace = query("[data-replace]");
const disposeButton = query("[data-dispose]");
const attach = query("[data-attach]");
const rate = query("[data-rate]");
const loop = query("[data-loop]");
const status = query("[data-status]");
const position = query("[data-position]");
const events = query("[data-events]");
const views = query("[data-score-views]");
const waveHost = query("[data-wave-surface]");
const owner = query("[data-playback-owner]");
owner.id = `wm-composition-owner-${++nextOwner}`;
let generation = 0;
let context: AudioContext | undefined;
let pair: Pair | undefined;
let wave: Waveform | undefined;
let offSync: (() => void) | undefined;
let loading = false;
let attached = false;
let descending = false;
let duration = 0;
let frame = 0;
let lastPaint = 0;
const observers = new Set<() => void>();
const history: string[] = [];
const message = (value: string) => {
if (scope.active) status.textContent = value;
};
const report = (error: unknown) =>
message(error instanceof Error ? error.message : String(error));
const notify = () => {
for (const observer of observers) observer();
};
const refresh = () => {
const unavailable = !pair || loading;
if (pair && !loading) {
rate.value = String(pair.sync.snapshot.rate);
loop.checked = !!pair.sync.snapshot.loop;
}
replace.disabled = unavailable;
attach.disabled = unavailable;
rate.disabled = unavailable;
loop.disabled = unavailable;
load.disabled = loading || !!pair;
disposeButton.disabled = !pair && !loading && !context;
attach.textContent = attached ? "Detach Score views" : "Attach Score views";
notify();
};
const appendEvent = (entry: string) => {
history.push(entry);
if (history.length > 6) history.shift();
events.textContent = history.join("\n");
};
const detachViews = () => {
views.replaceChildren();
};
const mountViews = () => {
detachViews();
if (!pair || !attached) return;
const roll = document.createElement("score-view");
roll.setAttribute("type", "piano-roll");
roll.setAttribute("player", `#${owner.id}`);
roll.setAttribute(
"aria-label",
"Score view borrowing the current score player",
);
const analysis = document.createElement("score-chord-analysis");
analysis.setAttribute("player", `#${owner.id}`);
analysis.setAttribute(
"aria-label",
"Chord analysis borrowing the same score",
);
views.append(roll, analysis);
};
const releasePair = () => {
// Remove borrowers before releasing their playback owner.
const previous = pair;
const previousWave = wave;
const previousSubscription = offSync;
pair = undefined;
offSync = undefined;
wave = undefined;
owner.playback = undefined;
for (const release of [
detachViews,
previousSubscription,
() => previousWave?.dispose(),
() => previous?.sync.dispose(),
]) {
try {
release?.();
} catch (error) {
report(error);
}
}
};
const disposeSession = () => {
generation++;
loading = false;
releasePair();
const previousContext = context;
context = undefined;
if (previousContext && previousContext.state !== "closed") {
void previousContext.close().catch(report);
}
duration = 0;
attached = false;
rate.value = "1";
loop.checked = false;
position.textContent = "No active session";
message("Disposed. Load an example to start a new session.");
refresh();
};
const command = async (input: TransportCommand) => {
const current = pair;
if (!current || loading) return;
const expected = generation;
// Resume directly in the click callback, before any asynchronous command work.
if (input.type === "play") await context?.resume();
if (!scope.active || expected !== generation || current !== pair) return;
try {
const result = await current.sync.dispatch(input);
if (!scope.active || expected !== generation || current !== pair) return;
if (result.status === "committed") {
message(`${input.type} applied; revision ${result.revision}.`);
}
} finally {
if (scope.active && expected === generation && current === pair) refresh();
}
};
const ui = mountTransport(
query("[data-transport]"),
{
snapshot: () => {
const snapshot = pair?.sync.snapshot;
const seconds = snapshot?.position ?? 0;
return {
playing: !!snapshot && !snapshot.paused,
seconds,
duration,
progress: duration > 0 ? Math.min(1, seconds / duration) : 0,
disabled: !pair || loading || !!snapshot?.pending,
};
},
play: () => command({ type: "play" }),
pause: () => {
void command({ type: "pause" }).catch(report);
},
seekFraction: (fraction) =>
command({ type: "seek", position: fraction * duration }),
subscribe: (observer) => {
observers.add(observer);
return () => {
observers.delete(observer);
};
},
},
{ label: "Coordinated playback", showTime: "full", onError: report },
);
const loadSource = async (nextDescending: boolean) => {
const expected = ++generation;
loading = true;
refresh();
message("Preparing score and rendering its backing track…");
let candidate: Pair | undefined;
try {
context ??= new AudioContext();
await context.resume(); // invoked in the Load / Replace gesture
if (!scope.active || expected !== generation) return;
if (pair) await pair.sync.dispatch({ type: "pause" });
const score = sampleScore(nextDescending);
const clip = await renderScoreToClip(score, { tailSeconds: 0 });
if (!scope.active || expected !== generation) return;
const channels = clip.channels();
if (!channels)
throw new Error("The rendered clip has no decoded samples.");
const peaks = computePeaks(channels, clip.sampleRate, {
baseSamplesPerPeak: 512,
});
const candidateOwner: { pair?: Pair } = {};
candidate = createSyncedPlayback(score, clip, {
context,
onOperationError: (operation, error) => {
if (scope.active && pair === candidateOwner.pair)
report(new Error(`${operation}: ${String(error)}`));
},
});
candidateOwner.pair = candidate;
candidate.scorePlayer.setVolume(0); // only the rendered clip sounds
// Render the candidate before replacing the old working composition.
const candidateHost = document.createElement("div");
waveHost.append(candidateHost);
let candidateWave: Waveform;
try {
candidateWave = renderWaveformVisualizer(candidateHost, peaks, {
height: 88,
durationSeconds: clip.duration,
pixelsPerSecond: 120,
});
} catch (error) {
candidateHost.remove();
throw error;
}
releasePair();
pair = candidate;
candidate = undefined;
waveHost.replaceChildren(candidateHost);
wave = candidateWave;
duration =
pair.scorePlayer.playback.snapshot().nominalDurationSeconds ??
clip.duration;
const observedPair = pair;
owner.playback = {
snapshot: () => observedPair.scorePlayer.playback.snapshot(),
subscribe: (listener) =>
observedPair.scorePlayer.playback.subscribe(listener),
seekNominal: async (seconds) => {
const result = await observedPair.sync.dispatch({
type: "seek",
position: seconds,
});
if (result.status !== "committed")
throw new Error(
"The seek was superseded by a newer session command.",
);
},
};
descending = nextDescending;
rate.value = "1";
loop.checked = false;
offSync = pair.sync.subscribe((event) => {
if (!scope.active || pair !== observedPair) return;
if (event.type === "commit") {
appendEvent(
`r${event.commit.revision}: ${event.commit.status} at ${event.snapshot.position.toFixed(2)} score s`,
);
} else if (event.type === "error" || event.type === "operation-error")
report(event.error);
refresh();
});
mountViews();
wave.redraw(pair.clipPlayer.seconds);
message(
`${descending ? "Descending" : "Ascending"} phrase ready. Click Play, then attach views.`,
);
} catch (error) {
candidate?.sync.dispose();
if (scope.active && expected === generation) {
const reason = error instanceof Error ? error.message : String(error);
report(
pair
? `Replacement failed; the previous source remains paused. ${reason}`
: error,
);
if (!pair) {
const failedContext = context;
context = undefined;
if (failedContext && failedContext.state !== "closed")
void failedContext.close().catch(report);
}
}
} finally {
if (scope.active && expected === generation) {
loading = false;
refresh();
}
}
};
scope.listen(load, "click", () => loadSource(false));
scope.listen(replace, "click", () => loadSource(!descending));
scope.listen(disposeButton, "click", disposeSession);
scope.listen(rate, "change", () =>
command({ type: "rate", rate: Number(rate.value) }).catch(report),
);
scope.listen(loop, "change", () =>
command({
type: "loop",
loop: loop.checked ? { startSeconds: 0, endSeconds: duration } : null,
}).catch(report),
);
scope.listen(attach, "click", () => {
attached = !attached;
mountViews();
message(
attached
? "Views attached to the current score and playback snapshot."
: "Views detached; playback remains owned by the session.",
);
refresh();
});
const draw = (now: number) => {
if (!scope.active) return;
if (pair && now - lastPaint >= 80) {
lastPaint = now;
const snapshot = pair.sync.snapshot;
wave?.redraw(pair.clipPlayer.seconds, true);
position.textContent = `${snapshot.position.toFixed(2)} score s · ${pair.clipPlayer.seconds.toFixed(2)} clip s · ${snapshot.rate}×`;
notify();
}
frame = requestAnimationFrame(draw);
};
frame = requestAnimationFrame(draw);
scope.add(() => cancelAnimationFrame(frame));
scope.add(() => ui.destroy());
scope.add(disposeSession);
refresh();
}
```
## Independent loop lengths
A group loop repeats the master's timeline. A local native loop repeats one
participant's content while the master keeps advancing. The following separate
session uses a continuous application-owned master and two native Audio buffer
loops: one second and one-and-a-half seconds. Both derive timing from the same
AudioContext; each has its own local phase.
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/bridge/composition/)
Click **Play loops**, then **Pause** and **Seek master to 4.25 s**. The expected
local positions are **0.25 s** for the one-second loop and **1.25 s** for the
one-and-a-half-second loop. At **2×**, the content positions retain those meanings
while the loops run faster. The progress bars read actual player positions; they
do not schedule the sound.
The implementation configures each `AudioClipPlayer` with its native
`loop: {start, end}` option and registers the same local bounds as
`TransportGroup.addFollower(..., {loop: {startSeconds, endSeconds}})` metadata.
The group maps joins, seeks and drift checks into each local phase. Metadata
alone does not turn a player into a native looping engine.
This uses the Kernel group directly with structural adapters. The default
`createSyncedPlayback` factory continues to reject clip-side loop overrides;
its group-loop option has a different responsibility. The demo does not claim
nonlinear tempo warping, time stretching that preserves pitch, or a shared
writable clock instance.
Complete independent-loop implementation
```ts
import { TransportClock } from "@webmusic/kernel/transport";
import {
TransportGroup,
type SyncFollowerTransport,
type SyncMasterTransport,
type TransportCommand,
} from "@webmusic/kernel/sync";
import { createAudioClip } from "@webmusic/audio";
import { AudioClipPlayer } from "@webmusic/audio/play/headless";
import type { DemoScope } from "../demo-lifecycle";
/** A short decaying tone followed by silence; each clip's end is its native loop boundary. */
function pulseClip(seconds: number, frequency: number, sampleRate: number) {
const samples = new Float32Array(Math.round(seconds * sampleRate));
const length = Math.round(0.12 * sampleRate);
for (let i = 0; i < length; i++) {
const time = i / sampleRate;
samples[i] =
0.18 *
Math.sin(2 * Math.PI * frequency * time) *
Math.min(1, time / 0.004) *
Math.exp(-45 * time);
}
return createAudioClip({ sampleRate, channelData: [samples] });
}
/** App-owned continuous master: only TransportGroup delegates mutations to it. */
function continuousMaster(context: AudioContext): SyncMasterTransport {
const now = () => context.currentTime;
const clock = new TransportClock(now);
return {
clock,
get position() {
return clock.position;
},
play: (when = now()) => clock.startAt(when, clock.position),
pause: () => clock.pause(now()),
stop: () => {
clock.pause(now());
clock.seekTo(0, now());
},
seekPosition: (position, when = now()) => {
if (clock.paused) clock.seekTo(position, now());
else clock.startAt(when, position);
},
setRate: (rate) => clock.setRate(rate, now()),
dispose: () => clock.pause(now()),
};
}
function follower(player: AudioClipPlayer): SyncFollowerTransport {
return {
get position() {
return player.seconds;
},
get running() {
return player.playing;
},
get clock() {
return player.clock;
},
play: (when) => player.play(when),
pause: () => player.pause(),
stop: () => player.stop(),
seek: (position) => player.seek(position),
setRate: (rate) => player.setRate(rate),
dispose: () => player.dispose(),
};
}
export function mountIndependentLoops(
root: HTMLElement,
scope: DemoScope,
): void {
const query = (selector: string) =>
root.querySelector(selector)!;
const start = query("[data-start]");
const pause = query("[data-pause]");
const seek = query("[data-seek]");
const rate = query("[data-rate]");
const disposeButton = query("[data-dispose]");
const status = query("[data-status]");
const timeline = query("[data-timeline]");
const bars = [...root.querySelectorAll("progress")];
const periods = [1, 1.5];
let context: AudioContext | undefined;
let group: TransportGroup | undefined;
let players: AudioClipPlayer[] = [];
const unregistered = new Set();
let busy = false;
let generation = 0;
let frame = 0;
let offGroup: (() => void) | undefined;
const report = (error: unknown) => {
if (scope.active)
status.textContent =
error instanceof Error ? error.message : String(error);
};
const refresh = () => {
start.disabled = busy;
pause.disabled = seek.disabled = rate.disabled = !group || busy;
disposeButton.disabled = !group && !context;
};
const dispose = () => {
generation++;
try {
offGroup?.();
} catch (error) {
report(error);
}
offGroup = undefined;
try {
group?.dispose();
} catch (error) {
report(error);
}
group = undefined;
for (const player of unregistered) {
try {
player.dispose();
} catch (error) {
report(error);
}
}
unregistered.clear();
players = [];
const oldContext = context;
context = undefined;
if (oldContext && oldContext.state !== "closed")
void oldContext.close().catch(report);
busy = false;
rate.value = "1";
for (const bar of bars) bar.value = 0;
timeline.textContent = "No active session";
if (scope.active)
status.textContent = "Disposed. Play loops creates a new session.";
refresh();
};
const run = async (command: TransportCommand) => {
if (busy) return;
const expected = generation;
busy = true;
refresh();
try {
if (command.type === "play") {
context ??= new AudioContext();
refresh();
await context.resume();
if (!scope.active || expected !== generation) return;
if (!group) {
const sessionContext = context;
group = new TransportGroup(
continuousMaster(sessionContext),
() => sessionContext.currentTime,
{ onOperationError: (_operation, error) => report(error) },
);
for (const [index, period] of periods.entries()) {
const player = new AudioClipPlayer(
pulseClip(period, index ? 660 : 330, context.sampleRate),
{
audioContext: context,
engine: "buffer",
loop: { start: 0, end: period },
},
);
players.push(player);
unregistered.add(player);
group.addFollower(follower(player), {
loop: { startSeconds: 0, endSeconds: period },
});
unregistered.delete(player);
}
offGroup = group.subscribe((event) => {
if (event.type === "error" || event.type === "operation-error")
report(event.error);
});
}
}
const result = await group?.dispatch(command);
if (
scope.active &&
expected === generation &&
result?.status === "committed"
) {
status.textContent = `${command.type} applied at master ${result.snapshot.position.toFixed(2)} s.`;
}
} catch (error) {
if (scope.active && expected === generation) {
dispose();
report(error);
}
} finally {
if (scope.active && expected === generation) {
busy = false;
refresh();
}
}
};
scope.listen(start, "click", () => run({ type: "play" }));
scope.listen(pause, "click", () => run({ type: "pause" }));
scope.listen(seek, "click", () => run({ type: "seek", position: 4.25 }));
scope.listen(rate, "change", () =>
run({ type: "rate", rate: Number(rate.value) }),
);
scope.listen(disposeButton, "click", dispose);
const draw = () => {
if (!scope.active) return;
if (group) {
const snapshot = group.snapshot;
const phases = players.map(
(player, index) => player.seconds % periods[index]!,
);
bars.forEach((bar, index) => {
bar.value = phases[index] ?? 0;
});
timeline.textContent = `Master ${snapshot.position.toFixed(2)} s · 1 s loop ${phases[0]?.toFixed(2)} s · 1.5 s loop ${phases[1]?.toFixed(2)} s`;
}
frame = requestAnimationFrame(draw);
};
frame = requestAnimationFrame(draw);
scope.add(() => cancelAnimationFrame(frame));
scope.add(dispose);
refresh();
}
```
## What this reference establishes
The code makes ownership, command units, late attachment, source replacement and
cleanup inspectable. Automated checks cover the declared contracts and lifecycle
ordering; actual browser, device and audible behavior needs its own recorded
acceptance. The examples are small application compositions, not a project file
format, undo system, or complete music workstation.
Continue with [Kernel synchronization](),
[Score views](),
[score-chord-analysis]() and
[Transport]() for the owning APIs.
---
# Headless
> The code-only objects @webmusic/bridge exports, and the rules every one of them follows.
Canonical documentation: https://koperative-lab.github.io/WebMusic/bridge/headless/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/bridge@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`@webmusic/bridge` has no `/headless` subpath — everything is a root export.
This section exists because its stateful object is documented under the
**headless** contract rather than the API one: it is something you construct,
drive and dispose, not a function you call.
The object has no DOM or styles. A directly constructed sync calls each
participant's optional dispose() method when released; it does not automatically
borrow participants without cleanup. Factories own the players they create and
close only contexts they create. A supplied context remains caller-owned.
## Exports documented here
| Export | Kind | Page |
| --- | --- | --- |
| `ScoreAudioSync` | class | [ScoreAudioSync]() |
| `createSyncedPlayback` | factory | on the same page — a class and its factories share a page |
| `createAudioMasteredPlayback` | factory | on the same page |
| `clipAsMaster` · `scoreAsFollower` | protocol adapters | [ScoreAudioSync]() — state/axis and disposer forwarding |
## Type ownership
`ScoreAudioSyncOptions`, `CreateSyncedPlaybackOptions`,
`CreateAudioMasteredPlaybackOptions`, `AudioMasterAdapterOptions`,
`SyncScoreTransport`, `SyncClipTransport`, `SyncMasterTransport`,
`SyncFollowerTransport`, `SyncTransportClockView`, `SyncLoopRegion` and
`SyncTransportContract` are explained with
[ScoreAudioSync]().
The remaining root values, constants and their options belong to the
[Bridge API](): timeline conversions, renderScoreToClip and
scoreFromTranscription. Those operations have explicit data, resource and fidelity
contracts; "function" does not necessarily mean allocation-free or reversible.
The source barrel is `bridges/score-audio/src/index.ts`. This manual map routes
values and types to their explanation owner. It is not proof that an automated
symbol-name check has validated every signature, default or lifecycle behavior.
---
# ScoreAudioSync
> The stateful transport that coordinates a score player and a clip player using a shared AudioContext reference and group commands, correcting drift as it goes.
Canonical documentation: https://koperative-lab.github.io/WebMusic/bridge/headless/score-audio-sync/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/bridge@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`ScoreAudioSync` drives two transports as one. One participant owns the clock
(the **master**) and the other follows it, with a drift monitor reseeking the
follower whenever the two diverge. It is defined by **role, not by family**:
the same object backs both a score-driven and an audio-driven pairing, which
is why swapping the roles is a different factory rather than a different
class.
`dispose()` stops the drift monitor and calls each participant's optional
`dispose()` method. Both factory-created players are therefore released with
the sync. The factories also close any `AudioContext` they created; an existing
context passed by the caller remains open. If an application must retain a
transport after disposing a manually constructed sync, pass a transport adapter
that omits `dispose()`.
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/bridge/headless/score-audio-sync/)
## Import
```ts
import {createAudioMasteredPlayback, renderScoreToClip} from '@webmusic/bridge';
const clip = await renderScoreToClip(score); // includes its rendering tail
const {sync, scorePlayer} = createAudioMasteredPlayback(score, clip);
scorePlayer.setVolume(0); // the clip is the sounding source
await sync.play();
await sync.seek(12.5);
// Keep the pair until the owning session/view is removed.
const dispose = () => sync.dispose();
```
Both factories build the two players on one shared `AudioContext` and hand
back the sync that drives them, so a caller never has to match contexts by
hand. `sync.dispose()` releases both players and closes a factory-created
context; a context supplied through `options.context` stays open for its owner.
Constructing `new ScoreAudioSync(master, follower, context, options)` directly is
the path when you already own both transports.
The factories use the audio buffer engine for scheduled starts. Changing the
shared playback rate changes the clip's pitch; `clipOptions.preservesPitch` is a
media-engine option and has no effect on this buffer path. Use a different
transport composition when pitch-preserving time stretch is required.
## Score as master
`createSyncedPlayback(score, clip)` builds a `ScorePlayer` and an
`AudioClipPlayer` on one shared `AudioContext` and returns a `ScoreAudioSync`
that drives them as a unit. The score is the master clock; `play()` proposes a
shared audio-clock origin that the score player arms via its scheduled
`play(when)` and the clip joins at the very same instant through the buffer
engine's scheduled `play(when)`. That aligns a transport origin when preparation
finishes in time; rests, backend capability and output latency determine the first
audible samples.
A master without scheduled starts ignores the proposal and the clip joins a
lead-in later. A drift monitor reseeks the clip if the transports ever
diverge.
The demo above renders the score **offline** into a backing track
(`renderScoreToClip`), then plays that clip as the sounding source while a
muted score master drives the piano-roll. This remains score-mastered even though
the clip provides the audible material. The displayed score and clip positions
are model observations; they are not a benchmark or proof of audible sample accuracy.
The demo renders with `tailSeconds: 0` so its Score-mastered clip ends with the
score. The import example uses Audio as master to play the full default rendering
tail. `createSyncedPlayback` rejects a Bridge-rendered clip with a positive tail;
use `createAudioMasteredPlayback` with the original Score for that clip
or render with `tailSeconds: 0` when Score must remain master.
For repeats, use `expandRepeats(score)` from `@webmusic/score` before rendering,
then pair that expanded Score instance. `scoreOptions.expandRepeats` changes
only the ScorePlayer timeline and is rejected for Bridge-rendered clips.
For an **external** backing track (a real recording), pass
`clipOffsetSeconds` — the clip time corresponding to score-seconds zero — and
read the [timeline conversion contracts]() when preparing alignment
data. The current sync itself consumes a fixed offset, not a nonlinear tempo map.
## Audio as master
To follow a real recording rather than a rendered backing track, swap the
roles: `createAudioMasteredPlayback` makes the clip's clock the master and the
score the follower.
```ts
import {createAudioMasteredPlayback} from '@webmusic/bridge';
const {sync, scorePlayer} = createAudioMasteredPlayback(score, recording, {
clipOffsetSeconds: 2.4, // score-zero sits 2.4s into the recording
});
scorePlayer.setVolume(0); // the recording is the sounding source
await sync.play(); // transport positions use the recording axis
const dispose = () => sync.dispose(); // eventual owner cleanup
```
Positions are on the **recording's** seconds axis here. The score is held out
of the recording's lead-in — a count-in, an anacrusis, room tone — and enters
when the recording reaches score-zero, rather than starting alongside it and
running that much early.
The same factory is the appropriate master for an offline-rendered clip with a
tail: the clip continues until its own end after the Score player finishes.
## Operational errors
Legacy controls and background reconciliation report asynchronous failures
through `operation-error` events and `onOperationError`. Revisioned `dispatch()`
commands also expose failures through their returned promise. A failure that
still owns the current authority attempts to pause participants; an older failure
cannot override a newer command. Cleanup continues if an adapter throws, without
guaranteeing that the failing participant paused. Observer exceptions are
contained rather than re-thrown into the queue.
## Revisioned commands
The Bridge root exports `TransportCommand`, `TransportCommit`, `TransportEvent`
and `TransportSnapshot` from Kernel. Their complete field and event contract is
owned by [Kernel session commands]().
`ScoreAudioSync` delegates to that existing group rather than owning another clock.
```ts
import type {ScoreAudioSync} from '@webmusic/bridge';
async function moveSession(sync: ScoreAudioSync): Promise {
const detach = sync.subscribe((event) => {
if (event.type === 'error' || event.type === 'operation-error') {
console.error(event.error);
}
});
try {
await sync.dispatch({type: 'rate', rate: 2});
const result = await sync.dispatch({type: 'seek', position: 12.5});
if (result.status === 'committed') console.log(result.snapshot.position);
} finally {
detach();
}
}
```
A committed seek reports the actual clamped position; it does not mean playback
has finished. A master that fails to resume is rejected by `dispatch` even if its
legacy seek method resolved. The older methods remain available with their
compatibility behavior; applications needing awaited rate/seek failure should
use dispatch. Direct mutations on the returned players bypass group commands
and are observed only where drift/stop reconciliation supports them.
For independent native clip loops, use explicit Kernel follower adapters and
[participant-local mappings]().
The two Bridge factories continue to reject `clipOptions.loop`; their top-level
loop controls a coordinated group loop, not independent clip loops.
## Time, ownership and contract source
One group has one selected position and command authority. `dispatch` publishes
revisioned invalidation and waits for engine command/re-entry settlement. The
engines retain private clocks and share an AudioContext reference; same-instance
writable-clock injection is not implemented. Independent sessions may use separate groups. Synchronization
uses fixed offsets and tick-detected loop boundaries; model conversions have their
separate information limits on the [Bridge API page]().
The public barrel and `bridges/score-audio/src/sync.ts, audio-master.ts and platform/kernel/src/sync.ts` define the value, option, state and event
contracts described below. The reference does not turn private implementation
fields into public API.
API
**Construction and factories**
| Member | Kind | Description |
| --- | --- | --- |
| `new ScoreAudioSync(master, follower, context, options?)` | constructor | Uses structural participants and a BaseAudioContext reference; caller must supply compatible clocks. Disposal calls each optional participant disposer. |
| `createSyncedPlayback(score, clip, options?)` | factory | Creates ScorePlayer, AudioClipPlayer and sync; returns `{sync, scorePlayer, clipPlayer}` with nominal score seconds as master axis. |
| `createAudioMasteredPlayback(score, clip, options?)` | factory | Returns the same objects with clip seconds as master axis; uses axis/lead-in adapters. |
**ScoreAudioSyncOptions**
| Member | Kind | Default | Description |
| --- | --- | --- | --- |
| `leadInSeconds` | option | `0.06` | Nonnegative scheduling headroom in reference seconds. |
| `clipOffsetSeconds` | option | `0` | Finite, nonnegative clip seconds at score zero; positive recording lead-in. Both factories reject invalid values with `RangeError` before acquiring a context or constructing players. The reverse adapter absorbs the offset. |
| `driftToleranceSeconds` | option | `0.03` | Tolerance for follower divergence. |
| `driftCheckIntervalMs` | option | `250` | Drift cadence; 0 disables follower drift checks and re-joins, while the active-session watcher still handles natural master completion and group loops. |
| `loop` | option | `null` | SyncLoopRegion on the master axis. |
| `tick` | option | Kernel default | TickSourceOptions; nominal interval 25 ms with worker fallback. |
| `onOperationError` | option | none | `(operation, error) => void` observer for background command failures; observer throws are contained. |
**Factory-specific options**
| Member | Kind | Description |
| --- | --- | --- |
| `context` | optional AudioContext | Shared by the two factory-created players; borrowed if supplied, created/owned otherwise. |
| `scoreOptions` | ScorePlayer options | Forwarded with the chosen context; see the ScorePlayer page for the full engine/backend option contract. The Score-master factory validates an initial `tempo` as a finite positive BPM whose ratio to the Score base tempo is within 0.25–4×. The audio-master factory rejects a defined initial `tempo` because its master clock would replace that rate. Both factories reject `expandRepeats: true` for a Bridge-rendered clip. Use `sync.setRate()` for later coordinated changes. |
| `clipOptions` | Restricted AudioClipPlayer options | Both factories enforce the chosen context and buffer engine, reject conflicting engine/media-adapter choices, and require decoded samples. Defined clip-side `loop` / `rate` overrides are rejected, including `false` / `1`. These guards run before context/player construction. |
| `CreateSyncedPlaybackOptions` | type | Extends sync options with context and player options; clip options omit `loop`, `rate`, and `mediaAdapterFactory`, and permit only `engine: 'buffer'`. |
| `CreateAudioMasteredPlaybackOptions` | type | The reverse factory uses the same restricted clip option shape and runtime validation. Use top-level `loop` or `sync.setLoop()` for group loops, and `sync.setRate()` for shared rate changes. |
**Commands**
| Member | Kind | Description |
| --- | --- | --- |
| `dispatch(command)` | `Promise` | Await a revisioned play/pause/stop/seek/rate/loop command; `seek.position` and loop bounds use the stable master axis. Shared rate is 0.25–4. Current-operation failures reject; newer authority returns `superseded`. |
| `play()` | `Promise` | Starts both; resolves startup work, not playback completion. |
| `pause()` | void | Pauses participants, retaining position and authoritative pause intent. |
| `stop()` | void | Stops participants and resets transport according to their contract. |
| `seek(seconds)` | `Promise` | Finite nonnegative position on the current master axis; awaits master transition and follower join. |
| `setRate(rate)` | void | Finite 0.25–4 inclusive; invalid values throw RangeError. Rejoins followers through group coordination. |
| `setLoop(regionOrNull)` | void | Sets/clears a master-axis range; tick-detected wrap is not sample-accurate boundary scheduling. |
| `checkDrift()` | number | Runs a check and returns follower divergence in seconds; may enqueue correction. |
**State and lifecycle**
| Member | Kind | Description |
| --- | --- | --- |
| `snapshot` | TransportSnapshot | One frozen coherent observation: revision/referenceTime/position/rate/paused/holding/loop/pending/disposed. Position uses the master axis and is independent of rate-scaled duration. |
| `subscribe(listener)` | unsubscribe function | Synchronously delivers the initial snapshot, then command invalidation/settlement/errors, background reconciliation and disposal. Unsubscribe never disposes playback. |
| `seconds` | readonly number | Master-axis position, nominal Score seconds or clip seconds according to factory. |
| `clock` | SyncTransportClockView | Read-through master reader, or mirror fallback; not a shared writable instance. |
| `loop` | SyncLoopRegion or null | Current loop region. |
| `dispose()` | void | Idempotent; stops group work, invokes optional participant disposers, closes only a factory-created context. pause/stop after disposal are no-ops; active commands such as play/seek/setRate/checkDrift reject or throw. |
| `onOperationError` | observer option | Background failures also appear as `operation-error` subscription events. Read current snapshot/clock for animation; subscriptions do not emit frame-by-frame position ticks. |
**Master protocol: SyncScoreTransport / SyncMasterTransport**
| Member | Kind | Description |
| --- | --- | --- |
| `play(when?)` | void or `Promise` | Optional absolute reference-time start proposal. |
| `pause()` | void | Hold position. |
| `stop()` | void | Stop/reset. |
| `seekNominal(position, when?)` | void or `Promise` | Settling seek on this master axis; returning paused after an attempted running seek is treated as failed resume. |
| `setRate(rate)` | optional method | Apply group rate. |
| `nominalSeconds` | readonly number | Master-axis position; reverse adapter exposes clip seconds through this member. |
| `clock` | optional reader | Preferred affine map; absent readers use MirrorClockMaster. |
| `dispose()` | optional method | Called by group disposal. |
**Follower protocol: SyncClipTransport / SyncFollowerTransport**
| Member | Kind | Description |
| --- | --- | --- |
| `play(when?)` | void or `Promise` | Must honor future reference time for scheduled alignment. |
| `pause()` | void | Retract/hold playback before joining. |
| `stop()` | void | Stop/reset. |
| `seek(seconds)` | void | Synchronous positioning while paused, before play. |
| `setRate(rate)` | optional method | Align slope before offset. |
| `seconds` | readonly number | Follower-axis position. |
| `playing` | optional boolean | Running state for drift checks without a reader. |
| `clock` | optional reader | Used instead of sampling when available. |
| `dispose()` | optional method | Called by group disposal. |
**Adapters and related public types**
| Member | Kind | Description |
| --- | --- | --- |
| `clipAsMaster(clipPlayer)` | SyncMasterTransport | Adapts existing AudioClipPlayer; scheduled running seek uses pause → seek → play(when). Wrapper disposer forwards to the player. |
| `scoreAsFollower(scorePlayer, options?)` | SyncFollowerTransport | Adapts nominal Score time, retains requested offset state and defers entry before score zero. Wrapper disposer forwards to the player. |
| `AudioMasterAdapterOptions.clipOffsetSeconds` | option | Default 0; finite and nonnegative, with invalid values rejected by `scoreAsFollower` before creating an adapter. |
| `SyncLoopRegion` | type | `{startSeconds, endSeconds}` on the chosen master axis; finite valid increasing range. |
| `SyncTransportClockView` | type | Alias of Kernel TransportClockReader: state, rate, paused, holding, positionAt and timeAt. |
| `SyncTransportContract` | type-only assertion | Pins the real players to Bridge roles; it adds no runtime validation. |
**Composed by.** The demo above constructs this object directly. No current
custom element owns a declarative ScoreAudioSync composition.
## Related
[ScorePlayer]() · [AudioClipPlayer]() · [Bridge conversions]() · [Kernel contracts]()
---
# Bridge
> The only package allowed to import both families: what @webmusic/bridge is for, the rule that defines it, and where its sync object and stateless functions are documented.
Canonical documentation: https://koperative-lab.github.io/WebMusic/bridge/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/bridge@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`@webmusic/bridge` is the one package in the repository allowed to import
**both** domain families. That is not a convenience — it is the rule that
defines it. `@webmusic/score` and `@webmusic/audio` never depend on each
other, so anything that must know a `Score` _and_ an `AudioClip` at the same
time has exactly one home, and this is it.
## When you need it
These jobs need both domains:
- **Play a score and a recording together**, using one shared `AudioContext`
reference, one group command authority and drift correction —
`ScoreAudioSync` and its two factories. Each engine still owns its transport
clock.
- **Turn a score into audio**, offline — `renderScoreToClip`.
- **Convert between the two notions of time** — a Score `TimeMap` and an
Audio `BeatGrid`, in either direction.
- **Assemble a transcription into a `Score`** — `scoreFromTranscription`
over the family-neutral note events `@webmusic/audio/analyze/transcribe`
emits. Transcription itself stays in Audio precisely so that Audio never
needs to know what a `Score` is.
## Where it sits
```text
@webmusic/score ─┐
├─▶ @webmusic/bridge
@webmusic/audio ─┘
```
Both families are **peer dependencies**, along with `@webmusic/kernel`: the
bridge borrows the score and audio instances your application already has
rather than bundling its own. It publishes last, after both families.
## The smallest example
```ts
import { createSyncedPlayback } from "@webmusic/bridge";
const { sync } = createSyncedPlayback(score, clip);
await sync.play();
// … sync.seek(seconds), sync.setLoop({startSeconds, endSeconds})
sync.dispose();
```
One call builds both players on one shared context and returns them alongside
the sync that owns them. What each option does, and what `dispose()` releases, is on the
pages below.
## Documentation
| | |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| [Playback composition]() | A runnable score/audio session, borrowed views, source replacement and independent native loops. |
| [`ScoreAudioSync`]() | The stateful sync object: construction, commands, state, the error channel, and what it owns versus borrows. |
| [`@webmusic/bridge`]() | The entry reference — every root export, including the stateless conversion and rendering functions. |
## What's next here
Event and element bindings, so a `` can follow an
`` declaratively rather than through code. The bridge
gets no Web Components group until it ships an element.
---
# Introduction
> Documentation for the five WebMusic packages — Score for notation, Audio for digital audio, the Bridge that syncs them, and the shared UI and kernel foundations.
Canonical documentation: https://koperative-lab.github.io/WebMusic/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/kernel@0.1.0, @webmusic/ui@0.1.0, @webmusic/score@0.1.0, @webmusic/audio@0.1.0, @webmusic/bridge@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
WebMusic is a toolkit for music software, interactive demos, and web-based
musical installations. Use ready-made **Web Components**, drive **Headless
objects** with your own interface, or compose **APIs and UI presenters**
directly. These choices share data and runtime contracts so a prototype can
grow without replacing every layer.
Online music workstations and expressive installations are long-term
application goals. Project models, undo, persistence, and collaboration remain
application responsibilities to design on top of the toolkit.
Five TypeScript packages organize these capabilities. Two domain families —
[Score]() for symbolic music and [Audio]() for digital audio —
never depend on each other; what they share lives below them, and what needs
both lives above:
| Package | Role |
| --- | --- |
| [`@webmusic/score`]() | Symbolic music — MIDI, MusicXML, MXL, ABC: playback, views, analysis. |
| [`@webmusic/audio`]() | Digital audio — decoding, playback, waveforms, MIR analysis. |
| [`@webmusic/bridge`]() | The only package that knows both domains — synchronized playback, score→clip rendering, transcription. |
| [`@webmusic/ui`]() | Dependency-free DOM presenters and styling hooks both families compose. |
| [`@webmusic/kernel`]() | The zero-domain runtime kernel — contracts and pure logic everything builds on. |
Inside each family the docs follow the package's own vocabulary: **Web
Components** document the `/element` form, **Headless** documents
`/headless`, **API** documents the package root and its capability subpaths,
and **UI Kit** documents `@webmusic/ui`. Pick the family that matches your
source material: notation — MIDI, MusicXML, MXL, ABC — is Score; recorded
sound is Audio.
Participants performing together should follow one session time authority,
with explicit mappings between musical positions and audio time. The current
[Bridge]() coordinates separate clocks; shared-instance injection is
further work. Rendering chooses a sound realization, while transcription
estimates notes: conversion does not imply a lossless round trip.
Start with **[Quick Start]()**: a Score player with synchronized notation and keyboard, plus a compact Audio
example. [Score]() owns setup and registration; [Audio]() includes
its setup and walkthrough. [Agent Toolkit]() supplies coding-agent
references, skills and project instructions.
Search covers every package at once, so a term from one family will find the
other.
---
# @webmusic/kernel
> The ten kernel entries — events, element, worker, audio-context, player, effect, meter, transport, tick and sync — and the contracts each one carries.
Canonical documentation: https://koperative-lab.github.io/WebMusic/kernel/api/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/kernel@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
Every entry is import-safe in Node, workers, and SSR. The root entry
re-exports all of them; the subpaths exist so applications and bundlers can
take exactly what they use.
| Entry | Contract |
| --- | --- |
| [`/events`](#events--webmusickernelevents) | Hardened typed `EventEmitter` |
| [`/element`](#element--webmusickernelelement) | `WebMusicElement` connected-lifetime cleanup plus SSR-safe attribute and registration helpers |
| [`/worker`](#worker--webmusickernelworker) | `WorkerLike` + `RequestTracker` correlation plumbing |
| [`/audio-context`](#audio-context--webmusickernelaudio-context) | SSR-safe AudioContext construction |
| [`/player`](#player--webmusickernelplayer) | `PlayerLike` playback control contract |
| [`/effect`](#effect--webmusickerneleffect) | `Effect`/`EffectNodes` over `BaseAudioContext` |
| [`/meter`](#meter--webmusickernelmeter) | Shared analyser meter contract and implementation |
| [`/transport`](#transport--webmusickerneltransport) | `TransportClock` affine anchor math + `TimelineMapping` |
| [`/tick`](#tick--webmusickerneltick) | Throttling-resistant tick delivery for lookahead schedulers |
| [`/sync`](#sync--webmusickernelsync) | Transport grouping, timeline mapping and drift coordination |
## events — @webmusic/kernel/events
`EventEmitter` is a typed emitter with two delivery modes: `emit`
propagates listener throws to the caller, while `emitSafely` snapshots the
listener list, keeps delivering after a faulty observer, and reports both
synchronous throws and async rejections through a failure callback.
```ts
import {EventEmitter} from '@webmusic/kernel/events';
const events = new EventEmitter<{tick: {beat: number}}>();
const off = events.on('tick', ({beat}) => console.log(beat));
events.emit('tick', {beat: 1});
off();
```
Both families re-export this implementation as their public `EventEmitter`.
## element — @webmusic/kernel/element
`WebMusicElement` is a minimal custom-element base owning one concern:
connected-lifetime cleanup. `onMount()` runs per connection and may return a
cleanup; `own()` registers additional cleanups that drain in LIFO order on
disconnect, with rollback if mounting fails partway. It owns no Shadow DOM,
no profile, no presenter, and no domain state.
```ts
import {WebMusicElement, defineOnce} from '@webmusic/kernel/element';
class MyBadge extends WebMusicElement {
protected override onMount() {
const timer = setInterval(() => this.refresh(), 1000);
return () => clearInterval(timer);
}
refresh() {}
}
defineOnce('my-badge', MyBadge);
```
Also here: `upgradeProperty`/`upgradeProperties` for the
set-before-upgrade pitfall, `numAttr`/`boolAttr`/`cssSizeAttr` attribute
readers, and `defineOnce` idempotent registration. Importing this entry
registers nothing and is SSR-safe; subclassing works in Node.
## worker — @webmusic/kernel/worker
`WorkerLike` is the minimal structural surface a worker must expose
(`postMessage` with transfer, `terminate`, message and error listeners), so
clients can accept a real `Worker`, a shim, or a test double.
`createRequestTracker()` correlates request/response pairs across the
message channel, with timeout, abort, and dispose paths that always settle
pending requests.
## audio-context — @webmusic/kernel/audio-context
`createWebAudioContext(missingMessage, options?)` constructs an
`AudioContext` where available and throws the caller's message where not
(SSR, Node), keeping AudioContext construction out of import-time paths.
`getAudioContextConstructor()` returns the constructor or `undefined` for
feature-testing.
## player — @webmusic/kernel/player
`PlayerLike` is the lowest-common-denominator playback control contract both
families' players satisfy:
```ts
interface PlayerLike {
play(): Promise | void;
pause(): void;
stop(): void;
seek(seconds: number): void;
setRate?(rate: number): void;
setVolume?(volume: number): void;
readonly seconds?: number;
readonly duration?: number;
readonly playing?: boolean;
on(event: string, listener: (data: unknown) => void): () => void;
dispose?(): void;
}
```
The optional members are deliberate: the base contract is the lowest common
denominator both families can always promise. The named **capability tiers**
and their guards are the supported feature-testing protocol:
| Capability interface | Adds | Guard |
| --- | --- | --- |
| `TimedPlayerLike` | `seconds`, `duration` | `isTimedPlayer()` |
| `StatefulPlayerLike` | `playing` | `isStatefulPlayer()` |
| `RateControlledPlayerLike` | `setRate()` | `isRateControlledPlayer()` |
| `VolumeControlledPlayerLike` | `setVolume()` | `isVolumeControlledPlayer()` |
| `DisposablePlayerLike` | `dispose()` | `isDisposablePlayer()` |
A generic consumer narrows once with a guard and gets the non-optional
members; a failed guard means the capability is absent, never an error.
Implementers advertise a tier by making its members non-optional. Both
families pin their players against the tiers they advertise at the type
level (`ScorePlayer`/`TonePlayer`: timed, rate-controlled, disposable;
`AudioClipPlayer`: all five), so drift fails `typecheck`.
```ts
import {isTimedPlayer, type PlayerLike} from '@webmusic/kernel/player';
function renderProgress(player: PlayerLike): number | undefined {
return isTimedPlayer(player) ? player.seconds / player.duration : undefined;
}
```
## effect — @webmusic/kernel/effect
`Effect` describes an audio post-processing unit over `BaseAudioContext`:
`createAudioNodes(context)` returns an `EffectNodes` value — an
`input`/`output` node pair, optionally live `params` (`AudioParam`s for
real-time control) and a `dispose`. An optional `label` lets UIs caption
controls. Both families' effect chains and the score synth panel speak this
contract.
## meter — @webmusic/kernel/meter
`AnalyserMeter` owns the transparent `input → analyser → output` tap both
families' level meters read, plus the byte-domain arithmetic over it: RMS,
scaled `level`, instantaneous `peak` and a decaying `peakHold`. It decides
nothing about *when* a frame is read — a presenter, canvas renderer or
framework drives that — and it holds no score, clip or DOM.
`aggregateSpectrumBars()` is the same maths for the frequency domain, folding
FFT bins into a bar count. Both existed once per family and had begun to
drift; only the mechanism moved here, while genuinely editorial differences
(minimum bar count, the frame shape each family publishes) stayed with the
families.
```ts
import {createAnalyserMeter} from '@webmusic/kernel/meter';
const meter = createAnalyserMeter({context, levelScale: 1.4, peakDecay: 0.02});
sourceNode.connect(meter.input!); // owned tap: input → analyser → output
meter.output!.connect(context.destination);
const {level, peak, peakHold} = meter.readLevel(); // call it from your own frame loop
meter.configure({fftSize: 2048, smoothingTimeConstant: 0.5}); // ports stay connected
```
`configure(options?: AnalyserMeterConfiguration)` accepts the tuning fields of
`AnalyserMeterOptions` (`fftSize`, `smoothingTimeConstant`, `levelScale`,
`peakDecay`, `minimumBars`), excluding source ownership. Omitted fields retain
their current values. Owned FFT size must be a power of two from 32 to 32768,
and smoothing must be finite in 0–1; a rejected candidate leaves previous
settings and ports intact. Borrowed analyser settings are never assigned.
The display scalars retain constructor normalization. Configuration after
`dispose()` throws.
For compatibility, live `peakHold` is `max(level, previousHold - peakDecay)`: it
holds scaled RMS with an absolute decrement per read. `peak` is a separate
scaled sample peak. These byte-analyser display measurements are not calibrated
loudness/true-peak measurements or decay measured in seconds.
## transport — @webmusic/kernel/transport
`TransportClock` is pure affine anchor math over an injectable clock: `start`
/ `startAt` / `pause` / `seekTo` / `setRate` update the anchor, and
`positionAt(t)` / `timeAt(position)` convert between host time and timeline
position without accumulating drift. An armed-start hold covers pre-roll
(`holding`), and `TransportClockReader` is the read-only view engines expose
to consumers. Both families' schedulers anchor on it, and the
[bridge]() reads the two clocks for skew-free synchronization.
`TimelineMapping` (`positionToSeconds`/`secondsToPosition`) is the structural
contract for musical-position-to-seconds mappings; score's `TimeMap` and
audio's `BeatGrid` both satisfy it.
## tick — @webmusic/kernel/tick
`createTickSource(options)` delivers ticks on a cadence that survives
background-tab timer throttling: an inline-Blob interval worker where the
environment allows it (with the Blob URL revoked on both success and CSP
failure), falling back to a main-thread interval otherwise. Lookahead
schedulers in both families and the bridge run on it.
```ts
import {createTickSource} from '@webmusic/kernel/tick';
const ticks = createTickSource({intervalMs: 25});
ticks.start(() => schedulerPass());
// later: ticks.stop() to pause, ticks.dispose() to release the worker
```
## sync — @webmusic/kernel/sync
`TransportGroup` drives one master transport and any number of followers on a
shared lead-in, checking drift on a cadence and re-seeking a follower that has
slipped past tolerance. `SyncMasterTransport` and `SyncFollowerTransport` are
the structural ports it accepts, so anything exposing that surface can join —
which is how the [bridge]() synchronizes a Score player against an
Audio clip without either package importing the other. Commands are
serialized, so a late async settlement cannot reorder state.
### Session commands and observation
The group is one command authority on the master's **stable position axis**.
`dispatch()` uses the same participant engines as the existing controls; it
creates no second player or writable clock. Commands should go through the group
while those participants form a session. Direct engine changes cannot receive a
group revision until observed reconciliation detects them.
| Member | Contract |
| --- | --- |
| `dispatch(command: TransportCommand)` | Returns `Promise`. Play, seek and rate wait for participant preparation/re-entry; pause/stop take effect immediately and await late-start reconciliation. Invalid commands throw before changing revision. Current-operation failure rejects, publishes `error`, and attempts to pause participants after partial application. Cleanup continues if an adapter throws; a superseded failure cannot override a newer command. |
| `snapshot` | `TransportSnapshot` read at one reference instant: `revision`, `referenceTime`, stable `position`, `rate`, `paused`, pre-roll `holding`, `loop`, `pending`, `disposed`. This is a frozen value, not a ticking event source. |
| `subscribe(listener)` | Registers before synchronously delivering `{type: 'snapshot', snapshot}`; returns an idempotent unsubscribe function. An initial listener failure removes its subscription; subsequent listener failures are contained. Detaching leaves playback alive. |
| `TransportCommand` | `{type: 'play'}`, `{type: 'pause'}`, `{type: 'stop'}`, `{type: 'seek', position}`, `{type: 'rate', rate}`, or `{type: 'loop', loop}`. Positions/bounds use the master's content axis; rate is finite and positive. `loop` is `{startSeconds, endSeconds}` or `null`; bounds must be finite, nonnegative and ordered. |
| `TransportCommit` | `{revision, command, status, snapshot}`. `status` is `committed` or `superseded`: a newer command, background reconciliation or disposal prevents old work claiming the current authority. Settlement describes the adapters' work, not proof of audible output. |
| `TransportEvent` | Initial/disposal `snapshot`; `invalidate` before a command with its revision/command/snapshot; `commit` with its result/snapshot; `error` with revision/command/error/snapshot; background `reconcile` with operation, `invalidate` or `settled` phase and snapshot; `operation-error` with operation/error/snapshot. |
`dispatch({type: 'rate', rate})` checks accepted rates where readers expose them;
transports without observable rate state cannot prove they honored a request.
A master that remains paused after a running seek is a rejected dispatch, even
when its adapter resolves instead of throwing. Older `play`, `pause`, `stop`,
`seek`, `setRate` and `setLoop` methods retain their signatures and compatibility
behavior, including best-effort optional rate matching. Their changes also pass
through the observation surface; use `dispatch` when command failure must be
awaited. Background errors are visible through both `operation-error` events and
the optional `onOperationError` construction callback.
Use `snapshot.position` or `clock.positionAt(referenceTime)` for moving display
positions. Subscription events describe state transitions rather than animation
frames. Invalidation precedes engine commands: engines cancel and re-arm through
their existing methods. It is not a callback enabling unrelated engines to share
a writable `TransportClock`. Disposal publishes a final snapshot, removes
observers and invokes registered optional participant disposers; its snapshot
retains the last readable position and is not a continuing playback observation.
### Participant-local time and native loops
`addFollower(transport, options?)` accepts `FollowerOptions`:
| Option | Default | Meaning |
| --- | --- | --- |
| `offsetSeconds` | `0` | Finite nonnegative local position at master zero. |
| `positionScale` | `1` | Finite positive scale; local position is `offset + masterPosition × scale`, local rate is `masterRate × scale`. A non-unit scale requires `setRate`. |
| `loop` | absent | `{startSeconds, endSeconds}` in the follower's local seconds, for a participant **already configured to loop natively**. This option maps phase; it does not enable a loop on the transport. |
Before the local loop end, intro positions pass through. At/past the end, local
positions fold into `[startSeconds, endSeconds)`. Scheduled joins and seeks use
this mapped phase; drift reads normalize wrapped or unwrapped clocks and compare
shortest circular phase error, converted back to master units for the tolerance.
Several followers may have different periods while the master remains continuous.
All mappings and loops are copied on registration; remove/re-add to change them.
`removeFollower` leaves that participant's current playback/resources with its
caller, while group disposal invokes the disposers of remaining members.
These mappings support affine scaling and native-loop phase. They do not consume
arbitrary nonlinear `TimelineMapping`, implement audio warping, or make tick-watched
**group** loops sample accurate. A native-loop adapter must expose consistent
local seconds, rate and scheduling behavior; incorrect metadata can produce
incorrect alignment even when its shape is valid.
`MirrorClockMaster` adapts a transport that has **no readable clock** into the
master role: it dead-reckons a mirror `TransportClock` across the command
surface and reconciles it against the transport's real position at drift checks
or periodic lifecycle samples when drift checks are disabled. Once forward
motion has established an observed position step, a long enough stationary
interval can report `stalled`; before any observed motion, repeated positions
alone are inconclusive. A moving transport that diverged re-anchors the mirror.
This is the one home of the sampled fallback
the bridge used to interleave through its clocked path.
### Element target discovery
`observeElementTarget(host, selector, listener)` is exported from
`@webmusic/kernel/element`. It resolves one unique target within the host's current
Document or ShadowRoot and calls `listener(target, state)` immediately. The exported
`ElementTargetState` is `ready`, `missing`, `invalid` or `ambiguous`; unsuccessful
resolution supplies `undefined`, never an arbitrary matching element.
Discovery follows insertion, removal, relevant attribute changes, replacement and
late custom-element definition while attached. A target changing a non-reflected
source property can dispatch a bubbling `webmusic:sourcechange` event to request a
fresh connection. The helper contains no musical models or playback policy.
The returned idempotent cleanup stops observation and ignores pending upgrade
callbacks. Call it on host disconnection, selector changes or moving the host to
another root; create a new observation after reconnecting. An initial listener
exception releases the observer before being rethrown. This helper never disposes
the selected target or grants authority to its commands.
---
# Kernel
> Domain-neutral contracts and runtime utilities for Score, Audio, Bridge and custom integrations.
Canonical documentation: https://koperative-lab.github.io/WebMusic/kernel/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/kernel@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`@webmusic/kernel` supplies domain-neutral contracts and runtime logic for
WebMusic players and custom integrations. It contains event emitters,
custom-element and worker lifecycle helpers, AudioContext construction,
playback and effect contracts, and transport math. It imports no Score or Audio model.
## Install
```sh
npm install @webmusic/kernel
```
Kernel has no runtime or peer dependencies. Node.js 22 or later is supported;
ES modules and CommonJS use the same public import paths. Browser-specific
operations require the relevant Web APIs, while imports remain safe during SSR.
## Source workspaces
The source tree contains five packages. Kernel provides shared runtime
contracts for both domain families:
| Package | Role | Workspace peer dependencies |
| --- | --- | --- |
| `@webmusic/kernel` | Domain-neutral runtime contracts and pure logic | — |
| [`@webmusic/ui`]() | Dependency-free DOM presenters and styling hooks | — |
| [`@webmusic/score`]() | Symbolic music — MIDI, MusicXML, MXL, ABC | Kernel required; UI optional |
| [`@webmusic/audio`]() | Digital audio — clips, playback, analysis and views | Kernel required; UI optional |
| [`@webmusic/bridge`]() | Score/Audio coordination and conversion | Kernel, Score and Audio required |
The earlier published release included Kernel, UI and Score. Source presence
does not establish that Audio and Bridge are available in the npm registry.
## Shared contracts and explicit instances
Score and Audio declare Kernel as a required peer so the application resolves a
compatible runtime contract. UI is optional for code-only use and is required
by Elements and UI-backed renderers. Bridge declares the domain packages and
Kernel as required peers.
A compatible dependency version does not allocate or share an AudioContext,
player or transport group. Applications explicitly create, pass and release
those runtime instances according to the owning API. A custom player can
implement `PlayerLike` structurally, and Element authors can reuse
`WebMusicElement` for connected-lifetime cleanup.
## The admission rule
Code qualifies for the kernel only if:
1. existing duplicated implementations demonstrate a reusable need, or
2. it is a domain-neutral contract shared by compatible participants.
Domain behavior stays with its owning package. The extraction ledger — what
moved here, from where, and what stayed behind — lives in the repository's
`platform/README.md`.
## For third parties
Implementing a WebMusic-compatible player, effect, or timeline mapping means
depending on `@webmusic/kernel` directly and implementing its structural
contracts — no domain package required. Element authors can subclass
`WebMusicElement` for connected-lifetime cleanup with SSR-safe imports.
See the [API reference]() for the ten capability subpaths and their
contracts.
---
# Quick Start
> Use WebMusic's npm packages to build a score player with synchronized notation and a three-octave piano keyboard.
Canonical documentation: https://koperative-lab.github.io/WebMusic/quick-start/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/kernel@0.1.0, @webmusic/ui@0.1.0, @webmusic/score@0.1.0, @webmusic/audio@0.1.0, @webmusic/bridge@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
Build a player for *Arabesque No. 1*, supplied as a compressed MusicXML score.
One player loads the file; a staff view follows its position and a three-octave piano keyboard
highlights sounding notes within its C3–C6 range. Playback uses the built-in
oscillator, so no SoundFont is needed.
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/quick-start/)
Press **Play** to enable sound. Try pausing and seeking with the player's slider:
both views follow the same player. The keyboard is a visual readout; for playable
keys, see [``]().
## Install the packages
In your browser application, install WebMusic's Score package and its Kernel
and UI dependencies:
```bash
npm install @webmusic/score @webmusic/kernel @webmusic/ui
```
Score supplies the musical behavior and Web Components; Kernel supplies runtime
primitives, and UI supplies their controls and views. The example below uses
TypeScript and an ES-module bundler such as Vite to resolve the package imports.
## Add the player and its views
Download [Arabesque No. 1 (MXL)]() and save it as `public/arabesque.mxl` in your
application. The player reads compressed MusicXML directly; no manual extraction
is needed. The example assumes that your application serves this file at
`/arabesque.mxl`; set `src` to your file's public URL.
Put this markup in `index.html`. The player owns the file load, musical state
and sound; the two `player="#piece"` selectors attach views to that same instance.
```html
```
## Register the components
Add this to `src/main.ts`. Importing the `/element` entries gives you the
registration functions; calling them makes the three tags available to the
browser.
```ts
import {defineScorePlayerElement} from '@webmusic/score/play/element';
import {definePitchViewElement, defineScoreViewElement} from '@webmusic/score/view/element';
defineScorePlayerElement();
defineScoreViewElement();
definePitchViewElement();
```
## How it works
| Code | What it does |
| --- | --- |
| `` | Loads the MusicXML once and owns playback, sound and the current position. Its controls let you play, pause and seek. |
| `time-control="full"` | Shows elapsed time and total duration beside the player controls. |
| `` | Borrows the loaded score and playback position to draw notation with a following playhead and active-note highlights. |
| `` | Borrows the same player's sounding notes and highlights their piano keys. |
| `low="48" high="84"` | Sets the keyboard's MIDI endpoints to C3 and C6: a three-octave span with both end keys included, for 37 keys. |
| `white-key-height="128" black-key-height="80"` | Sets white keys to 128 px tall and black keys to 80 px tall. |
| `fit-to-width="true"` | Fits all three octaves to the container width without horizontal scrolling; key heights stay unchanged. |
Keep `id="piece"` unique within the document. The `player="#piece"` selectors
connect both views to that player, so pausing or seeking updates their display
automatically. You do not need to load the score again or synchronize a second
player. To use another piece, change the player's `src`; the views follow its
replacement score and note state.
The staff view provides lightweight notation. For detailed engraving, see
[``]().
Add loading and error feedback
The bare player publishes readiness and load errors through `.playback`; it
does not draw those messages itself. To add the same feedback as the demo,
place a status element directly after `` in `index.html`:
```html
```
Add this helper to `src/main.ts` after the registration code. It shows loading
or failure messages and hides the status element once the score is ready.
```ts
import type {ScorePlayerElement} from '@webmusic/score/play/element';
/** Show loading and failures; keep the status row hidden during normal playback. */
export function mountQuickStartStatus(player: ScorePlayerElement, status: HTMLElement): () => void {
let lastState = '';
const write = (message: string): void => {
status.hidden = message.length === 0;
if (status.textContent !== message) status.textContent = message;
};
const message = (error: unknown): string => error instanceof Error ? error.message : String(error);
const unsubscribe = player.playback.subscribe((snapshot) => {
const state = `${snapshot.sourceRevision}:${snapshot.readiness}:${snapshot.state}`;
if (state === lastState) return;
lastState = state;
if (snapshot.readiness === 'error') {
write(`Could not load the score: ${message(snapshot.error)}`);
} else if (snapshot.readiness !== 'ready') {
write('Loading score…');
} else {
write('');
}
});
const onError = (event: Event): void => {
const {error} = (event as CustomEvent<{error: unknown}>).detail;
write(`Playback failed: ${message(error)}`);
};
player.addEventListener('webscore:error', onError);
return () => {
unsubscribe();
player.removeEventListener('webscore:error', onError);
};
}
```
Then call it at the end of `src/main.ts`:
```ts
const player = document.querySelector('#piece')!;
const status = document.querySelector('#player-status')!;
let detachStatus = mountQuickStartStatus(player, status);
window.addEventListener('pagehide', () => {
detachStatus();
player.stop();
});
window.addEventListener('pageshow', (event) => {
if (event.persisted) detachStatus = mountQuickStartStatus(player, status);
});
```
The Play click supplies the browser's user activation for sound. A missing or
invalid file now produces a visible message. Check the requested URL in the
browser's Network panel; replace `src` or reload the page to retry after fixing
the file. A failed playback command also appears in the status text.
For a framework component, call `detachStatus()` in its unmount cleanup.
Removing the player from the DOM releases its owned playback resources;
removing either view releases only that view's bindings and rendering resources.
Use the [Score setup]() for other formats, registration and integration options.
## Keep the behavior and change the interface
- **Use your own buttons:** see the
[ScorePlayer engine and its commands]().
- **Reuse controls with custom bindings:** see the
[Transport presenter]().
- **Add analysis:** attach companions through the
[shared playback source]().
Full references: [Score player]() ·
[Score view]() ·
[Pitch view]().
To build with a coding agent, see [Agent Toolkit]() for reference
selection, task prompts and project instructions.
## Play recorded audio
For recorded sound, install `@webmusic/audio` alongside Kernel and UI:
```bash
npm install @webmusic/audio @webmusic/kernel @webmusic/ui
```
Download the [Arabesque No. 1 WAV recording](), save it as `public/arabesque.wav`,
and add the player to your browser application:
```html
```
Click **Play** to enable audio. This example uses built-in WAV decoding and
needs no optional decoder. The player owns its file and playback resources;
removing it releases them. Check its error state and the URL if loading fails.
A Score player and an Audio player run independently. To coordinate them, use
the [Bridge composition]().
For custom controls and API + UI composition, see [Audio setup](),
[Audio playback](), and the
[Audio player reference]().
---
# @webmusic/score/analyze
> Score analysis — stateless algorithms, code-only sessions and live trackers, plus styled analysis Web Components.
Canonical documentation: https://koperative-lab.github.io/WebMusic/score/api/analyze/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/score@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`@webmusic/score/analyze` is the **analysis** capability of the `@webmusic/score` package. It reads `Score` objects from `@webmusic/score` and returns plain data — a score summary, chord timelines, key detection, chord segmentation, intervals, meter-aligned rhythm inspection, roman numerals, motif search, rhythm patterns, and voice-leading checks. Its interactive Web Components compose those results around a player; the [Analyze overview]() selects the component for each task.
The capability root and `/headless` entry have no DOM dependency, so they run in Node, workers, browsers, tests, import pipelines, and batch jobs. It does not play audio, render notation, or own a framework — those live in [`@webmusic/score/play`](), [`@webmusic/score/view`](), and [`@webmusic/score/react`]().
## Install
`npm install @webmusic/score` — use an available published version or the local workspace; [Install]() has the full setup.
The root model and this capability ship in that one package. A Web Component that loads a file from `src` dynamically uses the sibling `@webmusic/score/io` capability. Chord naming is powered by the bundled `@tonaljs/chord-detect` and `@tonaljs/chord`.
The analysis capability separates stateless analysis, stateful resources and UI:
The `@webmusic/score/analyze` root is the canonical stateless surface. `/headless` contains stateful code-only analysis components — the incremental session, live note-event trackers and Worker client — without UI, DOM access or styles. `/element` contains the styled Web Components. The elements reuse the public live trackers and keep score loading, DOM player binding, playhead state and rendering private. Each public entry tree-shakes independently; only `/worker` self-registers a runtime listener.
---
## Web Components
Two inspection surfaces are exported by `@webmusic/score/analyze/element`: passage harmony and the current sounding chord. Each owning page under [Web Components › Analyze]() explains its attributes, events, player binding and styling.
Whole-score summaries, histograms and rhythmic-vocabulary reports use the [report workflow](). Tonal estimation, selected intervals, metrical placement, specialist Roman and voice-leading displays, pitch diagnostics and retained chord history can be composed from the code-only results below or the [live trackers and projections](). Motif and recurring-rhythm algorithms remain code-only building blocks for application-owned displays.
The score algorithms below take a `Score` (from an importer, or built with `@webmusic/score`) and return plain data. The following examples assume a `score` that holds a I–IV–V–I progression in C major.
## Summarize a score
`summarizeScore` returns metadata, counts, duration, tempo, meter, key, and pitch range — handy for logs, APIs, CLI tools, and background jobs.
```ts
import { summarizeScore } from '@webmusic/score/analyze';
const summary = summarizeScore(score);
console.log({
title: summary.title,
parts: summary.parts,
measures: summary.measures,
notes: summary.notes,
durationSeconds: summary.durationSeconds,
tempoBpm: summary.tempoBpm,
timeSignature: summary.timeSignature,
pitchRange: summary.pitchRange,
});
```
| Field | Description |
| --- | --- |
| `title`, `composer` | Score metadata, when present. |
| `parts`, `measures`, `notes` | Part/measure counts and sounding-note count; explicit rests are excluded from `notes`. |
| `durationQuarters`, `durationSeconds` | Total length in quarters and seconds. |
| `tempoBpm`, `timeSignature`, `keySignature` | First tempo, meter, and key. |
| `pitchRange` | `{ low, high }` spelled pitches, when notes exist. |
## Distributions
`distributions(score): Distributions` returns three whole-score histograms. It is pure, accepts no options and retains no state. Each `DistributionBin` contains a display `label`, numeric `key` and numeric `value`.
```ts
import {distributions} from '@webmusic/score/analyze';
const result = distributions(score);
const totalWeight = result.pitchClasses.reduce((sum, bin) => sum + bin.value, 0);
const pitchShares = result.pitchClasses.map((bin) => ({
pitchClass: bin.key,
label: bin.label,
share: totalWeight > 0 ? bin.value / totalWeight : 0,
}));
console.table(pitchShares);
```
| Result | Bin key | Bin value and order |
| --- | --- | --- |
| `pitchClasses` | Pitch class `0`–`11` | Pitched-note duration weights in integer ticks, in chromatic order. Always 12 bins; non-pitched notes are omitted. |
| `intervals` | Signed melodic interval in semitones | Occurrence counts, sorted by interval. Each voice is scanned separately and notated rests break the chain. |
| `durations` | Duration in quarter notes | Note counts, sorted by duration. Exact rational durations form bins before conversion to numeric keys. |
Normalizing pitch-class weights uses their sum: total pitched-note duration weight, including overlapping voices. It is not the score’s elapsed length. Empty input has zero pitch weights and empty interval/duration lists. The [report demo]() renders all three results.
## Chord timeline scan
`chordTimeline` groups every pitched note overlapping a fixed musical window,
including notes that do not sound simultaneously, and names the combined set.
It shares the Tonal-backed naming policy with [`segmentChords`](#segment-chords),
but answers a coarser question. It also emits parsed `root` and `quality` when
recognized, `N.C.` below the pitch-class threshold or when unnamed, and `rest`
for a window with no pitched notes.
`windowQuarters` defaults to `1` and clamps to at least `0.125`; `minNotes`
counts distinct pitch classes, defaults to `2`, and clamps to at least `1`
(a fractional threshold effectively rounds up). Non-finite values throw
`RangeError`, including for an empty score. `mergeAdjacent` defaults to `true`:
equal adjacent labels share one segment whose `notes` are the deduplicated
union of contributing notes and whose `pitchClasses` cover the whole span.
These notes are evidence across the span, not a claim that they all overlap.
Set `mergeAdjacent: false` to retain each window's evidence separately.
```ts
import { chordTimeline } from '@webmusic/score/analyze';
const chords = chordTimeline(score, { windowQuarters: 1, minNotes: 2 });
console.table(
chords.map((segment) => ({
start: segment.startQuarters,
end: segment.endQuarters,
chord: segment.chord,
})),
);
```
## Detect key
`detectKey` estimates the tonal center and mode from pitch-class duration weights (a Krumhansl-style profile correlation). The result includes the best key, a confidence value, and the ranked candidate scores.
```ts
import { detectKey } from '@webmusic/score/analyze';
const key = detectKey(score);
const values = {
tonic: key.tonic,
mode: key.mode,
confidence: Number(key.confidence.toFixed(3)),
top3: key.scores.slice(0, 3).map((candidate) => `${candidate.tonic} ${candidate.mode}`),
};
```
Output:
```json
{
"tonic": "C",
"mode": "major",
"confidence": 0.607,
"top3": ["C major", "G major", "E minor"]
}
```
## Segment chords
`segmentChords` places boundaries at note onsets and offsets and labels the
pitched notes sounding over each half-open interval. `windowQuarters` defaults
to `2` and clamps to at least `0.125`; it caps intermediate pieces before
contiguous equal labels merge, so final segments may be longer. Non-finite
windows throw `RangeError`. Silence separates equal chords rather than merging
them across a gap. Its timing differs from the aggregate `chordTimeline` scan.
```ts
import { segmentChords } from '@webmusic/score/analyze';
const segments = segmentChords(score, {
windowQuarters: 4,
});
const values = segments.map((segment) => ({
startQuarters: segment.startQuarters,
endQuarters: segment.endQuarters,
chord: segment.chord,
pitchClasses: segment.pitchClasses,
}));
```
Output:
```json
[
{ "startQuarters": 0, "endQuarters": 4, "chord": "CM", "pitchClasses": [0, 4, 7] },
{ "startQuarters": 4, "endQuarters": 8, "chord": "FM", "pitchClasses": [0, 5, 9] },
{ "startQuarters": 8, "endQuarters": 12, "chord": "GM", "pitchClasses": [2, 7, 11] },
{ "startQuarters": 12, "endQuarters": 16, "chord": "CM", "pitchClasses": [0, 4, 7] }
]
```
## Pitch basis and transposing instruments
Score-based analysis currently reads stored `Note.pitch` values and does not
automatically apply `Part.transpose`. This affects chord/key pitch evidence,
pitch distributions and cross-part voice comparisons, including the incremental
analysis session. A mixed score for concert-pitch and transposing instruments
must not be interpreted as a reliable concert-pitch harmony analysis without
first creating an explicit concert-pitch analysis input.
For example, piano C/E plus B♭ clarinet written A actually sounds C/E/G, but
the current Score path analyzes C/E/A. `soundingPitch(note, part)` from the root
API can convert individual pitches for an application-owned analysis copy;
clear the copy's part transposition to avoid applying it again in playback.
Keep the original notated Score for notation. There is no exported whole-Score
normalizer or pitch-basis option yet. Direct `identifyChord(notes)` has no Part
context; `identifyChordFromMidi` and live trackers analyze the actual MIDI values
supplied by the caller.
## Identify a supplied chord
`identifyChord(notes)` filters to pitched notes, orders them by MIDI bass, and
retains their notated names. `identifyChordFromMidi(midis)` instead uses a fixed
mixed sharp/flat spelling. Both deduplicate octave doublings and return `''`
for an empty set, or a space-separated pitch-class list when unnamed.
Both paths share the existing spelling projection's candidate policy: Tonal
may assume a missing perfect fifth, parsed candidates must explain every
supplied pitch class, and ordinary major inversions rank ahead of the
ambiguous `minor augmented` interpretation. Thus E–G–C prefers `CM/E` rather
than `Em#5`. This is a naming preference, not proof of the music's intended
root. The direct functions return only the preferred label; the Headless
spelling projection also exposes alternatives and optional rootless readings.
No staff or fretboard projection is computed to obtain a label.
## Inspect selected intervals
`analyzeIntervals(score, selection, options?)` relates authored notes rather
than guessing an interval from a whole-score pitch histogram. Select note IDs,
a part/voice, a half-open quarter-note range, or a combination. Consecutive
pitched onsets in one voice form melodic intervals; concurrently sounding
pairs form vertical intervals. Explicit rests break melodic continuity.
```ts
import {analyzeIntervals} from '@webmusic/score/analyze';
const inspection = analyzeIntervals(score, {
fromQuarters: 0,
toQuarters: 4,
});
const intervals = inspection.intervals.map((entry) => ({
kind: entry.kind,
label: entry.label,
semitones: entry.semitones,
notes: entry.evidence.map((note) => note.noteId),
}));
```
The default `pitchMode: 'written'` uses stored notation. Set
`{pitchMode: 'sounding'}` to apply each part's declared transposition;
chromatic-only transpositions fall back to MIDI-derived spelling. Each result
has a stable note-pair ID, kind, spelled label such as `3M`, direction, signed
melodic or non-negative vertical semitones, quarter-note span, and two evidence
records with note/part/voice IDs and written/analyzed pitches. A range must
include both finite, ordered bounds. Dense chords produce pairwise vertical
results, so use a narrow selection when presenting them interactively. The
function does not edit notes, create a player, or own resources.
## Roman numerals
`romanNumerals` takes a key and expresses the chord progression relative to that key.
`romanNumeralForChord(symbol, key)` applies the same rule to one symbol.
`RomanNumeralOptions.segments` can reuse an existing chord segmentation instead
of recomputing it; its bounds and labels are retained.
Degrees use the major or natural-minor pitch-class scale, then prefer a lowered
neighbor for chromatic roots. Case and suffix retain chord quality (`ii7`,
`Imaj7`, `viiø7`, `III+`); slash basses do not add inversion figures. This is a
coarse pitch-class-relative label, not contextual harmonic analysis: for
example, B major in C minor produces `bI` under the flat-neighbor rule.
Unparsable chord or key names return the original chord label. Secondary
functions, modulation and figured-bass inference are outside this API.
```ts
import { romanNumerals } from '@webmusic/score/analyze';
const roman = romanNumerals(score, { tonic: 'C', mode: 'major' }, {
windowQuarters: 4,
});
const values = roman.map((entry) => ({
chord: entry.chord,
roman: entry.roman,
startQuarters: entry.startQuarters,
endQuarters: entry.endQuarters,
}));
```
Output:
```json
[
{ "chord": "CM", "roman": "I", "startQuarters": 0, "endQuarters": 4 },
{ "chord": "FM", "roman": "IV", "startQuarters": 4, "endQuarters": 8 },
{ "chord": "GM", "roman": "V", "startQuarters": 8, "endQuarters": 12 },
{ "chord": "CM", "roman": "I", "startQuarters": 12, "endQuarters": 16 }
]
```
## Find motifs
`findMotifs` searches each part's voices separately for repeated intervals and
exact notated durations; a phrase and its transposition match. Same-onset chord
members reduce to the highest pitch, and explicit rest/unpitched-only events
break phrases. Silent gaps without an explicit rest are not inferred as phrase
boundaries. At least two occurrences sharing no note are required, although
returned occurrences include all matching overlapping windows. Repeated-pitch
patterns (including length `1`) are omitted. IDs identify this scan's grouping;
`partId` and `noteIndexes` identify each occurrence's source notes.
An application can project these results into its own occurrence display.
This example assumes `melodyScore` contains `C D E G | C D E G` as quarter notes.
```ts
import { findMotifs } from '@webmusic/score/analyze';
const motifs = findMotifs(melodyScore, {
length: 4,
minOccurrences: 2,
});
const values = motifs.map((motif) => ({
intervals: motif.intervals,
rhythm: motif.rhythm,
occurrences: motif.occurrences.map((occurrence) => occurrence.startQuarters),
}));
```
Output (intervals in semitones, rhythm and positions in quarter notes):
```json
[
{
"intervals": [2, 2, 3],
"rhythm": [1, 1, 1, 1],
"occurrences": [0, 4]
}
]
```
## Rhythm patterns
`rhythmPatterns` counts adjacent duration patterns in each part's note order,
including rests, simultaneous notes and overlapping windows. It is a rhythmic
vocabulary rather than a per-voice motif search, and a pattern occurring once
is still returned. Its `length` is a positive safe integer (default `4`);
invalid values throw `RangeError` before scanning even an empty score.
For voice-local phrases with exact attack spacing and source-note identities,
use [`findRhythmPatternOccurrences`](#find-recurring-rhythmic-phrases) instead.
```ts
import { rhythmPatterns } from '@webmusic/score/analyze';
const patterns = rhythmPatterns(melodyScore, 4);
const values = patterns.slice(0, 2).map((pattern) => ({
pattern: pattern.pattern,
count: pattern.count,
onsets: pattern.onsets,
}));
```
Output:
```json
[
{
"pattern": [1, 1, 1, 1],
"count": 5,
"onsets": [0, 1, 2, 3, 4]
}
]
```
## Find recurring rhythmic phrases
`findRhythmPatternOccurrences(score, options?)` finds exact written rhythm
recurrences within each part and voice. It keys each phrase by note durations
and distances between successive attacks, both represented exactly before
returning quarter-note numbers. Pitch does not participate. The result keeps
each occurrence's part, voice, source note IDs, attack positions and span so an
application can highlight or navigate the repetitions in its own display.
```ts
import {findRhythmPatternOccurrences} from '@webmusic/score/analyze';
const groups = findRhythmPatternOccurrences(score, {
length: 4,
minOccurrences: 2,
});
const locations = groups.map(({durations, onsetGaps, occurrences}) => ({
durations,
onsetGaps,
starts: occurrences.map(({partId, voiceId, startQuarters}) =>
`${partId}/${voiceId} at ${startQuarters}`),
}));
```
`length` defaults to `4` attacks and must be a positive safe integer.
`minOccurrences` defaults to `2`; it must be a positive safe integer and is
effectively at least `2` non-overlapping written spans. All matching windows,
including overlapping ones, remain in `occurrences`. `partId` filters one
existing Score part; an unknown ID throws `RangeError`. Explicit rests split
phrases, while an unnotated silent gap becomes part of the onset-gap signature.
Simultaneous chord members form one attack with the longest member duration;
a tied continuation is not a new attack. Group IDs are stable only within one
scan, not across score edits or changed options. The function is pure and does
not infer performed groove, accents or syncopation.
## Inspect rhythmic placement
`inspectScoreRhythm(score, options?)` compares written attacks with the
score's meter-aware beat grid. `startQuarters` is inclusive and `endQuarters`
exclusive; both default to the full score. `partId` limits the attacks but not
the shared beat grid. `subdivision` chooses 1, 2, 3 or 4 equal divisions of a
notated denominator beat, defaulting to 2.
```ts
import {inspectScoreRhythm} from '@webmusic/score/analyze';
const inspection = inspectScoreRhythm(score, {
startQuarters: 0,
endQuarters: 4,
subdivision: 3,
});
const placements = inspection.onsets.map((onset) => ({
notes: onset.noteIds,
beat: `${onset.measure}:${onset.beat}`,
offbeat: onset.offbeat,
syncopationCue: onset.syncopationCue,
}));
```
The result separates `beats`, `subdivisions`, onset groups and a summary.
Each onset retains part/voice/note IDs, exact subbeat, authored accent,
nominal seconds and optional per-note performed timing/deviation. Its
`syncopationCue` is deliberately narrow: an authored accent or marcato on an
offbeat attack, not a complete syncopation or groove inference. A denominator
beat in 6/8 is an eighth note; the function does not infer compound-meter
accent groups. No notes are quantized or changed. Invalid ranges,
subdivisions and unknown part IDs throw; the function owns no resources.
## Voice leading
`voiceLeading` finds basic voice-leading problems where enough voice data exists: large leaps inside one voice, and parallel fifths/octaves between voices with matching onsets.
Issues preserve each raw identifier in `voices` and include `voiceParts`, an
array of part IDs paired by index. Use both values to identify a lane: different
parts may use the same voice ID. `voiceParts` is optional for compatibility with
older results. `projectVoiceFlow` can locate a legacy raw ID only when it is
unambiguous; it omits an ambiguous bracket rather than placing it on another
voice. Session and Worker snapshots freeze the added array with the result.
Onset matching uses exact Rational quarters, without a fixed MIDI-tick grid;
nearby distinct onsets remain distinct. Within one same-onset voice chord,
the first sounding member represents that melodic event. A notated rest breaks
parallel-motion comparisons even when the next sounding onset is not shared by
the other voice. These remain basic shared-onset checks:
they do not perform complete counterpoint analysis or infer missing voice parts.
This example assumes `counterpointScore` has two voices named `upper` and `lower`.
```ts
import { voiceLeading } from '@webmusic/score/analyze';
const issues = voiceLeading(counterpointScore);
const values = issues.map((issue) => ({
type: issue.type,
startQuarters: issue.startQuarters,
endQuarters: issue.endQuarters,
voices: issue.voices,
severity: issue.severity,
}));
```
Output:
```json
[
{
"type": "large-leap",
"startQuarters": 2,
"endQuarters": 3,
"voices": ["upper"],
"severity": "warning"
}
]
```
## Use in a pipeline
Because every function returns plain data, analysis can run after loading a file and before rendering or exporting.
```ts
import { detectKey, findMotifs, romanNumerals, segmentChords, summarizeScore, voiceLeading } from '@webmusic/score/analyze';
import { loadScoreFromUrl } from '@webmusic/score/io';
const score = await loadScoreFromUrl('/scores/etude.mid');
const report = {
summary: summarizeScore(score),
key: detectKey(score),
chords: segmentChords(score),
roman: romanNumerals(score, detectKey(score)),
motifs: findMotifs(score),
issues: voiceLeading(score),
};
```
## Incremental analysis for live editing
When a score is being edited interactively, re-running every analysis from scratch on each keystroke wastes work. `createAnalysisSession` keeps the previous result and, on `update(nextScore)`, recomputes only what the edit touched — it relies on the core model's structural sharing (`Score.edit` / `Score.withPart` keep untouched `Part` objects identical by reference) to skip unchanged parts entirely.
```ts
import { createAnalysisSession } from '@webmusic/score/analyze/headless';
const session = createAnalysisSession(score, { windowQuarters: 2 });
session.result; // { key, chords, roman, motifs, issues } — same values as the standalone functions
const next = score.edit((tx) => tx.updateNote(noteId, { pitch }));
const updated = session.update(next); // recomputes only around the edit
```
`update` is guaranteed to return a result deep-equal to a from-scratch analysis of the new score. What is truly incremental: key detection (cached per-part pitch-class histograms), chord segmentation and roman numerals (the sweep re-runs only over the edited time window and is spliced into the cached segments), voice leading (only voice pairs involving an edited part are re-checked), and motifs (only the edited part is rescanned, then merged). Adding, removing, or reordering parts falls back to a full chord re-segmentation. The React hook `useScoreAnalysis` uses a session internally, so live editing through `@webmusic/score/react` gets this for free.
## Follow playback in code
[createAnalysisFollower]() borrows a
`ScorePlaybackSource`, combines its current score and activity with an analysis
session, and supports nominal-seconds or quarter-note seeks with explicit
completion outcomes. It creates no player, DOM or presentation clock. Use its
state with the [projections](#the-projections), or pass your own UI callbacks.
## Analyze live note events in code
The live trackers are also headless domain components. Feed note events into
them directly; they publish chord or key results as plain data and never create
or paint UI.
```ts
import {
createLiveChordTracker,
createLiveKeyTracker,
} from '@webmusic/score/analyze/headless';
const chords = createLiveChordTracker({
onUpdate: ({ chord, midis, history }) => {
console.log({ chord, midis, history });
},
});
const key = createLiveKeyTracker((result, heard) => {
console.log(result.tonic, result.mode, heard);
});
chords.noteOn(60);
chords.noteOn(64);
chords.noteOn(67); // publishes C major
key.noteOn(60);
key.noteOn(64);
key.noteOn(67);
```
### Analysis in a Worker
[createAnalysisWorker]() runs a session
behind promises and documents its complete options, ownership, fallback,
supersession and error contract. Import it from
`@webmusic/score/analyze/worker-client` or the Analyze Headless entry. A custom
worker can use the `@webmusic/score/analyze/worker` module entry; the protocol
below describes its wire boundary.
### Worker protocol v1
The current protocol uses the exported identity
`ANALYSIS_WORKER_PROTOCOL === '@webmusic/score/analyze/analysis-worker'` and
`ANALYSIS_WORKER_PROTOCOL_VERSION === 1`. Every request and response carries
those fields plus a non-negative safe-integer request `id`. Requests use
`action: 'analyze' | 'update'`. A successful response contains `result`; a
failed response keeps an `error` display string and includes structured
`errorDetails`:
Import the identity constants and request/response types from
`@webmusic/score/analyze/worker-protocol`. That entry is import-safe in main-thread,
SSR, and Worker scopes; listener registration belongs exclusively to
`@webmusic/score/analyze/worker`.
```ts
interface AnalysisWorkerErrorDetails {
version: 1;
code:
| 'unsupported-protocol'
| 'invalid-request'
| 'analysis-failed'
| (string & {}); // forward-compatible unknown code
operation: 'protocol' | 'analyze' | 'update';
message: string;
retryable: boolean;
causeCode?: string; // for example a ScoreJSONError code
}
```
The client validates response identity/version, correlated id, the complete
`AnalysisResult` wire shape, discriminant, and structured error fields before
settling a call. A malformed, mis-correlated, or incompatible response is
terminal: all pending promises reject, listeners are detached when the Worker
supports `removeEventListener`, and an owned Worker is terminated.
The error operation is a closed v1 set and `causeCode`, when present, must be a
string; the error-code string is deliberately open for forward compatibility.
A valid remote failure rejects with `AnalysisWorkerRemoteError`; treat unknown
future `code` values generically even when handling known codes specially.
Worker ownership follows construction. Passing an existing Worker borrows it,
so `dispose()` does not terminate it. The default path and a supplied factory
create an owned Worker, which `dispose()` does terminate. Custom borrowed
`AnalysisWorkerLike` implementations should expose the optional
`removeEventListener` method so disposal can release listener closures.
:::caution[Cancellation boundary]
Analysis protocol v1 has no `AbortSignal`, timeout, or `cancel` action.
Latest-wins skips updates that are still queued; it does not interrupt analysis
already running in the Worker. The fallback also computes synchronously on the
calling thread and returns local errors rather than
`AnalysisWorkerRemoteError`. Result supersession is therefore not computation
cancellation.
:::
Session/Worker options are validated before analysis. `windowQuarters` must be
finite; `motifLength` and `minOccurrences` must be positive safe integers.
Invalid options reject consistently instead of producing a result containing
`NaN` timeline fields.
In React, `useScoreAnalysisAsync` from [`@webmusic/score/react`]() wraps this client (including the fallback) and returns `{ result, pending }`.
## Reports
`createScoreReport(score)` returns formatted summary rows for a whole score. Pair it with the existing `distributions` and `rhythmPatterns` APIs when building a statistics report. These calculations return data; the application chooses when to recompute it and how to display it.
Choose a report and an Arabesque No. 1 import below. MIDI and MXL expose different preserved notation while keeping the musical source aligned. Histogram mode exposes all three distributions; rhythm mode exposes pattern length. Returned data shows the actual function result. The demo owns its controls and optional UI presenters; it creates no analysis Element or playback resources.
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/api/analyze/)
### Report Import
```ts
import {createScoreReport} from '@webmusic/score/analyze/headless';
const report = createScoreReport(score);
console.table(report.rows);
```
### Report Summary report
`createScoreReport` combines the summary, key estimate and chord segmentation into a title, composer and label/value rows. It is pure: the same `Score` produces the same report, with no retained state or disposal.
Rows are already formatted: confidence becomes a percentage, tempo is rounded and suffixed with `bpm`, and meter is a fraction. For underlying numeric values, use [`summarizeScore`, `detectKey` and `segmentChords`](). Missing title/composer remain `undefined`; a report can omit them. With no pitched-note duration evidence, the Key row says `Unknown` and Confidence is `0%`; the key algorithm’s compatibility placeholder is not presented as a detected key.
### Report Histograms
Histogram data comes from the existing stateless [`distributions`]() API. It is separate from the formatted summary:
```ts
import {distributions} from '@webmusic/score/analyze';
const histogram = distributions(score);
console.table(histogram.pitchClasses);
console.table(histogram.intervals);
console.table(histogram.durations);
```
Pitch-class values are duration weights, while interval and duration values are counts. The demo displays pitch-class weights as percentages of total pitched-note duration weight and preserves the original values under Returned data. The API reference owns the units, bin shapes and voice/rest semantics.
### Report Rhythm patterns
Use [`rhythmPatterns`]() to count duration sequences independently of pitch:
```ts
import {rhythmPatterns} from '@webmusic/score/analyze';
const patterns = rhythmPatterns(score, 4);
console.table(patterns.map(({pattern, count, onsets}) => ({
pattern: pattern.join(', '), count, onsets: onsets.join(', '),
})));
```
The second argument is notes per pattern, a positive safe integer; invalid values throw `RangeError`, including for an empty score. Durations and onset positions use quarter notes; overlapping occurrences count separately. The demo recomputes after a score or pattern-length change and displays every returned pattern.
### Report Updating and presenting reports
Call these functions again with the next immutable score when an editor or importer changes it. They describe the supplied score rather than the player's current position. [`createAnalysisSession`]() separately maintains key, chord, Roman, melodic-motif and voice-leading results across score edits; its `result` does not contain summary, histogram or rhythmic-vocabulary fields.
For a custom visual report, pass these plain values to [`renderSummaryCard`, `renderHistogram` or `renderRhythmPatternList`](). DOM ownership and presenter cleanup remain in the application or demo, outside the Headless calculation.
API
**Construction** — `createScoreReport(score: Score): ScoreReport`. No options, commands, events or disposal. It borrows the score for the call and returns plain data.
| Member | Kind | Description |
| --- | --- | --- |
| `title` | result | Score title, or `undefined`. |
| `composer` | result | Composer, or `undefined`. |
| `rows` | result | Read-only `{label, value}` strings: Key, Confidence, Tempo, Meter, Parts, Measures, Notes, optional Range, and Chord segments. |
### Report Related
Use the [Analyze API]() for one-shot algorithms and [live Analyze Web Components]() for performance-following surfaces. The [analysis UI presenters]() can render reports without an Element wrapper.
## API Reference
| API | Kind | Use it for |
| --- | --- | --- |
| `defineAllAnalysisElements()` | Web Components | Register the live Analyze family from `@webmusic/score/analyze/element`; individual classes and helpers are owned by the [Element references](). |
| `summarizeScore(score)` | Summary | Metadata, counts, duration, tempo, meter, key, and pitch range. |
| `distributions(score)` | Statistics | Duration-weighted pitch classes, melodic interval counts and note-duration counts. |
| `chordTimeline(score, opts?)` | Harmony analysis | Zero-config chord labels over fixed musical windows (quarters). |
| `detectKey(score)` | Key analysis | Estimate tonic, mode, confidence, and ranked candidate keys. |
| `segmentChords(score, opts?)` | Harmony analysis | Label sounding pitch sets between note boundaries, with contiguous equal labels merged. |
| `identifyChord(notes)`, `identifyChordFromMidi(midis)` | Harmony analysis | Name a supplied sounding set, retaining notated spelling or using the fixed MIDI spelling. |
| `romanNumerals(score, key, opts?)`, `romanNumeralForChord(symbol, key)` | Harmony analysis | Convert chord segments or one symbol into coarse key-relative Roman labels. |
| `findMotifs(score, opts?)` | Motif analysis | Find repeated interval/rhythm patterns in each part. |
| `findRhythmPatternOccurrences(score, opts?)` | Rhythm analysis | Find exact voice-local onset-gap and duration recurrences with source identities and quarter-note spans. |
| `analyzeIntervals(score, selection, options?)` | Interval inspection | Inspect spelled melodic and overlapping vertical note relationships with note provenance; supports written or transposed sounding pitch. |
| `inspectScoreRhythm(score, options?)` | Rhythm inspection | Inspect meter-aligned beats, subdivisions, note onsets and explicit accent/offbeat cues in a quarter-note region. |
| `rhythmPatterns(score, length?)` | Rhythm analysis | Count repeated duration patterns and their onset positions. |
| `voiceLeading(score)` | Voice-leading analysis | Report large leaps and simple parallel-motion issues. |
| `createScoreReport(score)`, `ScoreReport` | Headless helper / type | [Whole-score formatted reports](#reports); exported from `@webmusic/score/analyze/headless`. |
| `createAnalysisSession(score, opts?)` | Headless session | [Incremental analysis](), from `@webmusic/score/analyze/headless`. |
| `createAnalysisFollower(options?)` | Headless follower | [Playback-connected analysis and navigation](), from `/headless`. |
| `createLiveChordTracker(callbacks)` | Live analysis | Track sounding MIDI notes and publish chord, note-set and recent-history state with no UI. |
| `createLiveKeyTracker(onUpdate)` | Live analysis | Accumulate heard pitch classes and publish the current key estimate with no UI. |
| `AnalysisSession`, `AnalysisResult`, `AnalysisSessionOptions` | Type | The session interface, its combined result, and its options. |
| `LiveChordState` | Type | Plain chord, sounding-MIDI and history result published by `createLiveChordTracker` (type-only export from `@webmusic/score/analyze`). |
| `createAnalysisWorker(workerOrFactory?)` | Worker client | [Promise-based analysis and worker ownership](). |
| `AnalysisWorkerClient`, `AnalysisWorkerLike`, `AnalysisWorkerFactory` | Type | The worker client and the worker/factory shapes it accepts, including borrowed-versus-owned disposal. |
| `ANALYSIS_WORKER_PROTOCOL`, `ANALYSIS_WORKER_PROTOCOL_VERSION` | Constant | The required v1 wire identity and major protocol version. |
| `AnalysisWorkerRemoteError` | Error | A structured remote failure with `code`, `operation`, `retryable`, and optional `causeCode`. |
| `AnalysisWorkerRequest`, `AnalysisWorkerResponse`, `AnalysisWorkerErrorDetails`, `AnalysisWorkerErrorCode`, `ScoreJSON` | Type | The versioned message and error protocol of the `@webmusic/score/analyze/worker` entry. |
| `ScoreSummary`, `ChordTimelineOptions`, `ChordTimelineSegment` | Type | Summary and chord-timeline result/option shapes. |
| `Distributions`, `DistributionBin` | Type | Three histogram arrays and their label/key/value bins. |
| `ChordWindowOptions` | Type | `{ windowQuarters? }` for `segmentChords` / `romanNumerals`. |
| `RomanNumeralOptions` | Type | Chord window options plus optional precomputed `segments`. |
| `MotifOptions` | Type | `{ length?, minOccurrences? }` for `findMotifs`. |
| `RhythmPatternOccurrenceOptions`, `RhythmPatternOccurrenceGroup`, `RhythmPatternOccurrence` | Type | Phrase length, repeat threshold and optional part filter; each group retains a signature and provenance-rich occurrences. |
| `IntervalPitchMode`, `IntervalSelection`, `IntervalAnalysisOptions`, `IntervalEvidence`, `AnalyzedInterval`, `IntervalAnalysis` | Type | Written/sounding pitch choice, note selection and provenance-rich interval results. |
| `RhythmInspectionOptions`, `RhythmBeat`, `RhythmSubdivision`, `RhythmPerformedOnset`, `RhythmOnset`, `RhythmInspection` | Type | Meter grid, subdivision, authored/performed onset evidence and rhythm-inspection result. |
| `Key`, `KeyResult` | Type | A `{ tonic, mode }` target, and a key result with confidence and candidates. |
| `ChordSegment` | Type | A chord window (in quarters) with pitch classes and chord label. |
| `RNAResult` | Type | A roman-numeral entry (in quarters) with chord and roman labels. |
| `Motif` | Type | A repeated interval/rhythm pattern with occurrence positions (quarters). |
| `RhythmPattern` | Type | A repeated rhythm pattern with count and onset positions (quarters). |
| `VoiceLeadingIssue` | Type | A reported issue with type, quarter range, voices, and severity. |
## The projections
Import these projections from `@webmusic/score/analyze/headless`; they are not
exports of the stateless capability root. They return data without mounting UI.
The **projections** turn a result into the shapes a read-out draws: marks for a
keyboard, columns for a stave, dots for a fretboard, bands for a scrolling lane,
segments for a wheel. The two [live Analyze components]()
use only the projections needed by their musical surfaces. All projections
remain available for application-owned markup; see
[key diagnostics and history]().
A projection is a pure data operation, not another live object. Keep one
analysis result and recompute its relevant projection when that input or its
configuration changes; the owning UI chooses how and when to draw it.
| Function | Inputs and result |
| --- | --- |
| `projectSounding(midis, context?)` | Returns `SoundingProjection`: `spelling`, pitch `marks`, `staff`, `fret`, `naming` and `description`. `SoundingContext` accepts `key`, `tuning`, `spelling`, preferred naming `prefer`, onset map `since` and current `now`. The timestamps share the caller’s axis. |
| `projectNameplate(spelling)` | Converts one `ChordSpelling` to `NameplateView`, including its primary and alternate names and voicing labels. |
| `soundingMidisAt(score, quarters, options?)` | Returns ascending unique sounding pitches. `windowQuarters` defaults to `0.25`, with a minimum `0.0625`; in a rest it can retain the most recent attack in the lookback window. It never reads a future attack as current. |
| `projectProgression(result, score, mode, options?)` | Returns `FlowLaneView` for `'chords'` or `'roman'`; `FlowProjectionOptions.roll` defaults to `true` and applies to chord mode. |
| `projectKeyFlow(score, options?)` | Returns a key `FlowLaneView`; `windowQuarters` defaults to `8`, with a minimum of `1`. |
| `projectMotifFlow(result, score)` | Returns motif occurrence bands and contours; four leading motif tracks are shown individually and remaining motifs share a folded track. |
| `projectMotifOccurrences(motifs, score)` | Returns the same motif lane from an existing `Motif[]`, without requiring a combined `AnalysisResult`. |
| `projectRhythmPatternFlow(patterns, score)` | Returns recurrence bands, attack flags and measure marks from `RhythmPatternOccurrenceGroup[]`; four leading pattern tracks are shown individually and the rest share a folded track. |
| `projectVoiceFlow(result, score)` | Returns voice bands and issue brackets for up to six voice tracks; issues without unambiguous rendered voices have no bracket. Full results remain in `AnalysisResult.issues`. |
| `projectTonality(key, options?)` | Returns `TonalityView` with outer/inner wheel segments, optional needle and centre labels. `key` may be `undefined`; options accept a reference `Key` and spelling preference. |
| `projectKeyCandidates(key, options?)` | Returns ranked `ChipItemView[]`; `limit` defaults to `5`. Undefined key produces no chips. |
| `projectPitchClassMeters(bins, options?)` | Returns `ChipItemView[]` normalized to the largest supplied/heard weight. Optional `heard` bins add comparison values without changing bin order. |
| `keyPitchClasses(key)` | Returns the key’s pitch classes, or an empty array for `undefined`. |
| `formatMetricalPosition(score, quarters)` | Formats a position through the score’s measure map, including pickups and irregular measures. Invalid/negative positions format from zero. |
The public structural data types belong to these projection results:
`PitchMarkView`, `StaffMarkView`, `StaffView`, `FretMarkView`,
`FretboardBarreView`, `FretboardView`, `ChordNameView`, `HarmonyVoiceView`,
`NameplateView`, `FlowBandView`, `FlowTrackView`, `FlowFlagView`,
`FlowBracketView`, `FlowLaneView`, `ChipItemView`, `WheelSegmentView`,
`WheelNeedleView` and `TonalityView`. They carry labels, music-derived marks
and positions; they do not import UIKit or select pixel dimensions/styles.
`projectSounding(midis, context)` computes the name, spelling, role of every
pitch, stave column and chord shape together. It supports custom compositions
that need those values; the fixed live components compute only their own surface.
`soundingMidisAt(score, quarters)` reads the notes to feed it: real octaves, read out of the
score, never pitch classes dressed up as pitches.
`projectNameplate(spelling: ChordSpelling): NameplateView` converts an existing
chord spelling into only nameplate data. Use it with `spellChord()` when a
current-name display does not need staff, keyboard or fretboard projections.
The lane projections — `projectProgression(result, score, mode, options?)`,
`projectKeyFlow`, `projectMotifFlow`, `projectMotifOccurrences`,
`projectRhythmPatternFlow`, `projectVoiceFlow` — return bands carrying
**two rulers**: `start`/`end` in nominal seconds, which is what a conveyor is
drawn on, and `stampStart`/`stampEnd` in quarters, which is what a playhead is
fed. `projectTonality`, `projectKeyCandidates` and `projectPitchClassMeters`
answer the read-outs that have no time axis at all, `keyPitchClasses(key)` gives
the seven classes a key is built from — what a keyboard draws as ghosts behind
the keys nobody is pressing — and `formatMetricalPosition(score, quarters)` says
where a position sits in the written music — `bar 12 · beat 3`.
`projectProgression` accepts `FlowProjectionOptions {roll?: boolean}`; `roll`
defaults to `true` for existing API callers. Set it to `false` for chord bands
alone and compose an independent note view; it affects chord mode only.
Pass the same `Score` that produced the `AnalysisResult`. Motif bands use the
referenced notes’ maximum actual offset, so gaps and overlaps do not turn summed
note durations into an incorrect occurrence end. Voice bands likewise extend
through the last sounding offset, including an earlier long note that overlaps
later short notes. Fretboard projection caching distinguishes tuning labels,
string pitches, inlays and neck length as well as the requested search window.
They take the `Score` and not just the `AnalysisResult` on purpose: a
`ChordSegment` carries pitch classes, and a stave needs octaves.
## The transport clock
[createTransportClock]() and
`rateFromDurations` are exported by `@webmusic/score/analyze/headless`. The
object reference owns samples, readings, scrubbing, defaults and time units.
It predicts display motion between supplied timestamps and owns no playback
clock or scheduler.
---
# @webmusic/score
> The thin, DOM-free Score root — immutable score model, primitives, time map, queries, events, and serialization.
Canonical documentation: https://koperative-lab.github.io/WebMusic/score/api/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/score@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
The `@webmusic/score` root is the **foundation** of Score: the immutable `Score` model, the musical primitives (`Rational`, `Pitch`, `Duration`), the `TimeMap`, note queries, a typed event emitter, and JSON serialization. This root entry has no UI or browser dependency.
It is intentionally thin. Functional APIs live in capability subpaths of the same package:
- **Playback utilities** (SFZ, recorded-note conversion, offline rendering) live in [`@webmusic/score/play`](). Stateful engines and effects use `@webmusic/score/play/headless`; browser transport adapters use `/play/drivers`.
- **Analysis** (`summarizeScore`, `chordTimeline`, key/chord/roman/motif/voice-leading) lives in [`@webmusic/score/analyze`]().
- **File loading** (MIDI, MusicXML, MXL, ABC) lives in [`@webmusic/score/io`]().
- **Layout and projections** live in [`@webmusic/score/view`](). Each capability's `/element` entry supplies its Web Components; [`@webmusic/score/react`]() supplies React adapters.
Install `@webmusic/score` once, then import the root model or a capability entry such as `@webmusic/score/play` as needed.
## Package subpaths
Every published entry of `@webmusic/score`, grouped by capability. A
capability root is its API; `/headless` is code-only domain objects;
`/element` is side-effect-free Web Component classes and `/auto` registers
them; `/global` is the browser IIFE. This table is the package map — the
capability pages below document what is inside each entry.
| Entry | Form | What is there |
| --- | --- | --- |
| `@webmusic/score` | package root | The immutable model, primitives, `TimeMap`, queries, events, JSON — this page. |
| `@webmusic/score/io` | [capability root]() | Format detection, loaders, parsers and serializers. |
| `@webmusic/score/io/load` | code-only sub-entry | The loader surface on its own, for consumers that want only `loadScore`. |
| `@webmusic/score/io/formats` | code-only sub-entry | Per-format parsers and serializers (MIDI, MusicXML, MXL, ABC). |
| `@webmusic/score/io/worker-client` · `@webmusic/score/io/worker-protocol` · `@webmusic/score/io/worker` | worker triple | Off-main-thread parsing: the client, the wire protocol, and the worker runtime. |
| `@webmusic/score/play` | [capability root]() | Stateless playback: SFZ parsing, offline WAV render, recording → `Score`. |
| `@webmusic/score/play/headless` | [Headless]() | `ScorePlayer`, `InteractivePlayer`, `Rack`, `Sound`, `Effect`, `LfoController`, `PlayerController`. |
| `@webmusic/score/play/drivers` | code-only sub-entry | Browser transport adapters: scroll, pointer, orientation, arbitrary values. |
| `@webmusic/score/play/element` | [Web Components]() | ``, ``, ``, ``, ``. |
| `@webmusic/score/play/auto` | registration | Imports the play elements and registers them on evaluation. |
| `@webmusic/score/play/global` | browser IIFE | `
```
The live demo pairs this component with one ``. Parameters configures those two elements; Copy includes their markup and Reset restores their original attributes and connection. For optional supporting views, see [Compose a performance](). `defineChordAnalysisElement(tag?)` is idempotent and SSR-safe; `defineAllAnalysisElements()` registers the Analyze family from the same entry.
## Behavior and lifetime
The lane fits its containing block. Automatic `window` uses the duration of the opening two bars so the cell labels have room to breathe; an explicit window keeps its visible duration when the container is resized. UIKit recalculates the drawing and pointer coordinates. Track labels, the time ruler and the reserved current readout have separate layout space. There is no candidate control row above the lane or evidence prose beneath it.
Clicking a cell selects it and seeks the bound player. Pointer drag, arrow keys, Home/End and `[` / `]` also navigate the lane; `selectRegion()` chooses a quarter-note passage for local inspection without seeking. For a selected cell, `.analysis.evidence` lists note IDs and part/voice identities that overlap that cell, and its headline uses the same combined pitch material as the lane. A custom `selectRegion()` still samples at its midpoint instead of merging all harmonies in an arbitrary range. `.analysis.candidates` retains possible readings for custom inspectors. The `.analysis` Roman degree uses the explicit `key` or a score-wide detected key; it is a coarse degree, not harmonic-function, modulation, inversion or cadence analysis. The underlying Analyze API retains its unsummarized chord segments. For specialist Roman and voice-leading results, use the [Analyze API]().
Chord naming and automatic key context use the Score's written pitches; this component does not apply part transposition to produce concert pitch. An inconclusive automatic key does not assign a Roman degree. Set `key` for an explicit local interpretation, or use [the interval API]() with `pitchMode: 'sounding'` when transposed pitch relationships are needed.
Navigation emits `webscore:seek` in nominal score seconds even when no player is attached. No player, synth or AudioContext is created by the component.
Explicit `.score` wins over `src`, then the selected player’s resolved data. Native bindings prefer the player’s `.playback` [shared source](), which supplies readiness, the current position and note identity. If an explicit Score differs from the native player's Score object, the component does not apply that player's activity to the local score or seek the mismatched owner. Targets without an available source retain the compatibility snapshot/event path. Late or replaced targets are rebound. Explicit input retains precedence on player source changes. Empty input and loading failure are shown on the surface. Stale URL loads cannot restore superseded data. Seek uses `seekNominal()` when available, otherwise converts nominal seconds by the current rate before `seek()`. Failed seeking restores the previous local position if no newer state has arrived.
The selector must match exactly one target in the current Document or ShadowRoot. Invalid or ambiguous matches remain unbound. Native single-score player mode supplies complete initial data/state; Rack/controller modes do not promise that snapshot. Disconnect cancels owned work, releases subscriptions/observation and destroys presenters, while the borrowed player keeps its own lifetime.
API
### Attributes
| Attribute | Values / default | Behavior |
| --- | --- | --- |
| `src` | URL, absent | Explicit MIDI / MusicXML / MXL / ABC score; wins over player data. |
| `format` | `midi` / `mxl` / `musicxml` / `abc`, inferred | Parser override for `src`; otherwise infer from extension or bytes. |
| `player` | selector, absent | Exactly one source in the component’s Document or ShadowRoot; borrow available score, state and note events. |
| `density` | `comfortable` / `compact`, `comfortable` | Type and spacing defaults for this surface. |
| `scheme` | `light` / `dark`, inherited | Pin this surface’s palette; removing it follows the page. |
| `motion` | `auto` / `continuous` / `stepped` / `none`, `auto` | Automatic reads a `data-motion` ancestor, then reduced-motion preference; explicit values override it. |
| `window` | seconds `0.5`–`600` or `auto`, `auto` | Visible time scale; automatic uses the duration of the opening two bars when available, otherwise the presenter default. UIKit fits that duration to the measured lane width. |
| `spelling` | `auto` / `sharp` / `flat`, `auto` | Names pitches and `.analysis` chord candidates; auto uses available key context. |
| `key` | `C major` / `A minor` form, absent | Local key context for the selected chord's degree; absent or unparseable values use score analysis. Does not change the score or playback. |
Unknown display values fall back to the documented default. This fixed component has no `type`, `show`, `shell`, `range` or `tuning` control; configure sibling pitch views on their own tags.
### Properties
| Property | Type | Behavior |
| --- | --- | --- |
| `.score` | `Score \| undefined` | Explicit score; clearing it resumes `src` or player-data fallback. |
| `.selection` | read-only `AnalysisSelection \| undefined` | Selected band or local region with `id`, `startQuarters` and `endQuarters`. |
| `.analysis` | read-only `AnalysisInspection \| undefined` | Headline, detail, evidence and candidate readings for the selection; detailed data is not shown in a visible panel. |
| `.density` | `comfortable \| compact` | Reflected density. |
| `.scheme` | `light \| dark \| undefined` | Reflected palette; undefined removes the override. |
| `.motion` | `auto \| continuous \| stepped \| none` | Reflected motion policy. |
| `.window` | `number \| auto` | Reflected visible duration in seconds. |
| `.spelling` | `auto \| sharp \| flat` | Reflected pitch spelling. |
| `.key` | `string \| undefined` | Reflected local key override; undefined removes it. |
`ChordAnalysisElement` is exported from `@webmusic/score/analyze/element`. `selectRegion(startQuarters, endQuarters)` selects a finite increasing quarter-note range, clamps it to the loaded score and throws `RangeError` for an invalid or wholly out-of-score range; without a score it has no effect. `clearSelection()` returns to the first available band. Neither method commands playback. There are no public playback methods.
### Events
All emitted events bubble and are composed. `AnalysisSeekDetail` and `AnalysisSelectDetail` are exported from `@webmusic/score/analyze/element`.
| Event | Detail | Trigger |
| --- | --- | --- |
| `webscore:seek` | `AnalysisSeekDetail` (`{quarters, seconds}`) | Lane navigation; quarter notes and nominal score seconds. |
| `webscore:analysisselect` | `AnalysisSelectDetail` (`{kind: 'chord', id, startQuarters, endQuarters}`) | Selecting a band or calling `selectRegion()`; local inspection state. |
### Accessibility
The lane is a named slider tab stop with keyboard navigation. A semantic ordered list accompanies the graphical data and marks the current score span with `aria-current`. Selected details are available through a nonvisual status; candidate readings are data for application-owned inspectors, not Element buttons. Automatic motion respects reduced-motion preference on moving surfaces; display controls remain outside the component.
Styling
The component uses light DOM. Its block host fills the available width and can shrink in flex/grid layouts; application CSS can override its layout. Set public tokens on this host or any ancestor. The component does not install host-local theme defaults that mask inherited values.
One outer surface owns background, border, padding and radius. The inner musical presenter stays unframed, so translucent backgrounds are painted once. Use `--wm-component-background`, `--wm-component-border` (a complete CSS border), `--wm-component-radius` and `--wm-component-padding`; the corresponding `--wm-workbench-surface-*` tokens provide a specific override. By default it has a neutral surface, a 1px border and `.6rem` padding on all sides, matching the player. Its light/dark colors follow the inherited scheme or the explicit `scheme` attribute. Set the component border and padding to `0` and its background to `transparent` for a frameless composition. `--wm-surface`, `--wm-foreground`, `--wm-accent`, `--wm-border` (a color) and `--wm-control-radius` supply shared palette and control defaults. Existing `--wm-analysis-*` and `--wm-harmony-*` palette overrides remain supported.
```css
score-chord-analysis {
--wm-component-background: transparent;
--wm-component-border: 1px solid #94a3b8;
--wm-component-radius: .75rem;
--wm-foreground: #0f172a;
--wm-accent: #7c3aed;
--wm-harmony-flow-tone: rgb(124 58 237 / 18%);
}
```
The now line follows `--wm-accent`, with `--wm-harmony-flow-now` as a specific override. Set `--wm-harmony-flow-tone` for one uniform band fill, or `--wm-progression-tone-0` through `--wm-progression-tone-11` for individual pitch-class colors.
Stable part tokens are `root` on the direct child wrapper, `presentation`, `surface`, `content`, `index`, `lane`, `viewport` and `reel`. The former `inspection` and `candidates` parts were retired with their visible panels. Use selectors such as `score-chord-analysis [part~="surface"]` in application CSS. These are light-DOM hooks, not Shadow DOM `::part()` selectors; private `.wui-*` classes are not an API. The same hooks remain available after configuration changes and reconnects.
Lane geometry uses `--wm-harmony-flow-height` and `--wm-harmony-lane-height`; changing colors does not change the time axis or pointer geometry.
The [harmony presenter]() and [workbench presenter]() own the complete token references. No header, tabs, dock controls or sibling pitch views are embedded in this component.
---
#
> A current-chord nameplate driven by the selected player’s sounding notes.
Canonical documentation: https://koperative-lab.github.io/WebMusic/score/element/analyze/score-live-chord-analysis/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/score@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
This component displays the chord named from the selected player's currently held MIDI notes. Optional alternate interpretations appear as secondary text beneath the main symbol, not as commands. Applications that need past transitions can retain timestamped [Headless tracker updates]().
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/element/analyze/score-live-chord-analysis/)
## Import
```html
```
The live demo pairs this component with one `` using the same `/midi/Arabesque%20No.1.mid` source as the other Score Analyze demos. Parameters configures those two elements; Copy includes their markup and Reset restores their original attributes and connection. For optional supporting views, see [Compose a performance](). `defineLiveChordAnalysisElement(tag?)` is idempotent and SSR-safe; `defineAllAnalysisElements()` registers the Analyze family from the same entry.
## Behavior and lifetime
The nameplate and `.chord` update immediately as held notes change. When a single note or another set has no defensible chord name, the main reading shows its actual pitch names; `.chord` remains undefined and no chord-change event is invented. The symbol, voicing and alternate rows keep their height through rests and note changes. Alternate readings, when shown, are noninteractive text below the primary chord. Display configuration stays in the external Parameters section. `webscore:chordchange` waits for a stable, nonempty chord symbol; a changed note set or reset cancels an outstanding notification. The component has no history lane, score seek, keyboard, staff, fretboard, tabs or settings strip.
This tag uses the selected player’s held notes and note/state events. When available, the player’s `.playback` [shared source]() supplies current activity and subsequent updates; the event path remains a compatibility fallback. It has no score-loading attributes. A native snapshot initializes current held notes, while a structural event-only source begins with incoming notes. Without note input it waits. Late or replaced targets are rebound.
The selector must match exactly one target in the current Document or ShadowRoot. Invalid or ambiguous matches remain unbound. Native single-score player mode supplies complete initial data/state; Rack/controller modes do not promise that snapshot. Disconnect cancels owned work, releases subscriptions/observation and destroys presenters, while the borrowed player keeps its own lifetime.
API
### Attributes
| Attribute | Values / default | Behavior |
| --- | --- | --- |
| `player` | selector, absent | Exactly one source in the component’s Document or ShadowRoot; borrow held notes, state and note events. |
| `density` | `comfortable` / `compact`, `comfortable` | Type and spacing defaults for this surface. |
| `scheme` | `light` / `dark`, inherited | Pin this surface’s palette; removing it follows the page. |
| `spelling` | `auto` / `sharp` / `flat`, `auto` | Pitch-name spelling from the available key context or an explicit preference. |
| `alternates` | `show` / `hide`, `show` | Include or omit noninteractive alternate readings below the primary symbol. |
| `stability-ms` | milliseconds `0`–`1000`, `80` | Delay a nonempty `webscore:chordchange` until the displayed symbol remains stable; `0` emits immediately. Invalid values use `80`. Does not delay the nameplate. |
Unknown display values fall back to the documented default. This fixed component has no `type`, `show`, `shell`, `range` or `tuning` control; configure sibling pitch views on their own tags.
### Properties
| Property | Type | Behavior |
| --- | --- | --- |
| `.density` | `comfortable \| compact` | Reflected density. |
| `.scheme` | `light \| dark \| undefined` | Reflected palette; undefined removes the override. |
| `.spelling` | `auto \| sharp \| flat` | Reflected pitch spelling. |
| `.chord` | read-only `string \| undefined` | Named chord symbol; undefined for silence or a note set without a defensible chord name. |
| `.stabilityMs` | `number` | Reflected notification delay in milliseconds. |
`LiveChordAnalysisElement` is exported from `@webmusic/score/analyze/element`. Inherited `.selection` and `.analysis` are undefined on this note-only component. Inherited `selectRegion()` and `clearSelection()` have no effect on this tag; assigning inherited `.score` does not turn it into a score lane. There are no public playback methods.
### Events
All emitted events bubble and are composed.
| Event | Detail | Trigger |
| --- | --- | --- |
| `webscore:chordchange` | `{chord, midis}` | The displayed nonempty chord remains unchanged for `stability-ms`; silence, reset, source replacement and changed notes cancel pending notification. |
### Accessibility
The nameplate exposes the current chord or literal pitch names, pitches and optional alternate readings as text. Its main and secondary rows retain their size as the interpretation changes, with horizontal scrolling for unusually long readings. It has no alternate-choice button, slider or transport command. Automatic motion respects reduced-motion preference on moving surfaces; display controls remain outside the component.
Styling
The component uses light DOM. Its block host fills the available width and can shrink in flex/grid layouts; application CSS can override its layout. Set public tokens on this host or any ancestor. The component does not install host-local theme defaults that mask inherited values.
One outer surface owns background, border, padding and radius. The inner musical presenter stays unframed, so translucent backgrounds are painted once. Use `--wm-component-background`, `--wm-component-border` (a complete CSS border), `--wm-component-radius` and `--wm-component-padding`; the corresponding `--wm-workbench-surface-*` tokens provide a specific override. By default it has a neutral surface, a 1px border and `.6rem` padding on all sides, matching the player. Its light/dark colors follow the inherited scheme or the explicit `scheme` attribute. Set the component border and padding to `0` and its background to `transparent` for a frameless composition. `--wm-surface`, `--wm-foreground`, `--wm-accent`, `--wm-border` (a color) and `--wm-control-radius` supply shared palette and control defaults. Existing `--wm-analysis-*` and `--wm-harmony-*` palette overrides remain supported.
```css
score-live-chord-analysis {
--wm-component-background: transparent;
--wm-component-border: 1px solid #94a3b8;
--wm-component-radius: .75rem;
--wm-foreground: #0f172a;
--wm-accent: #7c3aed;
--wm-accent-foreground: #fff;
}
```
Nameplate text follows `--wm-foreground`; `--wm-harmony-foreground` can override only this presenter family. Alternate names are static secondary text and wrap within the available width.
Stable part tokens are `root` on the direct child wrapper, `presentation`, `surface`, `content`, `index`, and `nameplate` and `symbol`. Use selectors such as `score-live-chord-analysis [part~="surface"]` in application CSS. These are light-DOM hooks, not Shadow DOM `::part()` selectors; private `.wui-*` classes are not an API. The same hooks remain available after configuration changes and reconnects.
The [harmony presenter]() and [workbench presenter]() own the complete token references. No header, tabs, dock controls or sibling pitch views are embedded in this component.
---
# Score Web Components
> Every drop-in Score element by capability — what each tag is for, how the groups compose, the registration entries and the shared theming variables.
Canonical documentation: https://koperative-lab.github.io/WebMusic/score/element/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/score@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
The drop-in custom elements of `@webmusic/score` — place a tag on the page, no
JavaScript wiring required. Each capability ships its elements from its
`/element` subpath, so the capability root tree-shakes free of DOM code. To load
and register them — via a CDN script, a bundler, or individual `define*` calls —
see [Score setup](). Only `play` publishes `/auto` and
`/global` entries; register the analyze and view tags with the `defineAll*` functions
listed in their sections below.
| Capability | Elements |
| --- | --- |
| [Play](#play) | Playing a score: the transport, a rack mixer, note input, recording and synth control. |
| [Analyze](#analyze) | Following a performance: chord progression and the current sounding chord. |
| [View](#view) | Whole-score representations, held-pitch representations and optional sheet engraving. |
## Play
The elements of [`@webmusic/score/play`](). **Five elements cover the capability, one per job**, with an individual reference and live demo for each.
| Element | Job |
| --- | --- |
| [``]() | **Play a score back.** Load a file or assign a `Score`; play/pause, seek, emit note events. Drive a whole `Rack` via `.rack`, or assign `.controller` to drive an engine you already own. |
| [``]() | **Mix a rack.** Master and per-member volume faders for a shared `Rack`. |
| [``]() | **Play notes by hand.** One element with three layouts — piano / grid / chords — plus optional QWERTY control through `keyboard`. |
| [``]() | **Capture a performance.** Record note input into a `Score`, replay it, download MIDI / MusicXML. |
| [``]() | **Shape the sound.** Timbre, effect chain, ADSR, EQ, LFO and macro knobs in one panel; the `sections` attribute picks which areas render, so `sections="eq"` is an EQ on its own. |
Transport and mixing are deliberately separate: point [``]() and [``]() at one shared `Rack`.
The input surfaces share one tiny contract — bubbling `webscore:noteon` / `webscore:noteoff` `CustomEvent`s (`{ midi, velocity }`) plus an optional `.onNote(midi, velocity, on)` callback — so any of them wires to any player or a `Sound.midiOut`. For the programmatic engines, timbres, effects, and ensemble behind them, see the [Play API]().
Each element page has a live demo, API details and styling variables.
[``]() captures note events from
[``]() or another event source; the
recorder page owns that composition and its recording contract.
| Element | Register | Job |
| --- | --- | --- |
| [``]() | `defineScorePlayerElement(tag?)` | Load a file or assign a `Score`; play/pause, seek, emit note events. |
| [``]() | `defineRackControlElement(tag?)` | Mixer (master + per-member volume faders) for a shared `Rack`. |
| [``]() | `defineNoteInputElement(tag?)` | Discrete note surface — `layout` piano / grid / chords, `keyboard` for QWERTY. |
| [``]() | `defineScoreRecorderElement(tag?)` | Record note input → `Score`, replay, download MIDI / MusicXML. |
| [``]() | `defineSynthPanelElement(tag?)` | Control panel — `sections` sound / effects / envelope / eq / lfo / macros. |
| — | `defineAllElements()` | Register every element at its default tag (what `@webmusic/score/play/auto` calls). |
## Analyze
Choose one tool for the musical question you want to inspect. Each tag renders
one surface, can borrow an existing player, and keeps its own selection without
editing the score.
| Purpose | Component | What it does |
| --- | --- | --- |
| Identify the current chord | [``]() | Read the primary chord and optional alternate interpretations of sounding notes. |
| Probe a harmonic passage | [``]() | Select a chord segment to inspect pitch evidence and a key-relative degree. |
Whole-score summaries, histograms and rhythmic-vocabulary reports use the [Headless report workflow](). [Motif search](), [recurring rhythmic phrases](), [key estimates](), [intervals](), [metrical placement](), [Roman numerals]() and [voice-leading checks]() remain code-only building blocks for application-owned displays.
### Compose a performance
```html
```
The player loads once. Score-based companions borrow its resolved data and position; pitch views borrow its held notes. Each is an ordinary sibling that can be placed, styled or omitted independently. Configuration belongs to each tag’s attributes and application-owned controls. Each tag-owned live demo pairs that Analyze component with one player; the example above shows an optional composition. No Analyze component mounts an internal keyboard, staff, fretboard, tab bar or settings dock.
Score input precedence is explicit `.score`, then `src`, then the selected player’s resolved data. Score lanes also work with standalone input: they update local inspection and emit seek requests without creating playback resources. When bound, navigation goes to the existing player with explicit nominal/transport time conversion. Selecting analysis evidence is local to that tool. The current-chord nameplate reads sounding note activity and does not seek score time.
### Shared player connection
Native bindings consume the [nonvisual playback source]().
They obtain data and an initial snapshot before waiting for another note/time
event, and follow replacements without fetching the player's URL again. Explicit
`.score` takes precedence over `src`, then borrowed player data. Failed explicit
loading does not silently fall back to another source. When explicit data differs
from a native player's Score, navigation rejects the mismatch rather than seeking
with unrelated geometry. Omit the companion `src` to share the loaded Score.
`player` selects one unique target in the same Document or ShadowRoot. Discovery
follows late insertion, upgrade and replacement until disconnect. The read-only
`data-player-state` attribute exposes target resolution (`missing`, `invalid`,
`ambiguous`, `ready`) and native source readiness. Legacy events remain usable
when a target has no source or reports `unavailable`, including custom player
renderers. They cannot supply resolved data or an initial snapshot automatically.
When a native source is available, legacy events are ignored to avoid duplicates.
A custom target may expose `.playback` and dispatch a bubbling
`webmusic:sourcechange` when replacing that property. Disconnecting releases
subscriptions, discovery and owned analysis work, without stopping the player.
| Element | Register |
| --- | --- |
| [``]() | `defineLiveChordAnalysisElement(tag?)` |
| [``]() | `defineChordAnalysisElement(tag?)` |
| — | `defineAllAnalysisElements()` registers both. |
## View
Three View elements group representations by the data and resources they use.
| Element | Representations | Register |
| --- | --- | --- |
| [``]() | `piano-roll`, `staff`, `waterfall`, `map` and `thumbnail` over a whole Score. | `defineScoreViewElement(tag?)` |
| [``]() | `keyboard`, `staff` and `fretboard` for currently held MIDI pitches. | `definePitchViewElement(tag?)` |
| [``]() | Engraved sheet music using the optional OpenSheetMusicDisplay peer. | `defineSheetViewElement(tag?)` |
`defineAllViewElements()` from `@webmusic/score/view/element` registers all three.
Each type stays on its owning page, with its controls in external Parameters.
`score-view` borrows resolved score data and position; `score-pitch-view` borrows held
notes. `score-sheet-view` requires explicit `.score` or `src` and follows player events;
it does not borrow the player's loaded score.
```html
```
No follower creates another player. Each component can be placed or omitted
independently; a tag-owned demo contains one player and that page's component.
See the [View API]() and [Headless models]()
for application-owned presentation and the same data projections.
## Theming
Set public CSS custom properties on an element, its ancestor, or `:root`. They
inherit into Score's UIKit presenters, including controls inside Shadow DOM.
The default palette is mostly neutral with square corners; active notes and
recording indicators retain their role-specific accent defaults.
```css
/* Apply to a shared wrapper, or put these declarations on one component. */
.music {
--wm-component-background: transparent;
--wm-component-border: 1px solid #8291a5;
--wm-component-radius: 12px;
--wm-foreground: #25344a;
--wm-foreground-muted: #596b83;
--wm-accent: #e23b3b;
--wm-accent-foreground: #fff;
--wm-border: #8291a5;
--wm-control-border: #8291a5;
--wm-control-radius: 6px;
--wm-focus: #146dcc;
}
```
`--wm-component-*` styles the outer surface. The border value is a complete CSS
border such as `1px solid #8291a5` or `none`; `--wm-border` and
`--wm-control-border` are colors. `--wm-component-padding` accepts CSS spacing.
Presenter-specific `--wm--surface-*` values take precedence when a
surface needs its own background, border, radius or padding.
Inner drawing surfaces and controls have separate tokens. For example, making
the outer frame transparent does not erase the contrast between white and black
piano keys. For a completely transparent drawing surface, also set the relevant
`--wm-surface`, `--wm-surface-muted`, `--wm-surface-inverse`, or the specific
background token listed by its owner. Pair dark surfaces with readable
`--wm-foreground`, `--wm-foreground-muted` and `--wm-foreground-on-inverse`.
Keep `--wm-accent-foreground` visible against the accent color; it is independent
of the component background.
| Surface | Owning styling reference |
| --- | --- |
| Transport and compatibility alias | [Score player]() |
| Piano/grid/chord input and recorder | [Note input]() |
| Channel mixer | [Rack control]() |
| Parameters, envelope, EQ, LFO and macros | [Synth panel]() |
| Score drawings and active notes | [Score view]() |
| Keyboard, staff and fretboard readouts | [Pitch view]() |
| Engraved paper and ink | [Sheet view]() |
| Live analysis | Each [Analyze component](#analyze) and its UIKit Harmony/Workbench tokens |
Existing legacy variables and explicit renderer color options retain their
documented precedence. CSS changes update DOM/SVG colors without replacing the
score or player. CSS cannot override an explicit renderer
color option that takes precedence over the theme.
Use `::part(...)` for the documented parts of Shadow DOM components such as
`score-note-input` and `score-synth-panel`. Score player, Analyze and View render in light
DOM: target their public `[part~="..."]` hooks instead. Do not select private
UIKit child classes. UIKit can also be mounted directly with the same variables,
parts and named handle nodes; see [UIKit styling]().
`score-rack-part` is a nonvisual declaration. Demo wrapper elements inherit their
theme into the components they compose.
---
#
> Play notes from piano, grid, chord or QWERTY surfaces through one shared note-event contract.
Canonical documentation: https://koperative-lab.github.io/WebMusic/score/element/play/score-note-input/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/score@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
**``** is the discrete-note performance surface. Set `layout` to pick the rendering and interaction model; all layouts share multi-touch, drag glissando, per-note reference counting, and the same output contract (`webscore:noteon` / `webscore:noteoff` + `.onNote`). Every layout also accepts the boolean `keyboard` attribute for QWERTY musical typing (see **Keyboard** below).
Its implementation keeps mapping rules and layout templates separate from DOM lifecycle and held-note interaction state, while the public element API remains unchanged.
## Live demo
The three layouts now share one live ``. Change **layout** in Parameters to move between Piano, Grid and Chords without leaving the demo; the remaining rows always edit that same element. The Grid option starts with a playable pad map, while the Piano range and default chord set remain available when their layouts are selected.
To capture a performance as a `Score`, connect this input to a
[``](). Its own page covers
recording, take playback and MIDI / MusicXML export.
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/element/play/score-note-input/)
## Piano
An on-screen piano keyboard. Hold and drag across keys to glissando. Set `start` and `end` to choose its MIDI range.
## Grid
A **10×3 Launchpad** laid out like the physical keyboard — **Q–P**, **A–;**, **Z–/** — for triggering samples or a percussion SoundFont. Map cells with the `map` attribute (`z1=35,a1=50,…`) or assign `.pads` (`{ midi, label? }` per cell, bottom-left = index `0`); only mapped cells are playable — the rest render dimmed and ignore pointer input. Row letters are **`q`** (top), **`a`** (middle), **`z`** (bottom); columns **`1`–`10`** left to right. Add `keyboard` for QWERTY musical typing.
## Chords
Buttons that each trigger a **whole chord** — diatonic triads of C major by default; override with `.chords`. Notes shared between held chords are reference-counted, so overlapping presses don't cut each other off.
## Keyboard
Add the boolean `keyboard` attribute on **any** layout — Piano, Grid, or Chords — for QWERTY **musical typing**: click **▸ Click to start** to focus the surface, then `A S D F …` play white keys, `W E T Y …` black keys, **Z** / **X** shift the octave, and **Esc** blurs. Blurring releases every held note and stops listening. Enable it in the same live demo by entering a value in the **keyboard** row; clear the field to turn it off.
## Import
```html
```
API
```ts
import { InteractivePlayer, Sound } from '@webmusic/score/play/headless';
import type { NoteInputElement } from '@webmusic/score/play/element';
const player = new InteractivePlayer();
player.addVoice('kbd', Sound.oscillator({ type: 'triangle' }));
const el = document.querySelector('score-note-input')!;
el.setAttribute('map', 'a1=35,a2=36,z3=42'); // or el.pads = [{ midi: 35, label: 'BD' }, …]
el.onNote = (midi, velocity, on) => {
if (on) player.noteOn('kbd', midi, velocity, 3600); // sustain until release
else player.noteOff('kbd', midi);
};
```
| Member | Kind | Description |
| --- | --- | --- |
| `layout` | attribute / property | `piano` (default) / `grid` / `chords`. Switching at runtime releases held notes first. |
| `keyboard` | attribute | Boolean — adds QWERTY musical typing on any layout (`piano`, `grid`, `chords`). |
| `start` / `end` | attribute | MIDI range (piano without `keyboard`: `48`–`71`; with `keyboard`, the visible keys span the QWERTY row from `start` and shift with Z / X). Grid uses `map` / `.pads` only — `start` applies only with `keyboard` (octave anchor, default `48`). |
| `map` | attribute / property | Compact grid map for `layout="grid"` — comma-separated `ref=midi` pairs such as `a1=35,z2=42` (`q` / `a` / `z` row, column `1`–`10`). Unmapped cells stay idle. Ignored when `.pads` is set. |
| `velocity` | attribute | Velocity for triggered notes, 1–127 (default `100`; grid `110`). |
| `row-interval` | attribute | Used by the exported `gridCellMidi` helper for custom isomorphic UIs (not the built-in `grid` layout). |
| `.pads` | property | Override the grid pad map — `{ midi, label? }[]` (layout `grid`). Bottom-left = index `0`; only cells with `midi` play (blank / idle pads). Takes precedence over `map`. |
| `.chords` | property | Override the chord set — `{ label, notes }[]` (layout `chords`). |
| `.onNote(midi, velocity, on)` | property | Callback fired on every press (`on=true`) and release (`on=false`). |
| `.releaseHeld()` | method | Release every held note (pointer, chord, and QWERTY holds). |
| `webscore:noteon` / `webscore:noteoff` | event | Bubbling `CustomEvent`s with `detail = { midi, velocity }`. |
Disconnecting or changing layout releases held notes and removes the old
presenter's input listeners. Events and `.onNote` callbacks preserve attack
before release even when a listener synchronously removes the element or
changes its layout. Each callback receives the velocity captured for the same
event; changing `velocity` during event delivery does not change that callback's
payload. Reconnecting mounts fresh interaction state with no held notes.
Styling
The host follows its available width and respects native `hidden`. Piano and grid layouts keep
readable key sizes in an internal horizontal viewport; chord buttons wrap into rows. In
`keyboard` mode, Left/Right scroll that viewport without releasing held notes. Without it,
focus the viewport for native keyboard scrolling. Changing container width does not remount
the surface.
The element uses the [UIKit Note tokens and parts](), including
`--wm-note-key-min-width` (default `2.25rem`), `--wm-note-cell-min-width` (default `2.5rem`),
and `::part(viewport)`. Inherited `--wm-note-*` and shared theme tokens remain available;
explicit legacy `--note-input-*` values below take precedence over their UIKit equivalents.
For a transparent frame with rounded keys, set `--wm-component-background: transparent`,
`--wm-component-radius: 12px` and `--wm-control-radius: 6px` on the element or its ancestor.
Key fills resolve through `--wm-note-key` / `--wm-surface` and
`--wm-note-key-alt` / `--wm-surface-inverse`; held notes use `--wm-note-active` /
`--wm-accent`, with independent `--wm-note-label-active` / `--wm-accent-foreground`.
These variables remain live when the layout changes.
After the explicit legacy border override, neutral key and pad outlines resolve through
`--wm-note-border` → `--wm-control-border` → `--wm-border` → `#d8d8d8`. Black piano keys retain their separate
`--wm-note-key-alt-border` outline.
```css
score-note-input {
--note-input-bg: #fff;
--note-input-surface: #f3f3f3; /* grid cells */
--note-input-key: #fff; /* white keys */
--note-input-key-alt: #222; /* black keys / chord buttons */
--note-input-active: #444; /* held highlight */
--note-input-border: #d8d8d8;
--note-input-height: 120px; /* piano height */
--note-input-cell-height: 42px; /* grid / chords */
--note-input-gap: 0.4rem;
--note-input-radius: 0;
--note-input-focus: #444; /* keyboard-mode focus ring */
--note-input-overlay: rgba(255, 255, 255, 0.82); /* keyboard click-to-start veil */
--note-input-accent: #111; /* keyboard start button */
--note-input-accent-color: #fff;
}
```
---
#
> The Score transport widget — play/pause and a draggable seek bar over a file, an assigned Score, a whole Rack, or a controller you already own.
Canonical documentation: https://koperative-lab.github.io/WebMusic/score/element/play/score-player/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/score@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`` is the Score transport: play/pause and a draggable seek bar, over music it loads and schedules itself. Point `src` at a file (or hand it a `Score`) and it plays — no JavaScript wiring, and no other element required.
In native score mode, View and Analyze companions resolve this player with
`player="#id"` and borrow its score, state and note events. One player can drive
[``](),
[``]() and
[``]() together.
For several Rack members, nest a [``]()
desk with nonvisual `` declarations inside this player. Applications
that already own an engine can use [the borrowed controller mode](#four-ways-to-drive-it).
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/element/play/score-player/)
## Add an effect chain
Use **Effects** in the demo's Parameters panel to add reverb, a three-band equalizer, filters, delay, chorus, compression, or distortion. Effects run from top to bottom; each card can be expanded, reordered, or removed. The equalizer is compiled to three peaking filters, so the demo uses the same public `Effect` recipes as application code rather than a docs-only audio path.
An effect is an object graph, not an HTML attribute. The editor therefore assigns the real `.effect` property and adds a **JavaScript setup** block beside the live markup. **Copy** includes both blocks. Committing an effect edit stops and rebuilds the player's audio graph, so playback returns to the start; **Reset** clears the chain back to dry output.
That property belongs to the element's single-score mode. When `.rack` is the
active source (including a nested `` composition), the Rack owns
the shared audio graph; put the chain on `rack.effect` or
`rackControl.effect` instead. The Rack editor uses this same stack UI and the
same `Effect.chain(...)` recipes on its summed master bus.
```js
import { Effect } from '@webmusic/score/play/headless';
/** @type {import('@webmusic/score/play/element').ScorePlayerElement | null} */
const player = document.querySelector('score-player');
if (player) player.effect = Effect.chain(
Effect.reverb({ wet: 0.28, dry: 0.82 }),
Effect.filter({ type: 'peaking', frequency: 1000, gain: 3, Q: 0.9 }),
);
```
## Import
```html
```
That is the whole setup. To see what any attribute does to a live element, use the control strip on the demo above — it writes to the real tag and shows you the resulting markup.
`@webmusic/score/play/auto` registers this element (and the other Score elements) on import instead; `@webmusic/score/play/global` is the prebuilt `
```
## ``
One member of the desk, declared in markup. This section owns its nonvisual
contract. Outside a direct `` parent, `src` is not fetched and no
Rack member or playback engine is created.
| Member | Kind | Default | Description |
| --- | --- | --- | --- |
| `id` | attribute | `part-N` | Member id and fader label; `N` is the declaration's position among the desk's direct children. Changing it reconciles the member without reloading its score. |
| `src` | attribute | — | URL of the score this part contributes, loaded through the sibling `@webmusic/score/io` capability while inside a desk. |
| `format` | attribute | adaptive | Forces the parser when the extension is missing or lies. Only read while `src` is loading. |
| `sound` | attribute | the rack's default | Oscillator name: `sine`, `square`, `sawtooth` or `triangle`. |
| `.score` | property | — | A `Score` assigned directly. Wins over `src`. |
| `.sound` | property | — | A `HeadlessSynth` assigned directly. Wins over `sound`, and the only way to give a part a synth the attribute cannot name. |
| `webscore:error` | event | — | `{ operation: 'rack-part', error }`, bubbling and composed. |
The `id` names the fader the desk draws, so give each part one; a part with no `id` falls back to its
position among its siblings. There is no `rate`, `loop`, `pan` or chrome: a part is not a player, so
there is nothing on it for those to act on. The player wrapping the desk owns playback for every part
at once, and the desk owns the levels.
Changing `id` renames the live member without downloading its score again, rebuilding its player or
resetting its mix state. Duplicate requested ids leave existing members intact and emit an error.
Changing `sound` re-voices the part in place. Changing `src` or `format`
reloads its score, which does replace the member — `Rack.add` on an existing id disposes the live
player, and a new score is a different member.
A source replacement or disconnection aborts its pending request. Late results cannot replace
the current declaration. A failed current load emits `webscore:error`; the already resolved
declaration remains until replacement succeeds or its source is cleared. Removing a declaration
removes its member from the desk. Supplied sounds remain caller-owned.
## Filling the rack
`` covers the markup case. From script, build the members headless and hand a rack over
instead. Either assign one — adopted, and never disposed
here — or let the element create its own and reach it through `.rack`:
```ts
import { Rack, Sound } from '@webmusic/score/play/headless';
import type { RackControlElement } from '@webmusic/score/play/element';
const rack = new Rack();
rack.add({ id: 'lead', score: lead, sound: Sound.oscillator() });
rack.add({ id: 'bass', score: bass, sound: Sound.oscillator({ type: 'sawtooth' }) });
document.querySelector('score-rack-control')!.rack = rack;
```
Add or remove a member later and the desk redraws its strips; move a level from anywhere and the
faders follow.
API
The desk reads master and member state from its Rack. Membership changes update
the strips; mix changes update their values. Direct `` children
declare members, and `.rack` can supply an existing Rack. Transport remains on
`` or the application-owned Headless controller.
| Member | Kind | Description |
| --- | --- | --- |
| `master` | attribute | Show the master strip. Absent means shown; `master="false"` leaves the channel strips alone. |
| `mute` | attribute | Show per-channel mute, enabled by default; `mute="false"` removes the command from the mixer. |
| `solo` | attribute | Show per-channel solo, enabled by default. Soloing one channel clears the rest; `solo="false"` removes the command. |
| child `` | markup | Each declares one rack member. Labelled by its `id`, or by position when it has none. |
| `.rack` | property | The `Rack` to mix. Assigning one adopts it — it is never disposed here. Left unset, the element creates its own and disposes it on disconnect. |
| `.effect` | property | Post-processing recipe for the rack's summed output. Assign an `Effect` or `Effect.chain(...)`; replacing it updates the owned or assigned Rack in place without rebuilding members. |
| `webscore:error` | event | `{ operation: 'rack-control', error }`, bubbling and composed. Reports member reconciliation, Rack mix commands, master-effect replacement, owned Rack cleanup or presenter failures. |
Styling
The host fits its container and respects native `hidden`. The master stays beside
an internally scrollable channel bank; names wrap within their strips, and a
nonempty bank can receive keyboard focus for horizontal scrolling. Resizing does
not rebuild controls or alter the Rack.
The `--rc-*` aliases override this desk only. Without them, controls keep their
`--wm-mixer-*` and shared UIKit theme inputs, including `--wm-foreground`,
`--wm-surface-muted`, `--wm-accent` and `--wm-focus`. Use the exposed parts to
change the strip or label layout without replacing the fader behavior.
The outer surface also reads `--wm-component-background`, `--wm-component-border`,
`--wm-component-radius` and `--wm-component-padding`. Override just this desk
with `--wm-rack-control-surface-background`, `--wm-rack-control-surface-border`,
`--wm-rack-control-surface-radius` or `--wm-rack-control-surface-padding`.
Background accepts `transparent`; the surface border accepts a complete border
or `none`. Channel declarations (`score-rack-part`) have no visual surface.
```css
score-rack-control {
--rc-border: #d8d8d8;
--rc-fader-width: 2rem;
--rc-fader-height: 96px;
--rc-track: #f3f3f3;
--rc-track-border: #d8d8d8; /* soft control outline */
--rc-fill: #999;
--rc-thumb: #111; /* position indicator */
--rc-radius: 0;
}
```
---
#
> Capture note events as a Score, replay the completed take, and export MIDI or MusicXML.
Canonical documentation: https://koperative-lab.github.io/WebMusic/score/element/play/score-recorder/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/score@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`` captures a note performance as a `Score`, then offers take
playback and MIDI / MusicXML download. It is the symbolic-music counterpart of
[``](): supply note events from
[``]() or another input instead of
opening a microphone.
The input surface and recorder are independent components. The demo captures a
sibling note input through `.source`, with a separate `InteractivePlayer` for
live monitoring. Record starts a fresh take, Stop completes it, and Play auditions
it. Release held notes before stopping to include them.
[Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/element/play/score-recorder/)
## Import
```html
```
`@webmusic/score/play/auto` and `/global` register the Play elements together;
see [Score setup]().
## Connect a note source
Choose one feed path for each input:
- `.input(midi, velocity, on)` monitors the note through the recorder's synth
and captures it while recording. Assign it to a note input's `.onNote`, as
above, for live sound and recording together.
- `.source = eventTarget` subscribes to `webscore:noteon` / `webscore:noteoff`
events while connected. This path only records; it leaves an input's existing
`.onNote` sound callback intact. Use it when the input already has monitoring.
The event detail is `{midi: number, velocity?: number}`; omitted velocity is
`100`. The recorder also accepts these events on itself. A single bubbling
event reaching both itself and its assigned source is captured once, but
feeding both `.input()` and the event path would capture the same performance
twice. Keep visible components as siblings: the recorder has no slot that
projects a nested input surface.
```ts
import type {NoteInputElement, ScoreRecorderElement} from '@webmusic/score/play/element';
const input = document.querySelector('score-note-input')!;
const recorder = document.querySelector('score-recorder')!;
recorder.source = input; // capture only; retain the input's existing sound callback
```
## Timing, takes and ownership
Capture measures elapsed seconds from `.record()` with a monotonic clock. Stop
converts completed notes to notation through `notesToScore`; `bpm` and
`quantize` are read at that moment. The default `quantize="0"` preserves
performed timing. `0.25` snaps to sixteenth notes measured in quarter-note
units.
Overlapping presses of the same MIDI pitch pair with releases in attack order.
Completed notes have a minimum captured duration of `0.02` seconds. Stop keeps
completed notes and discards presses that have not received a release. An empty
take produces no `Score` and no recorded event. Starting again clears the
previous take; stopping while idle returns the last completed take.
Changing `.source` discards unmatched presses while preserving completed notes
and the recording time origin. Disconnect cancels the active recording, removes
subscriptions, stops take playback and disposes the recorder's monitoring and
playback objects. The assigned source and last completed take survive reconnect;
call `.record()` to begin another recording.
Sources, supplied sounds and supplied AudioContexts are borrowed and are not
disposed by the recorder. The recorder releases its own routes and resources.
Live monitoring and take playback alternate use of a supplied sound's route:
input, access to `.player`, a new recording, or replacing `.sound` /
`.audioContext` stops take playback. This recorder owns its take audition; its
`.player` is the monitoring `InteractivePlayer`, not an external player binding.
API
The element class is `ScoreRecorderElement`; register it with
`defineScoreRecorderElement(tag = 'score-recorder')`. Missing, blank or nonfinite
numeric attributes use their defaults; finite values below the minimum clamp
to it.
| Member | Kind | Description |
| --- | --- | --- |
| `bpm` | attribute / property | Notation tempo in beats per minute; default `120`, minimum `1`. Read when a take stops. |
| `quantize` | attribute / property | Notation grid in quarter notes; default and minimum `0` (performed timing). For example, `0.25` means sixteenths. Read when a take stops. |
| `.source` | property | Borrowed `EventTarget \| undefined` for capture-only note events. No default source. Replacement discards unmatched presses; assignment survives reconnect. |
| `.input(midi, velocity, on)` | method | Monitor a note and, while recording, capture its press (`true`) or release (`false`). Returns `void`. |
| `.record()` | method | Start a fresh recording, stop take playback and discard the previous completed take. Returns `void`. |
| `.stop()` | method | Finish capture and return `Score \| undefined`; empty recordings return `undefined`. When already idle, return the last completed take. |
| `.sound` | property | Borrowed `HeadlessSynth \| undefined` used for monitoring and take playback. Live monitoring defaults to an owned triangle oscillator. Replacing it stops playback and releases the old monitoring route. |
| `.audioContext` | property | Borrowed `AudioContext \| undefined` shared by monitoring and take playback. Replacing it stops playback and releases the old monitoring route. |
| `.player` | property (get) | Lazily creates the internal monitoring `InteractivePlayer`. Reading it stops take playback so monitoring can reclaim its route. |
| `.take` | property (get) | Last completed `Score \| undefined`; cleared when a fresh recording starts. |
| `.recordingActive` | property (get) | Whether recording is active. |
| `webscore:recorded` | event | Bubbling, composed `CustomEvent` with `{score}` after a nonempty take stops. |
| `webscore:error` | event | Bubbling, composed `CustomEvent` with `{operation: 'play', error}` on failed take playback or a failed presenter command. |
The **Record** button toggles recording; **Play** toggles take audition. **MIDI**
and **MusicXML** download through `serializeMIDI` / `serializeMusicXML`. Playback
and exports remain disabled until a take exists. A rejected playback request
releases that playback and clears its playing state before reporting the error;
late rejection from a replaced playback is ignored.
**Accessibility:** the presenter is a group named “Recorder”. Its native buttons
support Tab, Enter and Space with visible focus; recording and playback expose
`aria-pressed`. The Record name changes to “Stop recording” while armed. Export
buttons are named “Download MIDI” and “Download MusicXML”. A polite, atomic status
region announces recording/take counts. This note recorder exposes no audio level
meter.
Styling
The host follows its available width and respects native `hidden`. The
[UIKit Recorder]() wraps controls and export buttons
within that width; long status text breaks across lines. Resizing does not change
the take or playback state. Use its public `--wm-recorder-*` tokens and
`::part(root)`, `::part(record)`, `::part(play)`, `::part(exports)`,
`::part(export)` and `::part(status)`.
`--wm-component-background`, `--wm-component-border` and `--wm-component-radius`
style the outer frame; `--wm-control-radius` styles buttons. The recording accent
falls back from `--wm-recorder-accent` to `--wm-accent` and then red, with
`--wm-accent-foreground` for its text. Explicit legacy `--rec-*` values take
precedence over their corresponding UIKit tokens.
```css
score-recorder {
--wm-component-background: transparent;
--wm-component-radius: 0;
--wm-control-radius: 0;
--wm-recorder-accent: #c0392b;
}
```
---
#
> One control panel for every knob and curve surface — the sections attribute picks sound, effects, envelope, eq, lfo and macros, in order.
Canonical documentation: https://koperative-lab.github.io/WebMusic/score/element/play/score-synth-panel/
Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/score@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline.
`` is a composable control surface for one synth voice. It does not choose a synthesizer
for you: assign small descriptors and callbacks for the controls you want, then connect its optional
effects / EQ bus to the audio graph you already own. This keeps the panel reusable with
`OscillatorSynth`, a worklet, a sampler, or an application-specific engine.
The Element composes published UIKit section-panel, parameter-rack, envelope, EQ, LFO and
macro presenters. Parameter descriptors, `LfoController`, `EqController` and the DOM-free
[`SynthPanelAudioGraph`]() own the
behavior behind those controls. The panel borrows its AudioContext and target callbacks;
it creates no player or synth voice.
The `sections` attribute is an ordered comma list. One element can show only the controls a view
needs, or all six modules together:
| Section | Assign | What appears |
| --- | --- | --- |
| `sound` | `.sound` / `.params` | Grouped rotary parameters whose `apply(value)` callbacks drive your synth. |
| `effects` | `.context` + `.effects` | Live parameters from an ordered array of `Effect` recipes. |
| `envelope` | `.envelope`, `.ranges`, `.apply` | An accessible draggable ADSR curve. |
| `eq` | `.context` + `.bands` | A draggable frequency-response editor and three-band Web Audio graph. |
| `lfo` | `.lfo` + `.target` | Shape, rate, depth, phase and run controls for a borrowed modulation target. |
| `macros` | `.macros` | Knobs that map one normalized value onto several targets. |
Changing `sections` re-renders the visible section presenters without disposing the panel's
`LfoController`. Reordering sections or changing only `sound`, `envelope`, `lfo` or `macros`
does not rebuild the audio chain. In particular, hiding `lfo` leaves an already-running controller active, so
modulation continues; showing it again presents the same shape, rate, depth and phase. Disconnecting
the whole panel disposes the controller and stops modulation. Reconnection restores those values
into a fresh stopped controller.
When both `effects` and `eq` are active their audio is spliced into one serial chain (effects → eq)
behind the shared `.input` / `.output`. The demo pairs a piano `