# 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
    Loading audio…
    ``` ```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 `` with the panel and exposes each property group through external Parameters. ## Try every property The demo is a real `` and `` composition. Play the keyboard, then edit the property groups under **Parameters**: - **Sound parameters** replaces the actual `.sound` descriptor array and updates the oscillator. - **Envelope** assigns ADSR values and the three graph ranges; dragging the curve writes the values back into the editor. - **Equalizer bands** rebuilds the real `.bands` graph from low, mid and high frequency / gain / Q. - **LFO and macros** configures shape, rate, depth, phase, run state, a cutoff target and two multi-target macro descriptors. - **Effects** adds, edits, reorders and removes real `Effect[]` entries. The panel's own effects section exposes their live parameters. Use the `sections` field to reorder or hide modules. **Copy** includes the composition plus the current Parameters and Effects editor setup; **Reset** restores every editor value, nested effect and section after ordinary edits or a browser back/forward-cache restore. The structured editors are the copy source of truth. Turning a Sound / Effect knob or dragging an EQ point directly on the panel is a live audition and does not silently rewrite a separate editor field. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/element/play/score-synth-panel/) ## Audio routing Assigning `.context` creates stable `.input` and `.output` boundary nodes. Route a synth into the input once and the output to a destination once. Changing `.effects`, `.bands`, or `sections` only replaces the internal path; it does not invalidate those external connections. When neither effects nor EQ is active, the boundaries are connected by a dry bypass. ```js panel.context = context; synth.connect(panel.input); panel.output.connect(context.destination); panel.effects = [Effect.reverb(), Effect.delay()]; // The two external connections above are still valid. ``` Effects run before EQ. Hiding `effects` or `eq` removes that module from the signal path while retaining its configuration for a later render. ## Section state and lifetime Changing `sections` re-renders the visible presenters while preserving sound values, ADSR, EQ bands, effect parameters and macro positions. A running LFO also remains active when its section is hidden; showing it again restores the same shape, rate, depth and phase. Disconnecting the whole element tears down its audio nodes and LFO loop. Reconnecting creates a fresh stopped controller with the retained modulation settings. The panel borrows `.context`, `.target`, descriptor callbacks and every object they close over. It never disposes your synth or target. It owns only the Web Audio nodes and UI presenters that it creates from those assignments. ## Import ```html ``` ## Audio routing and parameter updates Connect a source to `.input` and `.output` to your destination after configuring a context. These ports keep their identity within the same AudioContext as effects, EQ bands or their sections change. The panel builds and connects the candidate DSP before retiring the old chain; a construction or connection failure releases completed candidate resources and leaves the last usable chain in place. With no applicable effects/EQ, the same ports carry a dry bypass from input to output. Re-enabling DSP replaces that internal path and retains the external connections. Replacing or clearing `.context`, or disconnecting the panel, releases its ports; reconnect your routing after a new context or mounted lifetime. The panel never closes the borrowed context. Sound parameter callbacks may finish asynchronously. The control shows the requested value while `apply(value)` runs. A failed current request restores the preceding displayed value; an older failure cannot overwrite a newer request, replacement descriptors or a new mounted lifetime. Failures from a still-mounted control reach `webscore:error`; obsolete presenter rejections are contained after replacement or disconnect. The panel cannot undo side effects inside an application-supplied callback.
    API ```ts const WAVES = ['sine', 'square', 'sawtooth', 'triangle']; const panel = document.querySelector('score-synth-panel'); // "sound": one knob per descriptor; `group` clusters them in labelled blocks panel.sound = [ { group: 'osc', name: 'wave', value: 3, min: 0, max: 3, options: WAVES, apply: (i) => synth.setParam('type', WAVES[i]) }, { group: 'osc', name: 'tune', value: 0, min: -50, max: 50, step: 1, unit: '¢', apply: (v) => synth.setParam('detune', v) }, { group: 'filter', name: 'cutoff', value: 8000, min: 200, max: 12000, step: 10, unit: 'Hz', apply: (v) => synth.setParam('cutoff', v) }, ]; // "effects" + "eq": build the chain on a context, splice .input/.output into the graph panel.context = ctx; panel.effects = [Effect.filter(), Effect.delay(), Effect.reverb()]; panel.output.connect(ctx.destination); player.destination = panel.input; // "envelope": wire the ADSR editor to the synth panel.apply = (e) => { synth.setParam('attackSeconds', e.attack); synth.setParam('releaseSeconds', e.release); }; // "lfo" + "macros" panel.lfo = { shape: 'sine', rate: 1, depth: 0.6, phase: 0, running: false }; panel.target = { min: 300, max: 9000, unit: 'Hz', apply: (v) => synth.setParam('cutoff', v) }; panel.macros = [{ label: 'OPEN', targets: [ { name: 'cutoff', min: 600, max: 11000, apply: (v) => synth.setParam('cutoff', v) }, { name: 'level', min: 0.06, max: 0.22, apply: (v) => synth.setParam('gain', v) }, ] }]; ``` | Member | Kind | Description | | --- | --- | --- | | `sections` | attribute / property | Comma list of `sound` / `effects` / `envelope` / `eq` / `lfo` / `macros` (default `sound,effects`). Toggling re-renders; current values survive. | | `.sound` (alias `.params`) | property | `{ name, value, min, max, step?, unit?, options?, group?, apply }[]` — one knob each; `apply(value)` fires live and may return a promise. Failed current writes restore the prior displayed value without overwriting newer writes. | | `.context` | property | Borrowed `AudioContext` used to create the effects / EQ chain and stable boundary nodes. Replacing it releases the old graph. | | `.effects` (alias `.effect`) | property | `Effect[]` to chain and expose as knobs. | | `.input` / `.output` | property (get) | Stable entry / exit `AudioNode`s for the current `.context`. The internal effects → EQ path can rebuild without changing their identity; an empty path is dry. | | `.envelope` | property | Get / set the ADSR (`{ attack, decay, sustain, release }`). | | `.ranges` | property | Get / set per-stage envelope maxima in seconds (`{ attackMax, decayMax, releaseMax }`, default `2`). A live assignment repaints the graph. | | `.apply(env)` | property | Called on every envelope change. | | `.bands` | property | EQ bands `{ frequency, gain?, q? }[]` (default low / mid / high). | | `.lfo` | property | Get / set `{ shape, rate, depth, phase, running }`. Partial assignments update the mounted presenter; the getter returns current normalized state. | | `.target` | property | Borrowed LFO destination `{ min, max, value?, unit?, apply(value) }`. Assignment never applies eagerly; the panel never destroys it. | | `.macros` | property | `{ label?, value?, targets }[]` — each macro knob maps `0..1` into every target's range. | | `webscore:envelope` | event | `detail = { attack, decay, sustain, release }`. | | `webscore:macro` | event | `detail = { index, label, value }`. | | `webscore:lfo` | event | Full `.lfo` snapshot after a shape / rate / depth / running control change. Animation-frame phase changes and phase-only assignments do not emit an event. | | `webscore:error` | event | Bubbling, composed error boundary for presenter, sound-parameter callback, envelope and LFO failures. `SynthPanelEnvelopeErrorDetail` and `SynthPanelLfoErrorDetail` contain `{ operation: 'synth-panel', error }`. Direct audio-configuration property assignments can throw construction or connection errors. |
    Styling The host fills its container and can shrink inside flex/grid layouts; the native `hidden` attribute hides it. The shared section panel provides spacing without an additional outer frame. Parameter groups and macro readouts wrap; envelope and EQ values occupy rows below their plots. The plot-height tokens control the SVG only, so the component grows to include its readouts. Use `--wm-component-background: transparent` and `--wm-component-radius: 12px` for the panel/section surfaces, and `--wm-control-radius` for controls. Graph backgrounds are separate: set `--wm-envelope-background`, `--wm-eq-background` and `--wm-lfo-wave-background` (or their legacy `--synth-graph-bg` override) to `transparent`. Set `--wm-foreground-on-inverse` for readable graph ink on a custom dark surface. Graphs keep their inverse background and ink when only `--wm-surface` or `--wm-foreground` changes. Readouts below the plots use `--wm-foreground-muted`, then `--wm-foreground`, on the section surface. Specific presenter tokens and legacy overrides remain available for independent sections. ```css score-synth-panel { --synth-bg: var(--wm-surface, #fff); --wm-parameter-size: 46px; --wm-parameter-track: #dcdcdc; /* knob arc */ --wm-parameter-fill: #999; /* active arc */ --wm-parameter-pointer: #111; /* pointer */ --synth-graph-bg: #111; /* envelope / EQ / LFO graphs */ --synth-graph: #fff; --synth-accent: #c0392b; /* LFO playhead */ --synth-radius: 0; } ``` The shared envelope and LFO sections expose the same stable parts as their standalone presenters. Envelope exposes `root`, `svg`, `grid`, `area`, `curve`, `handle`, `readout`, `inputs` and `input`; LFO exposes `root`, `row`, `run`, `shapes`, `shape`, `control`, `label`, `rate-control`, `depth-control`, `input`, `rate`, `depth`, `value`, `rate-value`, `depth-value`, `wave`, `curve` and `head`. For inner graphs and controls, explicit `--synth-*` values retain precedence where they map onto the shared presenters. Outer boxes first read their `--wm-*-surface-*` and `--wm-component-*` values, then their legacy fallback. The Element supplies no default legacy values: without an explicit override, the relevant `--wm-panel-*`, `--wm-envelope-*`, `--wm-eq-*` and `--wm-lfo-*` tokens apply, followed by their shared UIKit tokens and built-in fallbacks. Parameter and macro controls use their `--wm-parameter-*` and `--wm-macro-*` tokens directly. The panel and LFO section omit an extra frame by default; panel border and padding can be set with `--wm-panel-border` and `--wm-panel-padding`.
    --- # > Current sounding pitches as a keyboard, staff or fretboard, selected by type. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/element/view/score-pitch-view/ 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. `` shows the currently held notes from one player or structural note source. Choose `type="keyboard"` (default), `staff` or `fretboard`; every type shares the same note state and owns no playback resources. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/element/view/score-pitch-view/) ## Import ```html ``` The demo contains one player and one pitch view. Change `type` in external Parameters. Type-specific rows appear only when relevant; their attribute values survive switching types. The player has its own parameter group. Copy includes both tags, and Reset restores their original attributes. ## Data and lifetime `player` must match exactly one target in the current Document or ShadowRoot. A nonempty `player` overrides the legacy `source` selector. Native snapshots initialize held notes; note and state events reconcile them after pause, seek, stop, end and source replacement. Late/replaced targets are rebound; invalid or ambiguous selectors remain unbound. Overlapping occurrences of the same MIDI pitch are counted, so releasing one does not erase another. The component has no `src`, `.score`, note command or transport command. Changing `type` replaces only the presentation and keeps the current held-note state. Disconnect releases its observer, subscriptions and presenter without stopping the player. To enter notes, use [``](). ## Keyboard `type="keyboard"` shows requested MIDI endpoints `low="36"` through `high="84"` by default. The getters report rounded requested endpoints; the presenter normalizes them to MIDI 0–127, orders reversed endpoints and limits the displayed span to 88 semitones. Notes outside the displayed range remain in `.active`. Released keys clear without a visual release trail. Use `white-key-width` and `black-key-width` for exact pixel widths, and `white-key-height` and `black-key-height` for key lengths. Explicit width fixes the board's pitch scale; a narrower host scrolls instead of compressing keys. With neither width set, the keyboard keeps its responsive sizing. Black width defaults to `0.62 × white width`; setting black width alone derives white width by dividing by `0.62`. Black width and height cannot exceed white dimensions. Set `fit-to-width="true"` to fit the entire normalized keyboard range into its container without horizontal scrolling. This overrides both explicit key widths and the CSS minimum key width while enabled, using the default `0.62` black/white width ratio. Key heights and held notes stay unchanged. Set it to `false` or remove it to restore the existing fixed widths or responsive minimum width. `follow="active"` reveals newly sounding pitches by default. Use `follow="none"` when a shared application container should pan the keyboard together with a [`waterfall`](). ## Staff `type="staff"` places current pitches on a `grand`, `treble` or `bass` staff. This is a sounding-pitch readout. To read notation over score time, choose [``](); for full engraving use [``](). ## Fretboard `type="fretboard"` marks every exact-MIDI location in the selected fret window. It does not choose a playable fingering or infer a chord voicing. Open-string tuning order is preserved, including reentrant instruments. Notes outside the window remain in `.active`; when none fit, the view reports “No sounding pitches in this fret range”. `fret-width` sets the distance between fret wires, `string-spacing` sets the distance between strings, and `string-width` sets their line thickness, all in pixels. Explicit fret spacing remains fixed when the container grows and pans when it does not fit. String thickness remains independent of the viewport scale. Omit these attributes to retain density-based, responsive defaults.
    API | Attribute | Default | Applies to | Description | | --- | --- | --- | --- | | `type` | `keyboard` | All | `keyboard`, `staff` or `fretboard`; unknown values use keyboard. | | `player` | absent | All | Unique selector for a held-note/state source; takes precedence over `source`. | | `source` | absent | All | Legacy selector fallback when `player` is empty. | | `low` / `high` | `36` / `84` | Keyboard | Requested lowest/highest MIDI key, rounded. | | `system` | `grand` | Staff | `grand`, `treble` or `bass`. | | `spelling` | `sharp` | All | `sharp` or `flat`; no key inference. | | `density` | `comfortable` | All | `comfortable` or `compact`. | | `scheme` | `inherit` | All | `inherit`, `system`, `light` or `dark`. | | `tuning` | `standard` | Fretboard | Standard guitar `[40,45,50,55,59,64]`, or 1–12 comma/space-separated MIDI pitches in visual string order; invalid values use standard. | | `first-fret` | `0` | Fretboard | First displayed fret, clamped to 0–24. | | `frets` | `12` | Fretboard | Visible fret count, clamped to 1–24. | | `follow` | `active` | Keyboard | `active` reveals newly active pitches; `none` leaves scrolling to the application/user. | | `fit-to-width` | `false` | Keyboard | Fit the complete normalized range to the container, overriding explicit widths and the CSS minimum width. Heights remain unchanged. | | `white-key-width` | responsive | Keyboard | Fixed natural-key width in px; ignored while `fit-to-width` is enabled. | | `black-key-width` | `0.62 × white width` | Keyboard | Fixed accidental-key width in px, capped at white width; alone derives white width as `value / 0.62`. Ignored while `fit-to-width` is enabled. | | `white-key-height` | theme height | Keyboard | Natural-key length in px. | | `black-key-height` | theme proportion | Keyboard | Accidental-key length in px, capped at white-key height. | | `fret-width` | responsive | Fretboard | Fixed distance between adjacent fret wires in px. | | `string-spacing` | density geometry | Fretboard | Distance between adjacent strings in px. | | `string-width` | density stroke | Fretboard | String stroke thickness in px, independent of viewport scaling. | `.type` is a writable reflected `PitchViewType` property. Other properties are read-only: `.active` returns a copy of ascending sounding MIDI pitches; `.low`, `.high`, `.system`, `.spelling`, `.density`, `.scheme`, `.firstFret` and `.frets` return the resolved/requested values described above. `.tuning` returns a copy of resolved open-string MIDI pitches. Configure the remaining values through attributes. The geometry getters `.whiteKeyWidth`, `.blackKeyWidth`, `.whiteKeyHeight`, `.blackKeyHeight`, `.fretWidth`, `.stringSpacing` and `.stringWidth` return the positive finite pixel request or `undefined`. Missing, empty, nonnumeric, non-finite, zero and negative geometry attributes use default geometry; removing an override restores the responsive/theme default. `.follow` returns the resolved mode. `.fitToWidth` is a read-only boolean, `false` by default. An empty or `true` attribute enables fitting; `false`, `0`, `no` and `off` disable it, ignoring case and surrounding whitespace. Other present values enable it. These settings do not replace the held-note tracker or player. `PitchViewElement`, `PitchViewType` and `definePitchViewElement(tag?)` are exported from `@webmusic/score/view/element`. Registration is idempotent and SSR-safe; `defineAllViewElements()` registers all View tags. There are no emitted events or public commands. **Accessibility.** Each surface is a labeled passive graphic (`role="img"`) with current-pitch labels. It has no playable or focusable piano keys; the keyboard’s visual active state is not a button’s `aria-pressed` state. Configure it with native controls outside the component.
    Styling By default, the keyboard and fretboard preserve readable key and marker sizes and scroll locally when their full range does not fit. `fit-to-width="true"` lets the keyboard compress its keys to show its complete range. A keyboard reveals newly sounding pitches; repeated snapshots and note releases leave manual scrolling in place. Wider fretboards expand fret spacing without enlarging the dots. Staff lines fill the container while noteheads retain their staff-space size. An overflowing surface is one keyboard focus stop for native scrolling; the musical input is unchanged. The light-DOM component uses the public [pitch presenter tokens](). Its rendered nodes expose `root` plus `board` for keyboard, or `root` plus `svg` for staff/fretboard. The presenter owns resizing and note graphics; the application owns host layout and spacing. There is no internal settings bar or additional sibling view. Motion follows the surrounding `data-motion` setting and reduced-motion preference. All three types inherit `--wm-component-background`, `--wm-component-border` (complete shorthand) and `--wm-component-radius`, overridden by `--wm-pitch-surface-background`, `--wm-pitch-surface-border` and `--wm-pitch-surface-radius`. The default frame remains transparent and borderless. Set `--wm-surface: transparent` for transparent nested surfaces. `--wm-foreground` and `--wm-accent` control shared ink and highlights; specific note/pitch paint tokens take precedence. Keyboard keys use `--wm-note-key`, `--wm-note-key-alt`, `--wm-note-border`, `--wm-note-label` and `--wm-note-radius`. Staff lines use `--wm-pitch-staff-line`. Fretboard geometry uses `--wm-pitch-fret-neck`, `--wm-pitch-fret-wire`, `--wm-pitch-fret-nut`, `--wm-pitch-fret-string` and `--wm-pitch-fret-inlay`. Theme changes on the host or an ancestor apply directly to the existing DOM/SVG without replacing the note tracker or held notes. Select exposed parts using ordinary light-DOM selectors such as `score-pitch-view [part~="board"]`. Key outlines fall back from `--wm-note-border` through `--wm-control-border` and `--wm-border` to `#d8d8d8` in a light scheme, matching component frames, or `#333` in a dark scheme. Set `--wm-border` on a shared ancestor to give a player, score view and keyboard the same border color.
    ## Related [Whole-score views]() · [Engraving]() · [Headless PitchView]() · [Analyze composition]() --- # > Real engraved sheet music via the optional OpenSheetMusicDisplay peer — beams, ties, slurs and page layout, with an optional playback cursor. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/element/view/score-sheet-view/ 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. Page-oriented engraving with multi-staff systems and responsive line breaks, drawn by the optional [`opensheetmusicdisplay`](https://opensheetmusicdisplay.org/) peer. [``]() provides continuous scrolling notation with beams, tuplets and playback highlighting. Any input format works — the element serializes the score to MusicXML itself — but **MusicXML in gives the best result**. A MIDI source has no staff assignment, no key signatures and a guessed clef, because MIDI never carried them. ```html ``` The peer is optional and **not installed by default**: ```sh npm install opensheetmusicdisplay ``` Browser bundlers can resolve the installed peer into a lazy chunk. Direct browser module use requires an import map for the package name, or an injected `.OpenSheetMusicDisplay` constructor. A dependency loading failure displays a message naming the package and the injection alternatives. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/element/view/score-sheet-view/) The demo contains one player and one sheet view, configured by external Parameters. SheetView still requires its own `src` or `.score`; `player` follows note events and does not borrow the player’s resolved score or native state snapshot. Copy includes both tags, and Reset restores their initial attributes.
    API | Member | Kind | Description | | --- | --- | --- | | `src` | attribute | URL of a MIDI / MusicXML / MXL / ABC file, loaded through the sibling `@webmusic/score/io` capability. | | `format` | attribute | Force a parser when the extension is missing or ambiguous. | | `player` | attribute | Selector for a player to follow; the OSMD cursor tracks the sounding note and a note released mid-chord leaves its siblings lit. | | `follow-cursor` | attribute | Scroll the cursor into view on each move. Defaults to on; set `follow-cursor="false"` to keep the page still. Changes apply to the existing cursor without rebuilding the engraving. | | `width` / `height` | attribute | Element size — a bare number is `px`, or any CSS length. A fixed `height` scrolls a long score inside the box. Size changes update layout without reloading the source or recreating OSMD. | | `.score` | property | Assign a pre-loaded `Score`; overrides `src`. | | `.OpenSheetMusicDisplay` | property | Inject the OSMD constructor instead of loading the peer (bundlers, tests, pinned builds). | | `.osmd` | property | Reuse an already-constructed OSMD instance. | No events. Swapping the score mid-render supersedes the older engraving instead of letting it paint over the newer one. For the same renderer without a custom element, call `renderOSMDStaffVisualizer(score, container, options)` from [`@webmusic/score/view/render`]() — the element is that call plus attribute plumbing.
    Styling The host fills its container and accepts application CSS or explicit `width` / `height` attributes. Removing a size attribute clears only the size set by the element; application inline styles and `hidden` remain usable. The engraving keeps a minimum layout width of `32rem` so narrow containers scroll horizontally instead of squeezing notes together. Override `--wm-sheet-min-width` when the score or application needs a different readable width. This changes the inner drawing, while the host continues to fit its container. The component composes [UIKit stage]() for this scrolling surface and [status]() for engraving errors. The default remains black ink on white paper. Paper follows `--wm-sheet-background`, then `--wm-surface`, then white; ink follows `--wm-sheet-foreground`, then `--wm-foreground`, then black. Set the paper or shared surface to `transparent` and choose an ink color suitable for the application background. The actual SVG noteheads, stems, staff lines and labels consume the ink token. OSMD's drawing background stays transparent underneath the stage. The frame accepts `--wm-component-background`, `--wm-component-border` (complete shorthand) and `--wm-component-radius`, with the stage's `--wm-stage-surface-*` overrides taking precedence. These and the ink/paper CSS tokens update after host or ancestor class/style changes without reloading the score or replacing its cursor. Use ordinary light-DOM selectors for the exposed `[part~="root"]` and `[part~="surface"]` nodes. The native OSMD cursor retains its own drawing style. An explicitly adopted `.osmd` instance retains the caller's rendering options and ink; configure that instance through OSMD's own API. The lower-level OSMD renderer likewise retains application-owned styles and `osmdOptions` precedence. No global SVG recoloring overrides explicitly colored content. | Token | Default | Purpose | | --- | --- | --- | | `--wm-sheet-min-width` | `32rem` | Minimum readable engraving width inside the scrolling viewport. | | `--wm-sheet-background` | `var(--wm-surface, #fff)` | Paper behind the engraved notation; accepts `transparent`. | | `--wm-sheet-foreground` | `var(--wm-foreground, #000)` | Default SVG music and label ink for an Element-created OSMD instance. | ```css score-sheet-view { --wm-sheet-min-width: 36rem; } ``` For an owned OSMD instance, the renderer reflows on container-width changes; height-only changes do not re-engrave the score. It uses `ResizeObserver`, falls back to window resize when unavailable, and releases observation and pending resize work on disposal. A borrowed `.osmd` instance keeps its existing lifecycle.
    --- # > One score, five views: piano roll, staff, waterfall, navigation map or static thumbnail. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/element/view/score-view/ 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. `` displays a loaded score as `piano-roll` (default), `staff`, `waterfall`, `map` or `thumbnail`. Each instance renders one surface. Select `type` in the demo’s external Parameters; only relevant drawing controls appear, and switching back restores their retained values. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/element/view/score-view/) ## Import ```html ``` The demo starts in staff mode with `public/mxl/Arabesque No.1.mxl`, a notation source for Debussy's *Arabesque No. 1* with one piano part and two staves. The player loads the MXL once and the view borrows the parsed score. The demo contains one player and one score view. Expand the player’s Parameters group to configure playback. Copy includes both tags; Reset restores their initial attributes. The thumbnail type can borrow the player’s loaded score while remaining a static preview. ## Annotations and compact layout All five types show supported directions retained in the Score: words such as `rit.`, rehearsal labels, dynamics, metronome marks, pedal signs/lines and crescendo/diminuendo hairpins. The staff places them around notation; the other types place them along their time axis with compact labels and span lines. Dense labels may be abbreviated or reduced to markers; SVG titles retain their full text. The other types do not reproduce staff engraving placement. These directions are visual and do not change playback tempo, volume or sustain. Set `show-annotations="false"` to hide these directions in any type. The demo’s **show-annotations** control applies to every type and is included by Copy and Reset. `.options.showAnnotations` overrides the attribute. Changing this option retains the loaded score and playback position. ```html ``` Hidden annotations reserve no drawing space. Staff height is recomputed from the remaining notation, with compact gaps between staves and at the outer edges. Notes, rests, clefs, key/time signatures, beams, tuplets, ties, slurs and articulations remain visible. Leave `height` unset to let the component shrink; an explicit height still defines the application’s viewport. ## Geometry Time, pitch and instrument dimensions are separate. Changing a note's displayed length does not change its duration or playback rate. | Surface / dimension | Attribute | Effect | | --- | --- | --- | | Piano-roll note length / waterfall note length | `pixels-per-second` | Nominal seconds become horizontal pixels in piano roll and vertical pixels in waterfall. Larger values draw longer notes. | | Piano-roll note thickness | `note-height` | Height of each MIDI pitch lane in px. | | Waterfall note width | `white-note-width` / `black-note-width` | Exact natural-note / accidental pitch-column widths in px. | | Gap between note spans | `note-spacing` | Subtracted from piano-roll / waterfall duration length; `0` keeps the full span. | | Staff notation size | `note-height` | Scales staff notation; spacing also accounts for glyph clearance. | | Keyboard key width / length | PitchView `white-key-width`, `black-key-width` / `white-key-height`, `black-key-height` | Independent pixel widths and heights for a separately composed keyboard. | | Fretboard width / string layout | PitchView `fret-width`, `string-spacing`, `string-width` | Fret spacing, distance between strings and string stroke thickness. | [``]() owns the keyboard and fretboard settings. To align a separate keyboard with waterfall notes, match their pitch range and column widths and use one shared horizontal scroller, as below. ## Views ### Piano roll `type="piano-roll"` draws pitch vertically and score time horizontally. It follows the score position and highlights active notes. `pixels-per-second` and the pitch range control the drawing’s geometry. Annotations occupy a compact strip above the notes, aligned to nominal seconds. Hiding them removes the strip and reduces the natural height. ### Staff `type="staff"` preserves each part’s source staff assignments as separate layers by default (`split-staves="true"`). MusicXML note-to-staff assignments therefore remain visible after import. Set `split-staves="false"` to combine the staves within each part; different parts remain separate. This changes the drawing without reassigning notes in the score or restarting playback. Staff notation uses the Score's written note values, spelling, rests and voices. Eighths and shorter values form beam groups, including secondary beams and hooks; source beam marks take precedence over automatic meter-based grouping. Tuplets keep their ratios and group numbers, with brackets when needed. Authored up/down stems, ordinary same-staff slurs, ties, hidden spacing notes and measure-level clef changes are retained. Stored measure boundaries govern the drawing, including pickups and later changes of time signature. All layers align the same written onsets and share a horizontal scroll position and playhead, so the displayed staves advance together. The bottom timeline supplies manual horizontal scrolling. `scroll-type` selects page, note or bar following; `default-key` supplies a key signature when none exists. A system-start line connects the displayed staves. Staves belonging to the same part are grouped with a brace and barlines extending across their gaps. Noteheads, stems, flags and beams follow active-note highlighting; shared glyphs remain highlighted while any associated note is sounding. These are drawing features and add no controls. One full-height divider marks the edge of the fixed signature area. Scrolling notes are clipped outside that area so they do not cover the clefs or signatures. Bar following keeps the measure start visible when it fits, then follows the current beat if the measure exceeds the viewport. Following also resumes after a hidden container becomes visible. A manual pan remains in place during resize until another player position requests following. Default spacing gives noteheads, accidentals and other marks room to draw. A positive `pixels-per-second` requests rhythmic spacing using `pixels-per-second × 60 / initialQpm` pixels per quarter; glyphs can add width, so staff notation is not a linear time ruler. Later tempo changes move the playhead through the complete score time map without changing written values. The renderer reads stored rational quarter positions directly, preserving triplets without rounding to a 1/16-quarter grid. MusicXML supplies notation that MIDI cannot recover, such as authored beams, spelling and clefs. When written values or clefs are absent, the drawing uses readable duration/clef inference while preserving the Score's timeline; that is not a reconstruction of the original sheet music. Active-note highlighting uses nominal sounding times independently of the written layout. Part-local clef changes are retained at their exact positions, including inside a measure; the fixed header follows the clef at the visible position. Authored clefs are respected even when ledger lines are needed. For scores without a clef map, inference balances ledger lines against unnecessary changes and protects ties and tuplet groups. Source words such as `rit.` and tempo instructions, rehearsal marks, dynamics, metronome marks, pedal signs/lines and crescendo/diminuendo hairpins are drawn at their stored musical positions. Placement and supported text styles are retained, with space reserved around nearby notation. Tuplets respect source number visibility. Staccato, tenuto, accent, marcato and staccatissimo marks, source rest positions and barline styles are drawn. Ties and slurs use separate endpoint and collision rules, including authored slurs between staves of one part. These visual directions do not introduce playback automation. Cross-staff and cross-measure beams, nested tuplet layout, complete grace-note engraving, lyrics and page/system justification remain outside this scrolling renderer's supported subset. [``]() retains the optional OSMD page-engraving workflow. The staff type needs no separate engine option or additional installation. ### Waterfall `type="waterfall"` draws falling notes on piano-style pitch columns, with no embedded keyboard. By default, occupied octaves fit the container width. Set `show-only-octaves-used="false"` for the standard A0–C8 piano range, or set positive `white-note-width` / `black-note-width` values for fixed natural-note and accidental column widths in pixels. These widths control falling-note geometry; overflow pans inside the component. Container resizing retains the score position and active notes. Without an explicit height, the falling-note viewport uses the score duration at the selected time scale, bounded to 80–200px. A requested height uses the available viewport for the notes. The [imperative renderer]() retains its explicit pitch-width defaults. When an application also needs a keyboard, compose a separate [``]() around the same player. #### Align a waterfall and keyboard This demo uses the opening passage of Arabesque No. 1 and one player. Both views use MIDI 48–83 (C3–B5), 32px natural columns and 20px accidental columns. The 21 natural notes make a 672px track. Change **Pitch width** to resize both views together, **Note length** to change only the waterfall's time scale, and **Key height** to change only the keyboard. A narrow container pans their shared track. Press Play to see the held keys follow the notes. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/element/view/score-view/) Use the same structure with a score file in your application: ```html
    ``` Explicit `min-pitch` / `max-pitch` preserve that exact waterfall range. The matching `low` / `high` select the same keyboard keys. Zero presenter insets align their left edges; `follow="none"` prevents the keyboard from panning independently. Disabling annotations removes the waterfall's side rail from this shared pitch grid. If you resize the columns in application code, update both views and the track width together. Annotations follow the vertical time axis in a side rail. Nearby words and directions share wrapped labels with leaders to their musical positions; separate span tracks retain pedal and hairpin durations. Hiding annotations returns the rail's width to the note columns without changing a requested viewport height. ### Map `type="map"` gives a whole-piece navigation strip using note-density cells, bar numbers and score landmarks. `for-part` filters the density by part id or name; `cells` bounds the cell count. Long scores group bars rather than creating per-note graphics. Scores without bar structure use an even division and have no bar ruler marks. Directions occupy a compact strip above the density cells at their quarter positions. `show-annotations="false"` removes that strip and expression labels; the bar ruler remains available for navigation. Click a cell or seek through the lane with its pointer/keyboard controls. The view emits `webscore:seek` with `{quarters, seconds}` in nominal score time and seeks the bound player. It prefers `seekNominal(seconds)`; structural targets with only `seek(seconds)` receive rate-scaled time. A failed player seek is reported and the local position rolls back when no newer state superseded the request. Without a player, navigation updates local inspection and still emits the event. The projection remains available as [`createScoreMap(score, options)`](), with no DOM or transport ownership. ### Thumbnail `type="thumbnail"` makes one static piano-roll layout and scales it into the box. Its natural height is `3rem` plus one compact annotation strip when directions exist; normal stage padding stays inside that height. Hiding annotations returns the component to `3rem`. Nearby directions form overview groups with a readable representative label and count; SVG titles and accessible descriptions retain the complete text and musical ranges. Labels keep their readable size when an explicit height fits the notes into the remaining space. Very short explicit heights scroll inside the stage so the annotation strip cannot collapse the note preview to zero height. Note color defaults to `var(--wm-score-view-note, currentColor)`. `note-color` accepts CSS colors or an RGB triple; `.options.noteRGB` overrides the attribute, and `.options.noteColor` overrides both. There is no active-note highlighting, seeking or playback animation inside the preview. `.active` is empty. Playback time can still be read through `.currentTime` when a player is attached, but time changes do not redraw the thumbnail. ```html ``` ## Data and playback Source precedence is explicit `.score`, then `src`, then the selected player’s resolved score. Explicit URLs load through [`@webmusic/score/io`](). A player-only view borrows data without another fetch. Changing `type` reuses the resolved score and player snapshot while replacing the old presentation. The `player` selector must resolve uniquely in the current Document or ShadowRoot. Native snapshots initialize position, including paused state, then data/state/time updates keep it current. Late or replaced targets rebind; invalid or ambiguous selectors remain unbound. Disconnect cancels pending work and releases owned subscriptions and renderers without stopping playback. No type creates a player or audio resource. Only `map` sends seek commands. Native binding prefers the target's `.playback` [shared source](). An available source suppresses duplicate legacy events. A target without a source, or one reporting `unavailable`, retains the compatibility snapshot/event path. `data-player-state` exposes resolution and readiness. An explicit Score with a different identity from native playback suppresses incompatible highlighting and map navigation; use the same Score instance to share data intentionally. A custom target replacing its `.playback` property dispatches `webmusic:sourcechange`. Missing source data displays a waiting status. The three note views show an empty status for a score without notes, and load or drawing failures show an accessible error message. Replacing the source or drawing options retries the applicable work. Disconnect also releases the waterfall resize observer.
    API | Member | Kind | Description | | --- | --- | --- | | `src` / `format` | attributes | Explicit MIDI / MusicXML / MXL / ABC URL and optional parser override. | | `player` | attribute | Unique source/position selector; explicit score input retains precedence. | | `type` / `.type` | attribute / writable property | `piano-roll`, `staff`, `waterfall`, `map` or `thumbnail`; unknown values use piano-roll. | | `width` / `height` | attributes | Bare numbers are px; CSS lengths pass through. Width defaults to 100%; height follows the drawing. Thumbnail uses 3rem plus any annotation strip. A fixed height makes a taller surface scroll within its box. | | `show-annotations` | attribute | All types; default true. `false` hides score directions and removes their reserved space. Absent, empty or `true` shows them. `.options.showAnnotations` takes precedence. | | `for-part` / `.forPart` | attribute / read-only property | Map density filter by part id/name; absent includes every part. | | `cells` / `.cells` | attribute / read-only property | Map cell limit; default 64, rounded with minimum 1. | | `show-only-octaves-used` | attribute | Waterfall only; default true. Show occupied octaves; `false` selects the standard A0–C8 piano pitch range. | | `scroll-type` | attribute | Staff only: `page` (default), `note` or `bar`. | | `split-staves` | attribute | Staff only; default true. Preserve source staff assignments within each part. `false` combines that part’s staves; absent, empty or `true` preserves them. `.options.splitStaves` takes precedence. | | `pixels-per-second` | attribute | Piano-roll/waterfall pixels per second; default 30. Staff uses notation-aware spacing; a positive value requests `value × 60 / initialQpm` pixels per quarter, with extra space where glyphs require it. | | `note-height` | attribute | Piano-roll note height or staff notation scale; default 6px. | | `note-spacing` | attribute | Piano-roll/waterfall note-length gap or staff notation spacing; default 1px. | | `min-pitch` / `max-pitch` | attributes | Piano-roll/waterfall MIDI pitch limits; unspecified bounds come from the score. Explicit waterfall bounds are exact and take precedence over automatic octave range selection. | | `white-note-width` | attribute | Waterfall natural-note pitch-column width; fits the container by default. A positive explicit natural or accidental width fixes pitch scale. | | `black-note-width` | attribute | Waterfall accidental pitch-column width; defaults to 0.62 × natural-note width and cannot exceed it. Alone, derives natural-note width as `value / 0.62`. A positive explicit width disables fitting. | | `default-key` | attribute | Staff fallback major-key index, 0…11 by chromatic pitch class (0 = C, 1 = D♭, 2 = D); default 0. This is not a fifths count. | | `note-color` / `active-note-color` | attributes | Score-renderer idle/active colors; the Element’s active default is red `rgb(240, 84, 119)`. `note-color` also styles thumbnail. Map uses timeline tokens. | | `.score` | writable property | Assign a loaded Score; overrides `src` and player data, cancelling obsolete loads. | | `configure(configuration)` | method | Atomically select `piano-roll`, `staff` or `waterfall` and replace optional mode-specific `.options`. Invalid modes, malformed options and unsupported keys throw `TypeError` before changing the view. Map/thumbnail use `.type`, `.options` and their attributes. | | `.options` | writable property | `ScoreViewRenderOptions` override note-renderer attributes; map/thumbnail also honor `showAnnotations`, `noteRGB` and `noteColor`; see the [renderer API](). | | `.currentTime` | read-only property | Current nominal score position in seconds. | | `.active` | read-only property | MIDI pitches at the current score position; empty for map and thumbnail. | Type-specific attributes stay on the element when another type is selected and do not affect that other surface. Missing or invalid numeric values use their corresponding defaults. `ScoreViewElement`, `ScoreViewElementType`, `ScoreViewSeekDetail`, `ScoreViewRenderOptions`, `ScoreViewType`, `parseColorAttr` and `defineScoreViewElement(tag?)` are exported from `@webmusic/score/view/element`. `ScoreViewElementType` includes all five Element types; the lower-level `ScoreViewType` remains `piano-roll | staff | waterfall`. Registration is idempotent and SSR-safe. `ScoreViewConfiguration` and `ScoreViewOptionsByType` describe the three full renderer modes and are exported from both `@webmusic/score/view` and `@webmusic/score/view/element`. `configure()` validates mode/option keys before committing; it preserves the current source and playback position. Legacy `.type` and `.options` assignments remain supported for all five Element types. ```ts import {ScoreViewElement} from '@webmusic/score/view/element'; const configuredView = new ScoreViewElement(); configuredView.configure({type: 'staff', options: {splitStaves: true, showAnnotations: false}}); ``` **Events.** Only map emits `webscore:seek`: `{quarters, seconds}`, bubbling and composed, with nominal seconds. Other types are passive displays. **Accessibility.** The map uses the shared [timeline interaction](). Other types provide labeled graphics and current-position highlighting where applicable; thumbnail adds no interactive controls. Native external Parameters configure the selected drawing.
    Styling Staff layers within one part sit closer together when their notation allows it. The renderer checks the whole score, including offscreen notes, and retains space for stems, ties and ledger lines without shrinking the notation. Only visible annotations contribute to clearance; `show-annotations="false"` removes their space as well as their marks. The component fits its containing block. Size the host with `width` / `height` or application CSS. Fixed-height note views use a scrollable [UIKit stage]() so a drawing taller than the box remains accessible. Piano-roll/staff/waterfall use their drawing attributes and `.options`; thumbnail stretches its SVG to the requested box. Idle notes inherit `--wm-foreground` through `--wm-score-view-note`, with neutral `#333` / `#ddd` defaults for light / dark schemes. Active notes use `--wm-score-view-active-note`, then the shared `--wm-accent`, then the existing red default `rgb(240, 84, 119)`. Explicit `note-color` / `active-note-color` attributes or `.options.noteRGB` / `.options.activeNoteRGB` override those defaults; `.options.noteColor` / `.options.activeNoteColor` accept full CSS colors, including variables. The [imperative renderers]() retain their separate neutral RGB defaults. Map uses [timeline]() tokens such as `--wm-timeline-lane-height`, `--wm-timeline-background`, `--wm-accent` and `--wm-playhead`. [Status]() owns the waiting, empty and error appearance. A separately composed PitchView uses its own [pitch tokens](). The stage/timeline frame accepts `--wm-component-background`, `--wm-component-border` (a complete border shorthand) and `--wm-component-radius`; the presenter's `--wm-stage-surface-*` or `--wm-timeline-surface-*` tokens take precedence. Set `--wm-surface: transparent` as well when clearing nested drawing surfaces. Staff, piano-roll and thumbnail SVGs have transparent drawing backgrounds. For staff, piano-roll and waterfall, `--wm-score-view-background` overrides the stage viewport background. Otherwise it follows `--wm-stage-background`, `--wm-stage-surface-background`, `--wm-component-background`, then `--wm-surface`, with neutral `#fff` / `#111` defaults for light / dark schemes. Map density cells use `--wm-score-view-map`, falling back to `--wm-accent`. Their opacity still represents density. Explicit `.options.noteColor`, `.options.noteRGB` and `note-color` override that default, in that order. Thumbnail follows the same explicit precedence with its separate default above. All five types use live CSS paint expressions: changing a class, ancestor token or inline custom property updates the existing drawing without reloading the score or moving its cursor. These are light-DOM nodes: target `[part~="root"]` / `[part~="surface"]` on the presenter, and `[part~="drawing"]` on full note views and thumbnail, using ordinary selectors rather than shadow-DOM `::part()`. ```css score-view { --wm-component-background: transparent; --wm-surface: transparent; --wm-component-border: 1px solid #8899aa; --wm-component-radius: .5rem; --wm-score-view-note: #cdd8ed; --wm-score-view-active-note: #ff6688; } ```
    ## Related [Current sounding pitches]() · [Engraved sheet music]() · [View API and renderers]() · [Headless ScoreView]() · [Headless map navigation]() --- # createAnalysisFollower > Analysis, current position and held-note state borrowed from a playback source, with explicit navigation outcomes. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/headless/analyze/analysis-follower/ 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. `createAnalysisFollower(options?)` keeps a score analysis and its playback position together. It borrows a `ScorePlaybackSource`, owns its subscription and an analysis session, and publishes plain state for application-owned UI. It creates no player, audio context, timer, file request or DOM. Use [createAnalysisSession]() for caller-driven score edits alone. Use this follower when custom UI also needs source replacement, readiness, active notes and navigation. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/headless/analyze/analysis-follower/) ## Import The caller supplies `score` and a retained `ScorePlayer` named `player`. The player remains owned by its caller. ```ts import {createAnalysisFollower} from '@webmusic/score/analyze/headless'; const follower = createAnalysisFollower({playback: player.playback}); const unsubscribe = follower.subscribe((state) => { console.log(state.result?.key, state.quarters, state.activeMidis); }); const outcome = await follower.seekQuarters(4); if (outcome.status === 'failed') console.error(outcome.error); // Keep the follower while the application view is mounted. const dispose = () => { unsubscribe(); follower.dispose(); }; ``` For standalone inspection, construct with `{score}` and no playback source. Seeks then move only the local cursor; no sound is started. ## Data and time ownership An explicit `score` wins over borrowed data. `setScore(undefined)` restores borrowing from the current playback source. A different explicit Score object produces `readiness: 'mismatched'`: its analysis remains available, while incompatible player activity and navigation are suppressed. The first source snapshot hydrates state immediately; later source changes replace the analysis input and invalidate older navigation. Active notes retain occurrence identity, so releasing one voice does not clear another occurrence of the same MIDI pitch. `activeMidis` is the unique ascending pitch set. `nominalSeconds` is the rate-independent score position. `quarters` uses the same score’s tempo map. The follower reads snapshots without predicting frames; use [createTransportClock]() separately if presentation needs interpolation. ## Navigation outcomes `seekNominal(seconds)` and `seekQuarters(quarters)` return `Promise`. A bound command requires a ready, matching source with `seekNominal` support. The source owns the command and the resulting snapshot position; a follower never starts or stops playback to implement a seek. Inputs must be finite nonnegative numbers; values beyond the score duration are clamped. Invalid positions and unavailable/read-only sources produce a failed outcome. Inspect `status`: `committed` means the command completed for the current binding; `superseded` means a newer request, input replacement or disposal took precedence; `failed` carries `error`. A late completion cannot restore an old score or overwrite newer state. These outcomes describe this command, not the end of playback or completion of any presentation animation.
    API **Construction** — `createAnalysisFollower(options?: AnalysisFollowerOptions): AnalysisFollower` | Member | Kind | Default | Description | | --- | --- | --- | --- | | `score` | option | borrow source | Explicit immutable `Score`; omit to follow source data. | | `playback` | option | none | Borrowed `ScorePlaybackSource`, normally `player.playback`. | | `analysis` | option | session defaults | `AnalysisSessionOptions`: `windowQuarters: 2`, `motifLength: 4`, `minOccurrences: 2`. Fixed for this follower’s lifetime. | **Commands** | Member | Kind | Description | | --- | --- | --- | | `setScore(score \| undefined)` | command | Sets explicit input or restores borrowed input, updating derived analysis and invalidating pending navigation. | | `setPlayback(source \| undefined)` | command | Detaches the previous source, observes the new one and hydrates its state. Does not dispose either source. | | `seekNominal(seconds)` | command | Navigates in nominal score seconds and returns an `AnalysisSeekOutcome` promise. | | `seekQuarters(quarters)` | command | Converts quarter-note position through the current score and returns the same outcome type. | **`AnalysisFollowerState`** | Member | Kind | Description | | --- | --- | --- | | `revision` | state | Revision of the published follower state. | | `score` | state | Effective Score, or `undefined` when unavailable. | | `result` | state | `AnalysisResult` for the effective score, or `undefined` without an available result. | | `readiness` | state | `ScorePlaybackReadiness` or `'mismatched'`; standalone supplied data is ready. | | `playbackState` | state | Source `ScorePlaybackState`. | | `nominalSeconds` | state | Current nominal position or `null` when unavailable. | | `quarters` | state | Current quarter-note position or `null` when unavailable. | | `activeNotes` | state | Read-only `ScorePlaybackNote[]` with source occurrence identity. | | `activeMidis` | state | Read-only unique ascending active MIDI values. | | `error` | state | Optional source/analysis error; availability is separate from failure. | Read the current snapshot through `follower.state`. `AnalysisSeekOutcome` contains `status` and `nominalSeconds`; a failed outcome also contains `error`. **Events and lifecycle** | Member | Kind | Description | | --- | --- | --- | | `subscribe(listener)` | subscription | Synchronously delivers the current state, then later changes. Returns an unsubscribe function. The callback receives one `AnalysisFollowerState`. | | `dispose()` | lifecycle | Idempotently releases source observation and listeners and supersedes pending seeks. Borrowed playback is left running and owned by its caller. | `setScore`, `setPlayback` and new subscriptions after disposal throw. Seeks after disposal return a failed outcome. Disposal leaves readable empty state with `readiness: 'disposed'`. Listener exceptions propagate to their caller; a throwing initial subscriber is removed. Derived-analysis failures appear in `state.error` with `readiness: 'error'` and no result. If the old subscription's cleanup throws, replacement still completes before that error is rethrown. If attaching the new subscription fails, borrowed state is cleared while explicit input is retained. When both operations fail, an `Error` with a read-only `errors` array reports both failures. Retrying `setPlayback` can attach a working source; teardown never stops the borrowed player. ### Presentation clock `createTransportClock(options?)` predicts a display position between playback samples. It consumes timestamps supplied by the application and returns nominal score seconds. It creates no timer, animation frame, player or subscription. Use it for smooth presentation when player updates arrive less often than frames. This predictor is separate from Kernel’s authoritative transport clock; its predicted position must not schedule sound or replace a player snapshot. #### Clock Import ```ts import {createTransportClock} from '@webmusic/score/analyze/headless'; const clock = createTransportClock({durationSeconds: 20}); clock.sample({nominalSeconds: 4, rate: 1.5, atMs: 1000}); const reading = clock.readAt(1020); console.log(reading.seconds, reading.held, reading.epoch); clock.stop(1030, 4.045); ``` Keep one timestamp origin for samples, reads, stops and releases. Times ending in `Ms` are milliseconds; positions and durations are nominal score seconds. `rate` is nominal seconds per elapsed real second. #### Clock Following and scrubbing Samples re-anchor the prediction. The clock measures their interval and coasts for a bounded interval, then settles with `held: true` if updates stop. It can detect a seek or loop wrap from a discontinuity, but cannot predict a loop region. The `epoch` increases on discontinuities so a presenter can snap immediately. Use `hold(seconds)` during a drag, then `release(atMs)`. The released position stays parked until a sample is accepted; briefly delayed echoes of the old position are ignored. These operations change local presentation only and never seek a player. Invoke the owner’s seek command separately, or use [createAnalysisFollower]() for an explicit navigation outcome. **Construction** — `createTransportClock(options?: TransportClockOptions): TransportClock` | Member | Kind | Default | Description | | --- | --- | --- | --- | | `durationSeconds` | option | no upper bound | Finite nonnegative duration clamps readings to `[0, duration]`. Invalid values disable the upper bound; zero is a valid bound. Positions are always nonnegative. | | `expectedIntervalMs` | option | `50` | Initial sample-interval hint. A positive finite value is clamped to `8…400` ms; invalid values use the default. Later valid sample intervals update the estimate. | **Commands and readings** | Member | Kind | Description | | --- | --- | --- | | `sample({nominalSeconds, rate, atMs})` | command | Accepts a `TransportSample`. Invalid positions/timestamps are ignored; invalid or negative rates preserve the last rate. `rate: 0` is valid. | | `readAt(atMs)` | query | Returns `TransportReading` without advancing state. Invalid timestamps return the current anchor; earlier timestamps do not extrapolate backward. | | `stop(atMs, seconds?)` | command | Parks at the supplied position, or the current prediction when omitted, sets rate to zero and increments the epoch. | | `hold(seconds)` | command | Holds a drag position, with clamping. Each distinct held position increments the epoch; incoming samples cannot move it. | | `release(atMs)` | command | Releases an active hold and parks there until a suitable sample. Allows a 250 ms grace period for stale seek echoes. No effect without a hold. | Invalid command positions retain the current anchor. Invalid stop/release timestamps retain the previous anchor timestamp. **`TransportReading`** | Member | Kind | Description | | --- | --- | --- | | `seconds` | result | Clamped nominal position. Before the first sample, `0`. | | `held` | result | Whether parked, scrubbed, rate-zero or past the bounded coast. Initially `true`. | | `epoch` | result | Discontinuity counter, initially `0`. | | `rate` | result | Last accepted rate, initially `1`; `stop()` sets it to `0`. | **Helpers** `rateFromDurations(nominalDurationSeconds, transportDurationSeconds): number` returns their ratio when both durations are positive finite numbers and the ratio is finite; otherwise it returns `1`. Unlike dividing current positions, this remains defined at the start of playback. **Lifecycle and events** There is no `dispose()`, subscription or event emitter. Drop the object when unused; release application-owned input/frame subscriptions yourself. Options are fixed at construction. The public types are `TransportClock`, `TransportClockOptions`, `TransportSample` and `TransportReading`. #### Clock Related [Analyze lanes]() use this predictor for display motion. [createAnalysisFollower]() reads the player’s authoritative state; [Kernel]() owns neutral transport contracts.
    ## Related [Analyze Web Components]() adapt shared nonvisual following behavior to their DOM inputs and presenters. [Live trackers]() accept raw note events; [projections]() turn analysis into plain data for custom surfaces. [createScoreMapView]() provides score-map navigation without analysis. --- # createAnalysisSession > An analysis session over an immutable score, reusing identity-keyed caches across edits. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/headless/analyze/analysis-session/ 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. `createAnalysisSession(score, options?)` holds one analysis and keeps it current as the score changes. `update(next)` reuses identity-keyed analysis caches for unchanged immutable parts and recomputes affected analysis paths. Score-wide relationships can still require broader work. Its result has the same analytical meaning as the corresponding stateless analyses of `next`; the session adds cache reuse, not a different musical interpretation. Reach for the session when a score is being edited or streamed in. Use [createAnalysisFollower]() when the result must follow a player’s loaded score, position and active notes. For a one-shot answer, the stateless functions on [`@webmusic/score/analyze`]() are cheaper and need no object. It **borrows** the `Score` and owns only its caches. There is no `dispose()`: the caches are keyed on the immutable parts themselves, so an edited part arrives with empty caches and an untouched one keeps its analysis. There is no playback clock, event subscription or automatic edit listener; the caller supplies each new immutable score through `update()`. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/headless/analyze/analysis-session/) ## Import ```ts import {createAnalysisSession} from '@webmusic/score/analyze/headless'; const session = createAnalysisSession(score); console.log(session.result.key, session.result.chords.length); const edited = score.edit((tx) => tx.updateNote(id, {dynamic: 'mf'})); const next = session.update(edited); // reuses unchanged-part caches ``` ## Why identity matters `Score.edit` and `Score.withPart` return a new `Score` whose **untouched parts are the same objects**. The session's caches are module-level `WeakMap`s keyed on those parts, which is what makes the incremental path safe rather than merely fast: a stale entry is impossible, because an edited part is a different object with no entry at all. Several sessions on one page share those caches. Calling `update` with the score it already holds is a no-op that returns the cached result, so a render loop can call it unconditionally. ## Off the main thread [createAnalysisWorker]() runs the same analysis session behind promises. Its reference covers worker ownership, fallback, update supersession, errors and cleanup.
    API **Construction** — `createAnalysisSession(score: Score, options?: AnalysisSessionOptions)` | Member | Kind | Default | Description | | --- | --- | --- | --- | | `windowQuarters` | option | `2` | Chord window length in quarter notes. | | `motifLength` | option | `4` | Notes per motif. | | `minOccurrences` | option | `2` | Minimum repeats before a motif counts. | **Commands** | Member | Kind | Description | | --- | --- | --- | | `update(score)` | command | Re-analyses after an edit and returns the new result. A no-op when the score is unchanged. | **State** | Member | Kind | Description | | --- | --- | --- | | `score` | state | The score the current `result` describes. | | `result` | state | The `AnalysisResult`. Stable reference until the next `update`. | | `result.key` | state | Krumhansl–Schmuckler key estimate, as `detectKey`. | | `result.chords` | state | Harmonic segmentation, as `segmentChords`. | | `result.roman` | state | Roman-numeral analysis of `chords` in `key`. | | `result.motifs` | state | Repeated melodic motifs, as `findMotifs`. | | `result.issues` | state | Voice-leading issues, as `voiceLeading`. | Every result is transitively frozen, so a consumer can hold one across an update without copying. Voice-leading issues retain raw `voices` identifiers and also supply `voiceParts`, paired by index, to distinguish equal voice names in different parts. Older worker results may omit that optional field; a voice projection does not assign an ambiguous legacy identifier to an arbitrary row. The options are fixed for a session's lifetime; create another session to change them. `AnalysisResult.key` is `KeyResult`, `chords` is a read-only `ChordSegment[]`, `roman` a read-only `RNAResult[]`, `motifs` a read-only `Motif[]`, and `issues` a read-only `VoiceLeadingIssue[]`. These result structures are described with their stateless producers on the [Analyze API](). The session does not add the rhythmic pattern Element's inspection state or report-formatting fields to this result. ### Worker client API The [Analysis worker](#analysis-worker) section below covers execution, cancellation and worker ownership. ### Analysis worker `createAnalysisWorker(workerOrFactory?)` runs an [analysis session]() through a Promise API. Use it when score analysis would otherwise block interaction. It borrows a supplied worker or owns one created by its default path or a supplied factory. When the default worker is unavailable, analysis runs on the calling thread. #### Worker Import ```ts import {createAnalysisWorker} from '@webmusic/score/analyze/headless'; const client = createAnalysisWorker(); try { const result = await client.analyze(score, {windowQuarters: 2}); paintAnalysis(result); } finally { client.dispose(); } ``` The same client is also exported by `@webmusic/score/analyze/worker-client`. Its promise resolves with an analysis result, not with playback completion. The source contract is `AnalysisWorkerClient`, `AnalysisWorkerFactory`, `AnalysisWorkerLike` and `AnalysisWorkerEventLike`, re-exported from the Headless entry and from `@webmusic/score/analyze/worker-client`. | Member | Return / contract | | --- | --- | | `createAnalysisWorker(workerOrFactory?)` | `AnalysisWorkerClient`. A supplied ready-made worker is borrowed; a factory result or default worker is owned. | | `analyze(score, options?)` | `Promise`; creates/replaces the worker's analysis session with the supplied options. Supersedes older pending updates as described below; each analysis promise retains its own result/error. | | `update(score)` | `Promise`; updates the session. For overlapping worker-backed updates, pending promises settle with the newest queued result and superseded queued scores are skipped. | | `dispose()` | `void`; rejects pending calls and detaches listeners where `removeEventListener` is available. Terminates only owned workers. Repeated calls are safe. | For a worker-backed client, a new `analyze` replaces older queued updates and ignores their already-sent replies. Those update promises remain pending and settle with the new analysis result, or with the newest `update` called after it. Such later updates wait for the newest analysis to succeed before running with its options. An older overlapping `analyze` still settles its own promise, but cannot release that queue. If the newest analysis fails, all pending update promises reject with that analysis error and their queued input is dropped; the client does not silently apply it with the previous session's options. A caller can retry `analyze` with valid options. Disposal and terminal worker failures reject every pending call. This supersession does not cancel CPU work already executing in the Worker. When Workers are unavailable or the default worker cannot be constructed, the default factory uses an in-process implementation with the same Promise API. That fallback performs analysis on the calling thread; it does not move CPU work into a background task. A supplied factory's construction error is not hidden by fallback. Worker runtime/protocol failures can make a client terminal; future calls then reject and an application must replace the client. Completed results are transitively frozen in both execution modes. Score serialization failures reject the returned promise without leaving a pending request. If installing a supplied worker’s listeners fails, construction removes partially installed listeners where removal is supported and terminates only an owned worker before rethrowing the error. `AnalysisWorkerLike` requires `postMessage(message)`, `addEventListener('message' | 'error', listener)` and `terminate()`; `removeEventListener()` is optional. Its listener receives `AnalysisWorkerEventLike` with optional `data` and `message`. Supply listener removal for borrowed long-lived workers so client disposal can detach fully. | Error / protocol export | Contract | | --- | --- | | `AnalysisWorkerRemoteError` | `Error` subclass constructed from `AnalysisWorkerErrorDetails`; exposes `code`, `operation`, `retryable`, optional `causeCode`, and the inherited message. | | `AnalysisWorkerErrorDetails` | `version: 1`, `code`, `operation` (`'protocol'`, `'analyze'`, `'update'`), `message`, `retryable`, optional `causeCode`. | | `AnalysisWorkerErrorCode` | Known codes include `'unsupported-protocol'`, `'invalid-request'`, `'analysis-failed'`; other strings remain representable. | | `ANALYSIS_WORKER_PROTOCOL` | `'@webmusic/score/analyze/analysis-worker'`. | | `ANALYSIS_WORKER_PROTOCOL_VERSION` | `1`. | | `ScoreJSON` | Return type of `Score.toJSON()`; metadata must be structured-clone safe. | | `AnalyzeRequestMessage` · `UpdateRequestMessage` | Request envelope (`protocol`, `protocolVersion`, numeric `id`) plus `action` and `score`; only `'analyze'` adds optional analysis options. | | `AnalysisWorkerRequest` | Union of the two request message types. | | `AnalysisWorkerResponse` | Same identity/version/id plus either `ok: true, result`, or `ok: false, error, errorDetails`. | No worker method emits a UI or playback event. Handle request failures through the returned promises. `retryable` describes a remote operation failure; it does not promise that a separately failed/terminated client can be reused. #### Worker Transfer and cancellation Each request serializes the complete `Score` through `toJSON()` and the worker reconstructs it. Transfer and reconstruction cost scale with score size even when unchanged parts reuse incremental analysis caches. This API has no `AbortSignal`, timeout or cancel command. Supersession skips queued updates; it cannot interrupt CPU work already running. The [worker protocol]() owns the wire message contract and bundler entry points. The React [`useScoreAnalysisAsync`]() hook composes this client. #### Worker Related [createAnalysisSession]() provides the synchronous result. [createAnalysisFollower]() combines synchronous analysis with borrowed playback state.
    ## Related **Composed by.** The score-based [live Analyze components](). Its motif data also remains available to application-owned views without a dedicated Element. **Related.** Whole-score summaries, histograms and rhythmic-vocabulary reports use the [report workflow](). They are not fields on `AnalysisSession.result`. [Live trackers]() analyze note events without a score; [createScoreReport]() formats a one-shot card. Source symbols for the synchronous API are `createAnalysisSession`, `AnalysisSession`, `AnalysisSessionOptions` and `AnalysisResult` at `@webmusic/score/analyze/headless`. --- # Live trackers > createLiveChordTracker and createLiveKeyTracker: name the chord sounding now, and estimate a key from what has been heard — from note events alone, with no score. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/headless/analyze/live-trackers/ 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. Two trackers that analyse a **performance** rather than a piece. Neither takes a `Score`: they accumulate `noteOn` events as they arrive, so the same code works for playback, a MIDI keyboard or [``](). They share a page because they are the same kind of thing driven by the same event, and a reader choosing between them is choosing what to ask, not which object to learn. They own their accumulated musical state but create no player, clock or subscription. Both are push-only: the application feeds note events and owns any source listeners. Release those listeners at teardown; the trackers have no disposal method. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/headless/analyze/live-trackers/) ## Import ```ts import { createLiveChordTracker, createLiveKeyTracker, } from '@webmusic/score/analyze/headless'; const chord = createLiveChordTracker({ onUpdate: ({chord, midis, history}) => paint(chord, midis, history), onChordChange: (name) => announce(name), }); const key = createLiveKeyTracker((result, heard) => paintKey(result, heard)); const offOn = player.on('noteOn', (note) => { chord.noteOn(note.midi); key.noteOn(note.midi); }); const offOff = player.on('noteOff', (note) => chord.noteOff(note.midi)); // On unmount: offOn(); offOff(); chord.reset(); key.reset(); ``` ## createLiveChordTracker — what is sounding Tracks the set of notes held right now and names it. `onUpdate` fires on every change with the chord name, the sorted MIDI values and the recent history; `onChordChange` fires only when the detected name changes to a nonempty name. Silence and reset still update state through `onUpdate`. Held MIDI pitches are reference-counted: two attacks need two releases. History contains at most eight non-empty chord names, most recent last. Use `onUpdate` to observe the transition to silence; `onChordChange` does not announce an empty name. Notifications are synchronous and callback failures propagate to the caller; they are not player-style error events. ## createLiveKeyTracker — what has been heard Accumulates a duration-agnostic pitch-class histogram and re-estimates the key on every note, calling back with the estimate and the number of notes heard. `heard` reports sample size, not statistical certainty. Repeating one pitch many times can still give a poor estimate of a whole piece. The correlation ranking uses major/minor profiles; its confidence is candidate separation, not a calibrated probability that the key is correct. It deliberately has **no `noteOff`**: the histogram counts onsets, so a release carries no information. Feeding it releases would double-count. ## Key diagnostics Candidate rankings and circle-of-fifths comparisons are available as plain data for an application-owned inspector. Feed heard notes to `createLiveKeyTracker`, then use its `KeyResult.scores` directly or the existing projections: ```ts import { createLiveKeyTracker, projectKeyCandidates, projectTonality, } from '@webmusic/score/analyze/headless'; const tracker = createLiveKeyTracker((result, heard) => { const candidates = projectKeyCandidates(result, {limit: 5}); const tonality = projectTonality(result, { reference: {tonic: 'C', mode: 'major'}, }); console.log({heard, candidates, tonality}); }); for (const midi of [60, 64, 67, 72]) tracker.noteOn(midi); tracker.reset(); ``` These projections do not render, select a reference or bind to a player. The application owns those controls and recomputes the projection when the reference changes. The [projection reference]() owns their contracts. `reset()` clears the key tracker without emitting a callback, so an application also clears its displayed evidence at that boundary. ## Pitch evidence The key tracker exposes the estimate and total heard count, not its internal pitch-class bins. An application needing per-pitch counts retains its own 12 counters, increments the validated MIDI pitch’s class on each note-on, and passes those values to `projectPitchClassMeters`. That projection formats supplied bins; it does not collect note events. Reset the counters at the application’s chosen seek, stop or source-change boundary. For whole-score duration weights, use [`distributions(score).pitchClasses`](), as shown in the [report workflow](). Counts and duration weights are different units: use one source at a time, or normalize compatible evidence explicitly before comparing it. ## Chord history `createLiveChordTracker` publishes the current name and a list of up to eight recent nonempty chord names. This list has no timestamps. For a time-based trail, the application can retain timestamped `onUpdate` values from its own chosen clock, detect transitions and apply its own retention bound and reset policy. The former Element’s 32-transition trail and eight-second coast were private presentation behavior, not a public Headless history API. They are not reproduced by `LiveChordState.history`. Use [``]() for the ready-made current nameplate, or retain tracker updates for a custom history.
    API **Construction** | Member | Kind | Description | | --- | --- | --- | | `createLiveChordTracker({onUpdate, onChordChange?})` | factory | `onUpdate(state)` after commands; `onChordChange(chord, midis)` only when the detected name changes to a nonempty name. No initial callback. | | `createLiveKeyTracker(onUpdate)` | factory | `onUpdate(result, heard)` after every note. | **Commands — chord tracker** | Member | Kind | Description | | --- | --- | --- | | `noteOn(midi)` | command | Increments a held-note count; returns `void`. Requires a safe integer in 0…127. | | `noteOff(midi)` | command | Decrements one held-note count; validates the same MIDI range and returns `void`. | | `reset()` | command | Clears sounding counts/history and publishes empty state; returns `void`. | **Commands — key tracker** | Member | Kind | Description | | --- | --- | --- | | `noteOn(midi)` | command | Adds one onset and calls `onUpdate`; requires a safe integer in 0…127 and returns `void`. | | `reset()` | command | Empties the histogram without a callback; returns `void`. | **State** | Member | Kind | Description | | --- | --- | --- | | `LiveChordState.chord` | state | The chord name, or `''` when nothing sounds. | | `LiveChordState.midis` | state | Sounding MIDI values, ascending. | | `LiveChordState.history` | state | Up to eight recent nonempty chord names, most recent last; no timestamps. | | `LiveKeyTracker.heard` | state | How many notes have been counted. | | `LiveKeyTracker.result()` | state | The current `KeyResult`, or `undefined` before any note. |
    The source contracts are `LiveChordTracker`, `LiveChordState` and `LiveKeyTracker` at `@webmusic/score/analyze/headless`. The key callback receives `KeyResult` as described by [detectKey](). Trackers do not interpret elapsed seconds or musical beats; they only process supplied pitches. The key tracker weights onsets equally, not by duration, velocity or recentness. ## Related **Composed by.** [``]() uses the chord tracker for sounding-note state. Key diagnostics and retained history are available to application-owned code through the APIs above. [createAnalysisSession]() works on an immutable score. [createActiveNoteTracker]() tracks held pitches without naming chords. --- # Score Headless > Code-only Score engines, state models and helpers — the purpose of each /headless entry and the reference page that owns every exported value. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/headless/ 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. Headless components are the code-only domain objects behind the elements: engines, sessions, controllers and their supporting helpers imported from `@webmusic/score//headless`. They never touch the DOM. | Capability | Contents | | --- | --- | | [Play](#play) | `ScorePlayer`, `TonePlayer`, controllers, rack, tempo. | | [Analyze](#analyze) | Analysis sessions, playback followers, live trackers, workers and presentation prediction. | | [View](#view) | Score windows, map navigation, current-pitch views and held-note tracking. | Use these objects when your application owns the interaction or presentation layer. The references document inputs, commands, state, event payloads and resource/time roles; pure helpers describe their inputs and results instead of inventing a lifecycle. Combine them with [UI Kit]() presenters or a custom surface. Rendering adapters are separate from Headless objects and are documented on the capability API pages. The export tables below route every exported value to its owning reference page. `check:docs` checks that each exported value name occurs in backticks on this page or on a page in the capability's directory; it does not verify the linked page's signatures, type coverage, unique reference ownership or completeness. Those require source review. ## Play The **Headless Components** of [`@webmusic/score/play`]() — the stateful engines you `new` up and drive yourself, with no DOM and no custom elements. Some pages include a real interactive demo; others describe helpers or optional integrations. In your own app you bring your own UI, or none. Each reference separates API details from its example. Import from `@webmusic/score/play/headless`. The stateless half of the capability — SFZ parsing, offline WAV render, recording back into a `Score` — is on [`@webmusic/score/play`](). **Engines** | Export | Kind | Page | | --- | --- | --- | | `ScorePlayer` · `Player` · `createScorePlayer` · `createScorePlayerFromUrl` · `playScore` · `playScoreFromUrl` | class · alias · factories | [ScorePlayer]() | | `InteractivePlayer` · `createInteractivePlayer` | class · factory | [InteractivePlayer]() | | `LoopPlayer` · `createLoopPlayer` | class · factory | [LoopPlayer]() | | `AbPlayer` · `createAbPlayer` | class · factory | [AbPlayer]() | | `TonePlayer` | class | [Tone.js]() | | `Metronome` | class | [Metronome]() | **Timbres and synths** | Export | Kind | Page | | --- | --- | --- | | `Sound` | recipe factories · class instances | [Sound]() | | `OscillatorSynth` · `createOscillatorSynth` | class · factory | [Synth backends]() | | `SoundfontSynth` · `createSoundfontSynth` | class · factory | [Synth backends]() | | `SpessaSynthSynth` | class | [Synth backends]() — the optional SF2 peer | | `RangeSampler` · `chooseZone` · `playbackRateFor` | sampler helpers | [Synth backends]() | **Effects** | Export | Kind | Page | | --- | --- | --- | | `Effect` · `insertEffect` · `resolveEffect` | namespace · helpers | [Effect]() | | `createReverbNode` | factory | [Effect]() | **Ensembles** | Export | Kind | Page | | --- | --- | --- | | `Rack` · `createRack` | class · factory | [Rack]() | | `RackTransportController` · `createRackTransportController` | class · factory | [Rack]() | | `SynthPanelAudioGraph` | class | [Synth backends]() — the graph `` owns | **Controllers** | Export | Kind | Page | | --- | --- | --- | | `PlayerController` | class | [PlayerController]() | | `MidiPlayerController` | `PlayerController` alias | [PlayerController]() — [MIDI binding distinction]() | | `TempoController` · `appendTap` · `tapBpm` · `TAP_WINDOW` · `TAP_RESET_GAP` | class · tap helpers | [Metronome]() | | `LfoController` · `createLfoController` · `lfoWave` · `LFO_SHAPES` | class · factory · helpers | [Synth backends]() | | `EqController` · `createEqController` | class · factory | [Effect]() | | `eqFrequencyToX` · `eqXToFrequency` · `eqGainToY` · `eqYToGain` | curve maths | [Effect]() | | `DEFAULT_EQ_BANDS` · `EQ_MIN_FREQUENCY` · `EQ_MAX_FREQUENCY` · `EQ_MAX_GAIN` · `EQ_RESPONSE_SAMPLES` | constants | [Effect]() | | `LevelMeterController` · `createLevelMeterController` | class · factory | [Synth backends]() — the score-side meter over the kernel tap | **Recording and rendering** | Export | Kind | Page | | --- | --- | --- | | `ScoreRecorderSession` · `recordedNotesToScore` · `recorderNow` · `SCORE_RECORDER_VOICE` | session · helpers | [InteractivePlayer]() — capture back into a `Score` | | `renderScoreToBuffer` · `renderScoreToWav` · `bufferToWav` | offline render | [ScorePlayer]() | **Input drivers** | Export | Kind | Page | | --- | --- | --- | | `bindClock` | driver | [Transport drivers]() | | `bindMidiInput` | driver | [Web MIDI]() | Option, event, state and structural interface types belong with their object or helper. Examples include `ScorePlayerOptions`, `PlayerTimeUpdate`, `RenderOptions`, `InteractivePlayerEvents`, `HeadlessSynth`, `TonePlayerOptions` and `TonePlayerEvents`. Use the linked references for their fields and ownership rules; a value-name coverage check is not a type reference audit. ## Analyze `@webmusic/score/analyze/headless` contains analysis objects: a session with caches that survive edits, a playback follower, a worker client, and trackers that accumulate notes. It also exports the pure `createScoreReport` presentation helper. The capability root provides the stateless analytical functions. No DOM, no styles, no rendering. Import from `@webmusic/score/analyze/headless`; the elements that draw these results are under [Web Components › Analyze](). | Export | Kind | Page | | --- | --- | --- | | `createAnalysisSession` | session | [createAnalysisSession]() | | `createAnalysisFollower` | playback follower | [createAnalysisFollower]() | | `createAnalysisWorker` · `AnalysisWorkerRemoteError` · `ANALYSIS_WORKER_PROTOCOL` · `ANALYSIS_WORKER_PROTOCOL_VERSION` | factory · error · constants | [createAnalysisWorker]() | | `createLiveChordTracker` | tracker | [Live trackers]() | | `createLiveKeyTracker` | tracker | [Live trackers]() | | `createScoreReport` | report builder | [createScoreReport]() | | `createTransportClock` · `rateFromDurations` | predictor · helper | [createTransportClock]() | | `projectSounding` · `projectNameplate` · `soundingMidisAt` · `projectProgression` · `projectKeyFlow` · `projectMotifFlow` · `projectMotifOccurrences` · `projectRhythmPatternFlow` · `projectVoiceFlow` · `projectTonality` · `projectKeyCandidates` · `projectPitchClassMeters` · `keyPitchClasses` · `formatMetricalPosition` | projections | [The projections]() | The worker row is one module rather than one object: the entry re-exports `./worker-client` wholesale, and `@webmusic/score/analyze/worker-client` publishes the same four values as an entry of its own. Result and option types (`AnalysisResult`, `AnalysisSession`, `AnalysisSessionOptions`, `AnalysisFollower`, `AnalysisFollowerOptions`, `AnalysisFollowerState`, `AnalysisSeekOutcome`, `LiveChordState`, `LiveChordTracker`, `LiveKeyTracker`, `ScoreReport`) are described with the object that produces them. Clock types (`TransportClock`, `TransportClockOptions`, `TransportSample`, `TransportReading`) belong to the follower’s presentation-clock section; projection types belong to [the projection reference](). A **session** analyses a whole score and re-analyses it cheaply after an edit. A **tracker** never sees a score at all: it accumulates note events as they arrive, so it works for a live keyboard as readily as for playback. Reach for the session when you have the piece, and a tracker when you have the performance. The report builder is a third shape: a pure formatting function with no retained state or disposal. Worker clients have explicit ownership and disposal; sessions and live trackers do not allocate background resources. ## View `@webmusic/score/view/headless` is the **state** half of drawing a score: it tracks what a viewer should be looking at without drawing any of it. No DOM, no canvas, no styles — that is the rule every `/headless` entry follows, and here it is the whole point, because your framework owns the surface. Import from `@webmusic/score/view/headless`. `createScoreView` owns sequence and viewport state for one Score. `createScoreMapView` follows score replacement and delegates quarter-note navigation. `createPitchView` follows held pitches without needing a score for manual input. Each releases its subscriptions through `dispose()` and leaves borrowed playback with its owner. The smaller `createActiveNoteTracker` retains only pitch counts and subscriptions and has no dispose method. None owns an audio or playback clock. | Export | Kind | Page | | --- | --- | --- | | `createScoreView` | view model | [createScoreView]() | | `createScoreMapView` | navigation model | [createScoreMapView]() | | `createPitchView` | held-pitch model | [createPitchView]() | | `createActiveNoteTracker` | tracker | [createActiveNoteTracker]() | Types (`ScoreView`, `ScoreViewOptions`, `ScoreViewState`, `ScoreViewTimeRange`, `ScoreViewType`, `ScoreViewListener`, `ActiveNoteState`, `ActiveNoteTracker`, `ActiveNoteListener`) are described with the object that produces them. `ScoreMapView`, `ScoreMapViewOptions`, `ScoreMapViewState`, `ScoreMapViewListener` and `ScoreMapSeekOutcome` belong to the map-view page; `PitchView`, `PitchViewOptions`, `PitchViewState` and `PitchViewListener` belong to the pitch-view page. ### Not here: the renderers `renderPianoRollVisualizer`, `renderStaffVisualizer`, `renderWaterfallVisualizer`, `renderOSMDStaffVisualizer` and `bindPlayerToVisualizer` are **not** headless components. They draw into a caller-owned element, so by rule they are `/render` adapters, and they are documented on [`@webmusic/score/view`]() with the rest of the code-facing surface. --- # AbPlayer > Live arrangement switching between two sources on one transport. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/headless/play/ab-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. `AbPlayer` switches between named arrangements (A / B / …) of one piece through `InteractivePlayer.select()`. Selection is synchronous and carries the cursor proportionally; the next metronome beat plays from the selected source. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/headless/play/ab-player/) ## Import ```ts import {AbPlayer} from '@webmusic/score/play/headless'; const ab = new AbPlayer({A: firstScore, B: secondScore}, {bpm: 100}); const off = ab.player.on('beat', (beat) => paintBeat(beat)); await ab.play(); ab.select('B'); off(); ab.dispose(); ``` The source contract is `AbPlayer`, `createAbPlayer` and `AbPlayerOptions` at `@webmusic/score/play/headless`. It owns an `InteractivePlayer`, its metronome and its default quiet triangle `Sound`; source scores and an injected `sound` are borrowed. The metronome drives written beats at the chosen BPM. Audio is prepared before the clock starts, so `play()` is async. Selection does not promise device-level zero latency or cancel every previously sounding release tail.
    API **Construction and inputs** | Member | Default / contract | | --- | --- | | `new AbPlayer(sources, options?)` · `createAbPlayer(sources, options?)` | Returns `AbPlayer`; `sources` is a record of name to immutable `Score`, in registration order. | | `AbPlayerOptions.sound` | Optional borrowed `HeadlessSynth`; default is an owned quiet triangle oscillator. | | `AbPlayerOptions.bpm` | Quarter-note BPM. Defaults to the first source's initial tempo converted using its tempo unit, otherwise `100`. | **Commands** | Member | Return / contract | | --- | --- | | `play()` | `Promise`; prepares audio then starts the metronome. Concurrent pending calls share startup; no-op when already playing or empty. | | `pause()` | `void`; cancels pending startup, including a pause issued inside a sound's preparation callback, stops the metronome and releases held notes, preserving cursor position. An immediate `play()` can start a fresh attempt. | | `select(id)` | `void`; switches with proportional cursor carry; unknown/current ids are ignored. | **State and events** | Member | Contract | | --- | --- | | `ids` | Read-only arrangement-id list in registration order. | | `active` | Selected arrangement id, or `undefined` for an empty source record. | | `playing` | Whether its metronome is running. | | `player` | Underlying `InteractivePlayer`; use `player.position` and `player.on(...)` for its state and events. | There is no separate `AbPlayer.on()`, seek or stop API. The underlying engine's [complete event and command contract]() applies when accessed through `player`; use `ab.select()` for arrangement switches so the wrapper's `active` state stays in sync. **Lifecycle** `dispose(): void` is idempotent, cancels a pending start and tears down the owned metronome/engine. It does not dispose an injected sound. After disposal, `play()` rejects and `select()` throws; `pause()` is harmless. Release caller-owned input and event bindings when detaching the UI.
    ## Related - [InteractivePlayer]() — source routing and beat events. - [Metronome]() — beat clock. - [Sound]() — timbre preparation and route ownership. --- # Effect > The post-processing chain: describe an effect as data, and let a player or the offline renderer wire it. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/headless/play/effect/ 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. `Effect` describes post-processing for a player, interactive voice, or [`Rack`]() master bus. A recipe allocates no Web Audio nodes until `build()` or `createAudioNodes()` runs. The built result belongs to the graph that realizes it; the recipe itself has no running clock or disposal. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/headless/play/effect/) ## Import ```ts import {Effect, ScorePlayer} from '@webmusic/score/play/headless'; const effect = Effect.chain( Effect.filter({frequency: 4000}), Effect.delay({delaySeconds: 0.25, feedback: 0.2, wet: 0.15}), ); const player = new ScorePlayer(score, {effect}); await player.play(); // Starts the player; does not wait for completion. // Keep the player during playback and release its realized graph at teardown. const dispose = () => player.dispose(); ``` ## Graph ownership and time Built-in recipes can be realized separately for separate graphs. A custom factory should also create a fresh node bundle per realization. Passing a fixed `EffectNode` to `Effect.custom()` returns that same bundle every time; sharing it shares its routing, parameters, and cleanup responsibility. `build(context)` takes an `AudioContext`; `createAudioNodes(context)` accepts a `BaseAudioContext`, including offline contexts, through the shared structural effect contract. A custom factory must actually support the chosen environment and use nodes from the supplied context. No helper closes a borrowed context. Effect recipes have no transport, events, or subscriptions. Numeric options use Web Audio units: seconds, Hz, dB, linear gains, and dimensionless ratios where indicated below. Tremolo and chorus start modulation oscillators at realization; their `EffectNode.dispose()` stops those oscillators. Live `AudioParam` values can be changed by the graph's owner using its own clock. Distortion and overdrive transfer curves include both input endpoints (`-1` and `+1`). Their symmetric sampling preserves zero-input silence under Web Audio's linear curve interpolation. For manual realization, retain the node bundle, remove the graph edges you connected, and call its optional `dispose()`. `insertEffect()` supplies a cleanup handle for its own insertion edges and the realized bundle. Built-in construction does not impose a uniform option validator: stated clamps apply, while other invalid values can fail in the underlying Web Audio API. Custom factories are responsible for cleaning allocations made before they throw. ## EqController `EqController` owns a chain of biquad bands and exposes both halves an EQ UI needs: the **audio** (an `input` and an `output` you splice into a graph) and the **geometry** (a sampled response curve, plus the four conversions between frequency/gain and normalised x/y). The conversions are exported separately because a presenter needs them before any audio exists — to draw the grid, place a handle, and turn a drag back into a band setting. `setContext()` exists because an EQ UI is built before a gesture has resumed an `AudioContext`. Until one is set, `context`, `input` and `output` are `undefined`; bands and coordinate conversions remain available, but `snapshot().response` is empty. Setting a context realizes the band chain; setting `undefined` releases it. Rebuilds are transactional: a failed construction leaves the previous graph and state usable. A successful `setContext()` or `setBands()` replacement changes the endpoint nodes, so a manual caller must reconnect its external route. ```ts import {DEFAULT_EQ_BANDS, createEqController, eqFrequencyToX} from '@webmusic/score/play/headless'; const eq = createEqController({bands: DEFAULT_EQ_BANDS}); const off = eq.subscribe(() => paint(eq.snapshot())); eq.setContext(context); // after a user gesture; realises the band chain source.connect(eq.input!); eq.output!.connect(context.destination); eq.setBand(0, {gain: -3}); const x = eqFrequencyToX(200); // geometry needs no audio at all // Retain this cleanup until the EQ's owner is removed. const dispose = () => { off(); source.disconnect(eq.input!); eq.dispose(); }; ```
    API ### Recipe factories The constructor is private. Every factory returns `Effect`; all options are optional. The option types are exported from the same Headless entry. | Member | Defaults and units | Realized `params` | | --- | --- | --- | | `Effect.gain(value = 1)` | Linear gain. | `gain` | | `Effect.panner(pan = 0)` | Stereo pan, clamped to −1…1. | `pan` | | `Effect.analyser(options?)` | `AnalyserOptions`: `fftSize` and `smoothing` use native node defaults when omitted; `onReady(node)` receives the live analyser during build. Caller polls it. | None | | `Effect.limiter(options?)` | `LimiterOptions`: `threshold: -3` dB, `release: 0.1` s. Fixed knee 0, ratio 20, attack 0.002 s. | `threshold`, `ratio` | | `Effect.bitcrusher(options?)` | `BitcrusherOptions.bits: 6`, clamped to 1–16; waveshaper amplitude quantization. | None | | `Effect.tremolo(options?)` | `TremoloOptions.frequency: 5` Hz, `depth: 0.5` clamped to 0–1. Gain modulation begins at build. | `rate`, `depth` | | `Effect.chorus(options?)` | `ChorusOptions.delaySeconds: 0.025`, `depth: 0.005` s, `frequency: 0.8` Hz, `wet: 0.5`; direct dry route has unity gain. | `rate`, `depth`, `wet` | | `Effect.filter(options?)` | `FilterOptions.type: 'lowpass'`, `frequency: 1000` Hz, `Q: 1`; optional `gain` in dB otherwise retains the native default. | `frequency`, `Q` | | `Effect.delay(options?)` | `DelayOptions.delaySeconds: 0.25`, `feedback: 0.3`, `wet: 0.3`, `dry: 1`; feedback/wet/dry are linear gains. | `time`, `feedback`, `wet` | | `Effect.distortion(options?)` | `DistortionOptions.amount: 0.4`, `oversample: '2x'`; waveshaper curve strength and native oversampling mode. | None | | `Effect.compressor(options?)` | `CompressorOptions`: optional `threshold`, `knee` (dB), `ratio`, `attack`, `release` (s). Omitted values retain native compressor defaults. | `threshold`, `ratio` | | `Effect.reverb(options?)` | `ReverbOptions.seconds: 1.8`, `decay: 2.2`, `wet: 0.28`, `dry: 0.82`; seconds/decay have a minimum of 0.1. Generates a stereo noise impulse. | `wet`, `dry` | | `Effect.custom(implementation)` | Factory `(context: AudioContext) => EffectNode`, or a fixed `EffectNode`. | Supplied by caller | | `Effect.chain(...effects)` | Series composition; skips `null`/`undefined`. An empty chain is unity gain. | No combined map for a nonempty chain | `params` lists the actual exported controls, not every underlying node property. For example, tremolo `depth` is its modulation gain parameter; changing it does not recompute the base gain chosen at construction. ### Realization, state, and helpers | Member | Kind | Description | | --- | --- | --- | | `label?: string` | state | Optional mutable display label for controls. | | `build(context: AudioContext): EffectNode` | command | Realizes a recipe into the supplied live context. | | `createAudioNodes(context: BaseAudioContext): EffectNode` | command | Structural cross-family/offline realization. | | `EffectNode` | type | `input`, `output`, optional `params: Record`, and optional `dispose()`. Alias of the kernel effect node contract. | | `insertEffect(context, source, destination, effect)` | helper | Inserts an optional effect, or a direct route when `effect` is `undefined`; returns `{dispose?: () => void}`. Cleanup removes the exact insertion edges and calls the bundle's disposer once. Construction rollback preserves the initiating error. | | `resolveEffect(effect, reverb)` | helper | Prefers a supplied effect. Otherwise turns truthy legacy `ReverbOptions` into `Effect.reverb`; `false`/`undefined` produce no effect. | | `createReverbNode(context, options?)` | helper | Returns a caller-owned `ConvolverNode` with a generated stereo impulse. Reads only `ReverbOptions.seconds` and `decay` with the defaults above; it does not create wet/dry mixing, so `wet`/`dry` have no effect here. Disconnect it when its route is removed. | A built chain attempts each child cleanup even if another child fails, then throws the first cleanup error. It releases chain-owned internal edges; callers still own external source/destination edges. The `Effect` recipe has no `dispose()` or `destroy()` method. ### EqController **Construction** — `new EqController(options?)` / `createEqController(options?)`: the borrowed `context` and the initial `bands` (`{frequency, gain?, q?}`; `DEFAULT_EQ_BANDS` when omitted). `EqBand` uses Hz, dB, and Q. Frequencies clamp to 30–18000 Hz; gain clamps to −18…18 dB and defaults to 0; Q defaults to 1 and is kept positive. The default bands are 120 Hz/Q 0.8, 1000 Hz/Q 0.9, and 6000 Hz/Q 0.9, all at 0 dB. An explicit empty band array gives a pass-through graph and empty response. | Member | Kind | Description | | --- | --- | --- | | `setContext(context)` | command | Realises the band chain, or releases it with `undefined`. The context is borrowed and never closed. | | `setBands(bands)` | command | Replaces the band set; `undefined` restores the defaults. | | `setBand(index, band)` | command | Patches one band in place; invalid/nonexistent indices are ignored. | | `snapshot()` | state | Fresh `EqControllerState`: copied bands, sampled response, and `ready`. Each `EqResponsePoint` has normalized logarithmic `x` and summed `gain` in dB. Up to 64 samples use the fixed 30–18,000 Hz axis; samples above the context's Nyquist frequency (`sampleRate / 2`) are omitted. Response is empty without filters. | | `context` · `input` · `output` | state | The borrowed context and the splice points, once a context exists. | | `subscribe(notify)` | subscription | Fires on state change, without an initial callback. Returns the unsubscribe. | | `dispose()` · `destroy()` | lifecycle | Final, idempotent cleanup; `destroy` is an alias. Disconnects owned nodes and clears subscribers; never closes the context. Subsequent mutation commands are inert. | | `eqFrequencyToX(frequency)` · `eqXToFrequency(x)` · `eqGainToY(gain)` · `eqYToGain(y)` | helper | Pure logarithmic-frequency/linear-gain coordinate conversions; normalized coordinates clamp to 0–1. | | `DEFAULT_EQ_BANDS` · `EQ_MIN_FREQUENCY` · `EQ_MAX_FREQUENCY` · `EQ_MAX_GAIN` · `EQ_RESPONSE_SAMPLES` | constant | The default band set and the drawable range (30–18000 Hz, ±18 dB, 64 samples). | `EqControllerOptions`, `EqControllerState`, `EqBand`, and `EqResponsePoint` are exported alongside the class. The EQ has no timer, transport, or named event emitter. Synchronous graph, parameter, subscriber, and cleanup failures can throw; disposal still marks the controller final before releasing its graph.
    ## Related [`ScorePlayer`](), [`InteractivePlayer`](), and [`Rack`]() consume effects. [``]() composes effects and EQ through [`SynthPanelAudioGraph`](). Use [`Sound`]() to choose the timbre before processing. --- # InteractivePlayer > The pull engine: notes sound as you send them, so a keyboard or a sequencer drives the timing. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/headless/play/interactive-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. `InteractivePlayer` owns **no musical clock** — it sounds only when *you* feed it a signal. Each `advance()` pulls and plays the next beat's notes, so a gesture, metronome, scroll, or `requestAnimationFrame` drives playback. It supports many voices and sources; source selection is synchronous and the next driven beat uses the selected source. Its trigger methods intentionally stay synchronous. Call `await player.preload()` before the first driven note: it resumes a suspended AudioContext and prepares SFZ, sample, or SoundFont voices. A synchronous trigger fails closed while the context is still suspended instead of queuing a note that could sound after an unrelated later resume. Supplied voices are borrowed by default; `synthOwnership: 'owned'` transfers their disposal to the player. Optional [clock input](#clock-input) and [Web MIDI input](#web-midi-input) helpers are documented below with the engine they drive. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/headless/play/interactive-player/) ## Import ```ts import {InteractivePlayer, Sound} from '@webmusic/score/play/headless'; const player = new InteractivePlayer(); player.addVoice('piano', Sound.oscillator(), {synthOwnership: 'owned'}); player.addSource('piece', score, {route: () => 'piano'}); await player.preload(); // resume and prepare audio from a user gesture player.advance({secondsPerBeat: 0.5}); // Stop the caller's input/clock bindings before teardown. player.dispose(); ``` The source contract is `InteractivePlayer`, `InteractivePlayerOptions`, `AddVoiceOptions`, `AddSourceOptions`, `AdvanceOptions`, `BeatNote`, `NoteInput`, `VoiceResolver`, `SourceRoute` and `InteractivePlayerEvents` at `@webmusic/score/play/headless`. The [Play API]() covers stateless recording and score conversion. ## Time and resource roles Sources borrow immutable scores and index them on a written beat grid. `beatUnitQuarters` chooses that grid; `secondsPerBeat` from a host controls the performed duration of a driven beat. A `BeatNote` retains both written `durationBeats`/`onsetInBeat` and effective `durationSeconds`, including performed timing and merged ties. The player does not advance from elapsed time by itself. Audio attacks use the context clock plus `lookaheadSeconds`. Automatic releases also wait for that audio clock; suspension does not consume a note's gate. Explicit releases remain immediate. Each occurrence owns its release, so a note started inside release feedback keeps its own timer. The player owns its graph, voice routes, source indexes and recording state. An injected context or destination is borrowed; supplying both requires matching context identities. Without either, it creates and closes its own context. Each supplied voice synth is borrowed unless its `AddVoiceOptions` transfer ownership. A borrowed synth receives only its returned route cleanup on removal/disposal, not a global disconnect or disposal.
    API **Construction and options** | Member | Default / return | Contract | | --- | --- | --- | | `new InteractivePlayer(options?)` · `createInteractivePlayer(options?)` | `InteractivePlayer` | Constructs without starting a clock. | | `InteractivePlayer.fromUrls(sources, options?)` | `Promise` | Loads a record of named score URLs; registers them in key order with the first active. Add voices before playing. | | `audioContext` · `destination` | Internally created context / its destination | Borrowed graph context and output route. | | `effect` · `reverb` | Dry output | Explicit effect wins over legacy reverb settings; see [Effect](). | | `lookaheadSeconds` | `0.03` | Offset from context time for a triggered attack. | | `masterVolume` | `1` | Initial linear master gain. | | `resolveVoice(note, sourceId)` | None | `VoiceResolver` chooses a voice id or returns `undefined`. | | `defaultVoice` | First registered voice | Fallback voice id when no note/source/resolver route applies. | | `AddVoiceOptions.volume` · `muted` | `1` / `false` | Initial per-voice mix. | | `AddVoiceOptions.effect` · `synthOwnership` | Dry / `'borrowed'` | Per-voice processing and optional `'owned'` backend teardown. | | `AddSourceOptions.beatUnitQuarters` | `1` | Quarter notes per driven beat. | | `AddSourceOptions.route` | None | `SourceRoute`: part-name to voice-id record, or `(partName) => voiceId \| undefined`. | **Source and voice commands** | Member | Return / contract | | --- | --- | | `addVoice(id, synth, options?)` · `removeVoice(id)` | `this`; register or remove a voice and its route. | | `hasVoice(id)` · `listVoices()` | `boolean` / `string[]`. | | `setVoiceVolume(id, volume)` · `muteVoice(id, muted = true)` · `soloVoice(id, solo = true)` | `this`; change per-voice mix. | | `setMasterVolume(volume)` · `setMix(record)` | `this`; master gain or a record of voice gains. | | `addSource(id, score, options?)` · `removeSource(id)` | `this`; register or remove indexed score material. | | `addSourceFromUrl(id, url, options?)` | `Promise`; source options additionally accept `format`. | | `listSources()` | Registered source ids. | | `select(id, {carry?}?)` | `this`; carries proportional cursor position by default; `carry: false` rewinds the selected source. | | `seekBeat(beat)` · `rewind()` | `this`; wrap a beat index within the source, or reset to beat zero. | | `peek()` | `BeatNote[]` for the next beat without sounding it. | A same-id `addSource()` replaces the indexed material and resets that source's cursor to zero. If it was selected, the replacement stays selected. For one id, a newer URL load, synchronous replacement or removal invalidates and aborts the previous request. Superseded loads may resolve without installing anything; current failures reject. Loads for different ids remain independent. Removing the selected source selects the first remaining registration, if any. **Playback and recording commands** | Member | Return / contract | | --- | --- | | `preload()` | `Promise`; resumes audio and awaits voice readiness. | | `advance({secondsPerBeat?}?)` | `BeatNote[]`; sounds the current beat and advances, wrapping at the end. | | `advanceHold()` | `BeatNote[]`; advances with a sustained gate; call `allNotesOff()` to release it. | | `playBeat(beat, {secondsPerBeat?, moveCursor?}?)` | `BeatNote[]`; plays a wrapped beat index; moves the cursor after that beat unless `moveCursor: false`. | | `play(notes)` | `this`; sounds a `NoteInput[]` independently of score sources. | | `noteOn(voice, midi, velocity = 100, durationSeconds = 0.3)` | `this`; one directly triggered note. | | `noteOff(voice, midi)` · `allNotesOff()` | `this`; release a voice/pitch or every active note. | | `record()` | `this`; starts a new take of notes this engine fires. | | `stopRecording(options?)` | `Score`; ends that take. Options are `bpm` (120), `ppq` (480), `quantizeGrid` (fraction of a quarter; absent means unsnapped), and `title` (`'Recording'`). | `NoteInput` requires `midi`; optional `velocity` defaults to `100`, `durationSeconds` to `0.3`, `timeOffsetSeconds` to `0`, and `voice` to the configured fallback. `BeatNote` supplies `midi`, `velocity`, `durationBeats`, `durationSeconds`, `onsetInBeat`, `part`, and optional `voice`. **State** | Member | Contract | | --- | --- | | `context` | Lazy AudioContext getter; may construct the graph. | | `activeSourceId` | Selected source id or `undefined`. | | `position` | `{beat, total, progress}` on the selected written beat grid. | | `isRecording` | Whether the engine's performance recorder is armed. | **Events — `on(name, listener)` returns an unsubscribe function** | Event | Payload | | --- | --- | | `beat` | `{source, beat, total, notes: BeatNote[]}`. | | `sourceChange` | `{source, beat, total}`. | | `wrap` | `{source}` after `advance()` / `advanceHold()` wraps. | | `noteOn` | `{voice, midi, velocity, time}`; `time` is AudioContext seconds. | | `noteOff` | `{voice, midi, time}`. | | `operationError` | Contained operation failure: `kind`, `source`, `operation`, `error`. | | `listenerError` | Subscriber failure: `kind`, `source`, `event`, `mode`, `error`. | **Lifecycle** `dispose(): void` is idempotent. It aborts outstanding instance URL loads, releases voices/routes and owned resources, and clears source/recording state. The caller owns external MIDI, clock, DOM and framework subscriptions; unbind those before disposing. There is no built-in `play()` transport toggle, `pause()`, `stop()` or advancing timer: `play(notes)` is a note command. ### ScoreRecorderSession `ScoreRecorderSession` is the capture direction: live note events in — from [``](), a MIDI device, this very player — and the recorded notes back out when you finish. `recordedNotesToScore()` is the second half, turning that flat list into a real `Score`. It is the model behind [``](). It owns no audio and no clock: you tell it when a note goes on or off, and it timestamps with the clock you gave the constructor — `recorderNow()`, a monotonic seconds source, by default. Passing your own clock is what makes a session testable, and what lets a recording share a timebase with a player instead of drifting against it. Same-pitch presses are queued: each release completes the earliest unmatched attack, preserving its original onset and velocity. Durations have a minimum of 0.02 seconds. `finish()` returns only completed notes and drops unmatched presses. When changing an input source during a take, call `discardPending()` to drop those presses while retaining completed notes and the original start time; the session remains armed. Notation conversion preserves each note's performed seconds and velocity, while `bpm` and `quantize` determine its notated quarters. Each distinct voice gets a separate part, including an unnamed voice alongside a voice named `part`; fallback identifiers cannot merge those voices. ```ts import {ScoreRecorderSession, recordedNotesToScore} from '@webmusic/score/play/headless'; const session = new ScoreRecorderSession(); session.start(); session.capture(60, 100, true); // press session.capture(60, 0, false); // release completes the recorded note const notes = session.finish(); const score = notes ? recordedNotesToScore(notes, {bpm: 100, quantize: 0.25}) : undefined; ``` **Construction** — `new ScoreRecorderSession(clock?)`; `clock` defaults to `recorderNow`. | Member | Kind | Description | | --- | --- | --- | | `start()` | command | Begins a take, discarding any previous one. | | `capture(midi, velocity, on)` | command | Queues a press or completes the earliest unmatched press of that pitch; returns whether it completed a recorded note. | | `discardPending()` | command | Discards unmatched presses without changing completed notes, the recording start time or active state. | | `finish()` | command | Ends the take, drops unmatched presses, and returns completed `RecordedNote[]`, or `null` when the session was not armed. | | `cancel()` | command | Ends it and discards. | | `active` · `noteCount` | state | Whether a take is running, and the notes captured so far. | | `recordedNotesToScore(notes, {bpm, quantize, title?})` | helper | Assembles captured notes into a `Score`; `quantize` is a fraction of a quarter (`0.25` means sixteenths, `0` disables snapping). Returns `undefined` for an empty list. | | `recorderNow()` | helper | The default monotonic seconds clock, SSR-safe. | | `SCORE_RECORDER_VOICE` | constant | The voice id captured notes are written to (`'rec'`). | The recorder retains one press per MIDI pitch: another press of the same pitch replaces its pending onset. A completed note stores `midi`, `velocity`, `voice`, `onsetSec` and `durationSec`; release duration has a 0.02-second minimum. `noteCount` counts completed notes, not currently held presses. `ScoreRecorderNotationOptions` requires `bpm` and `quantize`; `title` defaults to `'Recording'`. `ScoreRecorderRecordedDetail` is the element's `{score: Score}` payload type, not an event emitter on this session. There is no subscription, audio resource or `dispose()` method on the session; cancel unfinished takes and detach the caller's input listeners. ### Clock input The `bindClock` helper advances an `InteractivePlayer` one written beat at a time. It borrows the player and owns its tick source until cleanup. #### Clock input Import ```ts import {bindClock, InteractivePlayer, Sound} from '@webmusic/score/play/headless'; const player = new InteractivePlayer(); player.addVoice('main', Sound.oscillator(), {synthOwnership: 'owned'}); player.addSource('piece', score); await player.preload(); const unbind = bindClock(player, {bpm: 100}); // starts immediately unbind(); // stop external beats first player.dispose(); ``` The source contract is `bindClock(player: InteractivePlayer, options?: ClockOptions): () => void`. It borrows the player and owns a `Metronome`/tick source until cleanup. The tick callback supplies `secondsPerBeat = 60 / bpm` to `player.advance()`. It neither creates a player nor prepares/resumes audio on its behalf. **Input and options** | Member | Default / contract | | --- | --- | | `player` | Required borrowed `InteractivePlayer`, ready for synchronous `advance()`. | | `ClockOptions.bpm` | `120`; finite beats per minute, effective minimum `1`. | | `ClockOptions.createTickSource(intervalMs)` | Kernel tick-source factory, worker-backed where available. The returned source is owned by this binding. | **Lifecycle and notifications** `bindClock()` starts immediately and returns an idempotent unbind function that stops and disposes its metronome's current tick source. It does not dispose the player or release already sounding notes. Call `player.allNotesOff()` if stopping the binding must silence them immediately. There is no binding state getter or event emitter; observe `player.on('beat', ...)`. Use `Metronome` directly if a caller needs live `setBpm()` or reusable start/stop control. ### Web MIDI input `bindMidiInput` turns MIDI note-on or clock messages into `InteractivePlayer.advance()` calls. In its default mode, a keyboard press plays the next beat of the selected score, regardless of the pressed pitch. An `onNote` callback can replace that action with a custom response. `MidiPlayerController` is an alias of [`PlayerController`](). It wraps a `ScorePlayer`; its name does not mean it requests MIDI access, subscribes to MIDI transport messages, or sends MIDI output. For output timbres, use [`Sound.midiOut()`](). #### MIDI input Import ```ts import {bindMidiInput, InteractivePlayer, Sound} from '@webmusic/score/play/headless'; const player = new InteractivePlayer(); player.addVoice('main', Sound.oscillator(), {synthOwnership: 'owned'}); player.addSource('piece', score); await player.preload(); const access = await navigator.requestMIDIAccess(); const input = [...access.inputs.values()][0]; if (!input) throw new Error('Connect a MIDI input first.'); const unbind = bindMidiInput(player, {input}); unbind(); player.dispose(); ``` The source contract is `bindMidiInput`, `MidiInputOptions` and `MidiInputLike` at `@webmusic/score/play/headless`. The helper borrows both the player and device. It owns no clock; incoming messages drive beats. Prepare the player's audio separately, from a user gesture, before allowing input. Automatic device lookup has no readiness/error event; request access yourself and pass `input` when the UI must report permission or device errors. **Input and options — `bindMidiInput(player, options?): () => void`** | Member | Default / contract | | --- | --- | | `player` | Required borrowed `InteractivePlayer`. | | `input` | Requests the first available MIDI input when omitted. An explicit `MidiInputLike` also supports injected/test inputs. | | `mode` | `'note'`; accepts `'clock'`. | | `pulsesPerBeat` | `24`; in clock mode each group of this many `0xF8` pulses calls `advance()`. Supply a positive count. | | `onNote(midi, velocity)` | Optional callback in note mode; replaces the default `advance()` call. | `MidiInputLike` requires an assignable `onmidimessage` callback property (or `null`), with an event `{data: Uint8Array | number[]}`. Binding replaces that property's previous callback; it is not a multiplexing subscription. **Message behavior** | Mode / message | Action | | --- | --- | | `'note'`: channel note-on `0x90…0x9F` with velocity greater than zero | Call `onNote(midi, velocity)` if supplied, otherwise advance one score beat. | | `'note'`: note-off or zero-velocity note-on | Ignored; this helper does not pair held notes. | | `'clock'`: `0xF8` | Count pulses and advance at `pulsesPerBeat`. | | `'clock'`: start `0xFA` or stop `0xFC` | Reset the pulse counter only. No automatic player play/pause command. | **Lifecycle** The returned unbind function is idempotent. It disables retained callbacks and restores the previous `onmidimessage` handler only while this binding still owns that property. A newer application handler remains intact. Cleanup also prevents a pending automatic device request from attaching a listener. It does not close the device or dispose/silence the player. There are no helper state getters or emitted events. Without Web MIDI and without an explicit input, the helper warns and returns a harmless cleanup; automatic access rejection is swallowed. `MidiPlayerController` has the construction, commands, state, events and `destroy()` lifecycle documented on the [PlayerController page](). #### MIDI input Related - [InteractivePlayer]() — note commands and source beats. - [PlayerController]() — the exact `MidiPlayerController` alias contract. - [Sound]() — MIDI output backend. - [Live trackers]() — analyze actual MIDI pitches from custom input handling.
    ## Related - [ScorePlayer]() — autonomous score scheduling. - [Transport drivers]() and [Web MIDI]() — external beat sources. - [Note input and recorder elements]() — browser composition. - [Sound]() — voice recipes and ownership. --- # LoopPlayer > A ready-made A–B loop over a score region. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/headless/play/loop-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. `LoopPlayer` wraps a `ScorePlayer` with an A–B loop region: set the in/out points, optionally repeat a fixed number of times, then play through. Drag the handles and toggle the loop. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/headless/play/loop-player/) ## Import ```ts import {LoopPlayer} from '@webmusic/score/play/headless'; const loop = new LoopPlayer(score, {loopIn: 0.25, loopOut: 0.75, loopCount: 4}); const off = loop.player.on('timeupdate', ({progress}) => paint(progress)); await loop.play(); // Keep the wrapper for the interaction lifetime; call this on teardown. const dispose = () => { off(); loop.dispose(); }; ``` `createLoopPlayer(score, options?)` returns the same wrapper. `play()` resolves when the underlying start operation completes, not when the score ends. ## Time and ownership The wrapper owns a newly constructed [`ScorePlayer`]() and two subscriptions. It converts fractional region endpoints to the player's transport seconds. It does not create another session clock. Finite repeat counting observes backward progress jumps; it is not a separate sample-accurate loop-boundary notification. Explicit wrapper seeks and loop-region edits update the counting baseline; their playhead jumps do not consume a repeat. After the count is reached, the underlying loop is cleared and playback continues to the end. The wrapper's `loop` property remains the requested armed state, and the end handler seeks to zero and re-arms it for the next play. A supplied synth follows ScorePlayer's default borrowed ownership. This wrapper does not expose `synthOwnership`, context injection, or the complete ScorePlayer options; retain the supplied synth's cleanup path. Use ScorePlayer directly when those construction controls are required.
    API **Construction and LoopPlayerOptions** | Member | Default | Contract | | --- | --- | --- | | `new LoopPlayer(score, options?)` | — | Creates the owned player and arms the requested region. | | `createLoopPlayer(score, options?)` | — | Returns a `LoopPlayer`. | | `synth` | Default player synth | Borrowed backend passed to the player. | | `effect` | None | Effect recipe passed to the player. | | `loopIn` | `0.25` | Initial fractional start, clamped to 0–1. Supply ordered finite endpoints. | | `loopOut` | `0.75` | Initial fractional end, clamped to 0–1. | | `loop` | `true` | Requested loop-enabled state. | | `loopCount` | `Infinity` | Observed wraps before clearing the underlying loop. The wrapper does not validate this as a positive integer. | **Commands and state** | Member | Kind | Contract | | --- | --- | --- | | `play()` | `Promise` | Starts/resumes the owned player, forwarding start rejection. | | `pause()` | `void` | Pauses the player. | | `seek(seconds)` | `void` | Delegates transport-seconds seek and suppresses the wrapper's wrap counter during the synchronous seek call. It does not return the player's seek Promise. | | `seekFraction(fraction)` | `void` | Clamps to 0–1 and seeks using the current duration. | | `setLoopRegion(inFraction, outFraction)` | `void` | Clamps and orders both endpoints, then applies the region. A resulting playhead jump does not count as a loop wrap. | | `player` | Readonly `ScorePlayer` | Underlying player for events and additional operations. | | `duration` | Readonly number | Player duration in transport seconds. | | `loopIn` / `loopOut` | Read/write number | Setting either uses `setLoopRegion`; reading returns the stored fraction. | | `loop` | Read/write boolean | Setting resets the repeat counter and applies or clears the loop. | | `loopCount` | Read/write number | Setting resets the counter. | | `dispose()` | `void` | Removes wrapper subscriptions and disposes the owned player; repeated cleanup is supported. | **Events and boundaries** LoopPlayer has no emitter or `on()` method of its own. Subscribe through `player` using the ScorePlayer event contract. It exposes no wrapper-level `stop()` or rate setter. Numeric clamping does not reject `NaN`; use finite fractional inputs and a positive repeat count or `Infinity`. There is no supported post-disposal playback lifetime.
    ## Related [ScorePlayer]() owns scheduling and the event contract. [Transport presenter]() can display caller-bound state. The programmatic demo above composes the wrapper with UI; there is no current `` public tag. --- # Metronome > A beat clock with tap-tempo, independent of any score. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/headless/play/metronome/ 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. `Metronome` is a beat callback driven by the kernel tick source, using a worker where available and a timer fallback otherwise. Driving an `InteractivePlayer` with it (`bindClock(player)`) gives the pull engine an external beat clock; here it just ticks and flashes on each beat. Its cadence is a rounded millisecond timer interval, not sample-accurate audio scheduling. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/headless/play/metronome/) ## Import ```ts import {Metronome} from '@webmusic/score/play/headless'; const clock = new Metronome((secondsPerBeat) => flashBeat(secondsPerBeat), {bpm: 120}); clock.start(); clock.setBpm(100); clock.stop(); // releases the current tick source; start() can create another ``` The source contract is `Metronome` and `ClockOptions` at `@webmusic/score/play/headless`. It owns the tick source created by its factory; the callback and anything that callback drives remain caller-owned. There is no audio context, score, UI, event emitter or `dispose()` method.
    API **Construction** — `new Metronome(onTick, options?)`. | Input / option | Default | Contract | | --- | --- | --- | | `onTick(secondsPerBeat)` | Required | Callback on each tick; argument is `60 / bpm` in seconds. | | `ClockOptions.bpm` | `120` | Beats per minute, with an effective minimum of `1`; supply a finite number. | | `ClockOptions.createTickSource(intervalMs)` | Kernel tick-source factory | Receives the rounded beat interval in milliseconds. The metronome owns/disposes each returned source. | **Commands and state** | Member | Return / contract | | --- | --- | | `start()` | `this`; starts ticking, or does nothing if already running. | | `stop()` | `this`; stops and disposes the current tick source. Repeat calls are safe. | | `setBpm(bpm)` | `this`; changes BPM and replaces a running tick source at the new cadence. | | `bpm` | Current BPM. | | `running` | Whether a tick source is installed. | **Lifecycle** — call `stop()` on unmount. It can later restart; it does not release anything owned by `onTick`. For a clock bound to a player, use the cleanup returned by [bindClock]().
    ## TempoController `TempoController` is the tap side of tempo, DOM-free: press timings in, a BPM and — in conduct mode — an advance decision out. It owns no clock and no audio; pair it with an [`InteractivePlayer`]() as its `target` when a press should sound the next beat. ```ts import {TempoController} from '@webmusic/score/play/headless'; const tempo = new TempoController({mode: 'tap'}); const off = tempo.subscribe(() => paint(tempo.snapshot())); button.onpointerdown = () => tempo.press(); button.onpointerup = () => tempo.release(); off(); tempo.dispose(); ``` Taps accumulate in a rolling window of `TAP_WINDOW` (6) intervals, up to 7 presses, and a gap longer than `TAP_RESET_GAP` (2) seconds starts a fresh series rather than averaging across a pause — the alternative is a tempo that drifts toward wrong for as long as anyone remembers tapping. `tapBpm(taps)` is that inference on its own and `appendTap(taps, time, resetGap?, windowSize?)` is the windowing, both exported so a caller can hold the tap array itself.
    TempoController API **Construction — `new TempoController(options?)`** | `TempoControllerOptions` member | Default / contract | | --- | --- | | `mode` | `'tap'`; `TempoMode` also accepts `'conduct'`. | | `conduct` | `'hold'`; `TempoConductMode` also accepts `'full'`. | | `target` | Borrowed `AdvanceTarget`: required `advance(options?)`, optional `advanceHold()` and `allNotesOff()`. | | `now` | Monotonic seconds callback; injected clocks also return seconds. | | `resetGap` | `TAP_RESET_GAP` (`2` seconds); minimum zero. | | `windowSize` | `TAP_WINDOW` (`6` intervals); at least one integer interval. | `TempoControllerConfig` can change `mode`, `conduct` and `target` through `configure()`. It does not replace the time source or tap-window options. | Member | Kind | Description | | --- | --- | --- | | `press(atSeconds?)` | command | Registers a press; returns the `TempoPressResult` it implies, or `undefined` for an ignored press. | | `release()` | command | Ends the press, for conduct-hold mode. | | `configure(config)` | command | Changes mode, conduct style or target live; a held note is released before observers see the change. | | `snapshot()` | state | `TempoControllerState` — `mode`, `conduct`, inferred `bpm` (`null` before enough valid taps), `holding`, accepted-press `activation` count, optional conduct `secondsPerBeat` (capped at 2 s). | | `subscribe(notify)` | subscription | Fires on state change. Returns the unsubscribe. | | `dispose()` | lifecycle | Idempotent. Releases held sound and makes the controller inert. | | `tapBpm(taps)` · `appendTap(taps, time, …)` | helper | The inference and the windowing as pure functions. | | `TAP_WINDOW` · `TAP_RESET_GAP` | constant | The window (6 intervals / at most 7 timestamps) and reset gap (2 s). |
    `TempoPressResult` is `{type: 'tap', bpm: number | null}` or `{type: 'beat', secondsPerBeat?: number}`; an ignored press returns `undefined`. Tap mode measures tempo without playing. Conduct mode calls the borrowed target; hold mode uses `advanceHold()` when available and releases through `allNotesOff()`. `subscribe()` is a change notification without a payload or immediate initial call; read `snapshot()` when mounting. The pure `appendTap()` helper returns a new timestamp array and `tapBpm()` returns rounded BPM or `null`; neither retains resources. ## Related - [Transport drivers]() — `bindClock` cleanup. - [InteractivePlayer]() — conducted beats. - [AbPlayer]() — arrangement switching on an owned metronome. --- # PlayerController > An event-driven transport controller over a ScorePlayer. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/headless/play/player-controller/ 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. `PlayerController` wraps a push `Player` in a small event-driven API (play / pause / seek / scrub + `timeupdate` / `transportchange` / `end`), handy for binding to your own transport buttons. `transportchange` fires synchronously when code calls the controller's play/pause commands, so presenters do not need to poll. Its `error` event is an operational `play()` failure; `listenerError` independently reports a subscriber that threw or rejected, so application bugs never masquerade as engine failures. `MidiPlayerController` is an alias of this class; it does not install MIDI listeners. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/headless/play/player-controller/) ## Import ```ts import {PlayerController, ScorePlayer} from '@webmusic/score/play/headless'; const player = new ScorePlayer(score); const transport = new PlayerController(player, score); const off = transport.on('transportchange', ({playing}) => paintPlaying(playing)); transport.play(); // void; operational play failures arrive through 'error' off(); transport.destroy(); player.dispose(); ``` The source contract is `PlayerController`, `ControllerEvents`, `ControllerEvent` and `PlayerTransportState` at `@webmusic/score/play/headless`. The constructor specifically takes a `ScorePlayer` (also exported as `Player`) and its `Score`. It borrows both and owns only event subscriptions and controller state. It creates no musical clock. All numeric seek/current/duration values below use the player's rate-scaled transport seconds; `currentPosition` is musical time. Use the controller as the UI's command path. Its `playing` and `rate` are controller state, not a general observer of calls made directly on the borrowed player. `playing` becomes true when `play()` requests startup, before asynchronous audio preparation completes.
    API **Construction** — `new PlayerController(player: ScorePlayer, score: Score)`; no options. `new MidiPlayerController(player, score)` is the same constructor. **Commands** | Member | Return / contract | | --- | --- | | `play()` | `void`; emits optimistic transport state, starts the player, and reports a failed start through `error`. | | `pause()` | `void`; clears controller playing state and pauses the player. | | `stop()` | `void`; pauses and seeks to zero unless a synchronous callback issues a newer play, pause, seek or destroy command. | | `seek(seconds)` · `scrub(deltaSeconds)` · `seekFraction(fraction)` | `void`; clamp/forward seeks in transport seconds or normalized fraction. Use the player directly when an awaitable seek is required. | | `setRate(rate)` | `void`; clamps finite values to `0.1…4` and applies that scale to the supplied score's initial BPM. Non-finite input throws `RangeError`; a failed tempo write restores the previous controller rate unless a newer rate command has already superseded it. | **State** | Member | Contract | | --- | --- | | `currentTime` · `duration` | Current and total transport seconds. | | `currentPosition` | Player's musical `TimePosition`. | | `progress` | Player's normalized position. | | `playing` | Controller's requested transport state, corrected on end or play failure. | | `rate` | Controller rate, initially `1`. | | `loaded` | Whether player duration is greater than zero; this does not mean samples have preloaded. | **Events — `on(name, callback)` returns an unsubscribe function** | Event | Payload | | --- | --- | | `cursor` | `TimePosition`. | | `noteOn` · `noteOff` | Score `Note`. | | `end` | Completed `Score`. | | `timeupdate` | The player's `PlayerTimeUpdate`, including explicit nominal/transport fields. | | `transportchange` | `PlayerTransportState`: `{playing: boolean}`. | | `error` | `unknown` operational play error. | | `listenerError` | Subscriber fault: `kind`, `source`, `event`, `mode`, `error`. | **Lifecycle** `destroy(): void` detaches subscriptions and clears listeners/controller playing state. It does not pause or dispose the borrowed player. Dispose or pause the player separately according to its owner; do not keep using a destroyed controller as an active view binding.
    ## Related - [ScorePlayer]() — owned clock, time fields and audio lifecycle. - [Web MIDI]() — actual MIDI input binding. - [Rack]() — ensemble transport controller. --- # Rack > Many instruments under one transport, so an ensemble starts, stops and seeks as a unit. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/headless/play/rack/ 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. A `Rack` composes single-instrument players sharing an `AudioContext` and a master bus, with per-member volume / mute / solo. Timeline transport commands fan out to members; interactive members advance from caller input. Member players retain separate clocks. Rack does not inject one shared clock or pass one common future start timestamp to all timeline members. `rack.rename(oldId, newId)` changes a member's public name in place. Its player, route, order, volume, mute and solo state are preserved; renaming onto an existing id is rejected. `await rack.preload()` warms all members. `rack.play()` performs the same work automatically for timeline members; a caller driving interactive members should preload before its first `advance()`. Rack-supplied sounds are borrowed unless their `soundOwnership` is explicitly `'owned'`. Internally, member construction, mix-state calculation, and AudioContext adaptation are separate from transport orchestration. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/headless/play/rack/) ## Import ```ts import {Rack} from '@webmusic/score/play/headless'; const rack = new Rack({masterVolume: 0.8}); rack.add({id: 'melody', score: melody}); rack.add({id: 'bass', score: bass, volume: 0.6}); await rack.play(); // Keep it while the ensemble is in use; release during session teardown. const dispose = () => rack.dispose(); ``` `createRack(options?)` is the factory equivalent. Start completion is separate from musical completion; listen for `end` when the timeline members finish. Construction is lazy, but reading `context` realizes the graph. Preloading and playback may need a user gesture to resume the context. ## Reusable effects on the master bus `Rack` accepts the same lazy `Effect` recipes as `ScorePlayer` and `InteractivePlayer`. A rack-level chain runs once after all member channels are summed. `rack.setEffect(next)` and the equivalent `rack.effect = next` property replace that live master route without rebuilding members, changing their mix state, or resetting transport position. If the replacement cannot be built, the previous route stays active. Reassigning the same recipe is a no-op. A different custom recipe returning the exact active `EffectNode` bundle keeps the existing route and cleanup owner. Other custom replacements must return fresh input/output nodes; distinct bundles sharing a current endpoint are rejected before Rack changes its connections. The caller retains cleanup responsibility for such a rejected overlapping bundle, because its disposer may also destroy the active graph. Effect changes during graph construction are rejected, including changes from the initial lazy builder. ```ts import {Effect, Rack} from '@webmusic/score/play/headless'; const room = Effect.chain( Effect.compressor({threshold: -20, ratio: 4}), Effect.reverb({wet: 0.2}), ); const rack = new Rack({effect: room}); rack.setEffect(Effect.chain(Effect.filter({frequency: 4_000}), room)); rack.effect = undefined; // dry summed output const dispose = () => rack.dispose(); // retain until this route is no longer used ``` This is separate from `rack.add({effect})`: a member effect colours only that instrument before its channel gain, while `rack.effect` processes the summed master output. ## RackTransportController `RackTransportController` gives a rack the same event-driven transport surface [`PlayerController`]() gives a single player: a snapshot, a progress clock, and events when transport state changes — so a UI drives the ensemble without reaching into its members. It owns the play-generation race, the longest-member clock and the lockstep seek policy. The rack is **borrowed**. `destroy()` releases polling and subscriptions; if the controller still considers playback active, it also stops the rack. It never disposes the rack or its members. Its longest-member readout is a projection of a member's position, not another musical scheduling authority. ```ts import {Rack, createRackTransportController} from '@webmusic/score/play/headless'; const rack = new Rack(); const transport = createRackTransportController(rack); const off = transport.on('transportchange', ({playing, progress}) => paint(playing, progress)); await transport.play(); transport.seekFraction(0.5); off(); transport.destroy(); rack.dispose(); ```
    API **Rack construction and options** | Member | Default | Contract | | --- | --- | --- | | `new Rack(options?)` / `createRack(options?)` | — | Creates a lazy coordinator with a snapshot of `RackOptions`. | | `audioContext` | Created lazily if needed | Borrowed when supplied. | | `destination` | Context destination | Borrowed target; its context is used if no explicit context is provided. Supplying mismatched contexts fails graph construction. | | `masterVolume` | `1` | Master linear gain; use values from 0–1. | | `effect` | None | Lazy recipe for the summed master output. | **RackAddOptions and RackMember** | Input | Default | Contract | | --- | --- | --- | | `score` | Required | Score for the newly constructed player. Rack does not accept an existing player in this option shape. | | `id` | Generated `player-N` | Public member key. Adding the same key replaces its member. | | `mode` | `'timeline'` | `RackMemberMode`: timeline uses ScorePlayer; interactive uses InteractivePlayer. | | `sound` | Default oscillator | Supplied HeadlessSynth or a default backend. | | `soundOwnership` | `'borrowed'` for supplied sound | `'owned'` transfers release responsibility to the member player. Default-created sound is owned. | | `effect` | None | Member processing before its channel enters the master bus. | | `volume` | `1` | Member linear level. | | `tempo` | Score tempo | Forwarded to timeline players. | | `beatUnitQuarters` | InteractivePlayer default | Forwarded to the interactive source's beat grouping. | Returned `RackMember` values are readonly snapshots of `id`, `mode`, `player`, `volume`, `muted`, and `solo`. A snapshot captured before graph construction has `player: undefined`; retaining it does not make that field update later. Call `get()` or `list()` again after construction and on `memberschange` / `mixchange`. Old snapshots can retain old identifiers, mix values, or released player references; do not use them as a second authoritative mixer state store. **Rack commands and state** | Member | Returns | Contract | | --- | --- | --- | | `add(options)` | `RackMember` | Registers a member; builds immediately if the graph already exists. Replacement disposes the old member. | | `remove(id)` | `this` | Removes and disposes that member, then recalculates remaining solo gates; removing the final solo member restores other unmuted channels. Unknown keys are ignored. | | `rename(oldId, newId)` | `this` | Preserves player, mix state, and order. Missing/occupied keys or rename during graph construction throw. | | `get(id)` | `RackMember \| undefined` | Fresh snapshot for one member. | | `list()` | `RackMember[]` | Fresh snapshots in insertion order. | | `preload()` | `Promise` | Builds/prepares members and ensures the context is running; retries when membership changes during preparation. | | `play()` | `Promise` | Prepares members, starts timeline players, and rolls back partial/canceled starts. Concurrent starts coalesce. Does not start an interactive progression. | | `pause()` / `stop()` | `this` | Cancel pending starts and pause or stop timeline members. | | `seek(seconds)` | `this` | Seeks timeline members in their transport seconds; does not await their returned work. | | `advance(options?)` | `Record` | Drives interactive members using `AdvanceOptions`; results keyed by member id. | | `setVolume(id, volume)` | `this` | Clamps the member level to 0–1 and emits mix change. | | `mute(id, muted?)` | `this` | Sets the member mute flag; default true. | | `solo(id, solo?)` | `this` | Sets the member solo flag; default true. Multiple members may be soloed. | | `setMasterVolume(volume)` | `this` | Clamps and sets master level. | | `setEffect(effect)` | `this` | Replaces the master route transactionally; pass `undefined` explicitly for dry output. Exact active-bundle reuse retains its route; distinct overlapping bundles and changes during graph construction throw. | | `context` | `AudioContext` | Getter that realizes the lazy graph; fails after disposal. | | `masterVolume` | number | Readonly master level. | | `effect` | `Effect \| undefined` | Read/write master recipe; setter delegates to `setEffect`. | | `on(event, listener)` | Unsubscribe | Subscribe to `RackEvents`. | | `dispose()` | `void` | Idempotently cancels pending work, disposes member players/routes, and requests closure of an owned context. Borrowed contexts and sounds remain caller-owned. | Graph construction failures roll back acquired resources. Master-effect build failure preserves the old route; later cleanup failures use `operationError`. Member replacement releases the old member first. If the new member cannot be built, that id remains absent and the remaining solo gates are recalculated. Do not reuse a disposed Rack as a new session. Use finite mix/transport inputs; numeric clamping is not a general invalid-input validator. **RackEvents** | Event | Payload | Meaning | | --- | --- | --- | | `memberschange` | void | Add, remove, or rename changes membership. | | `mixchange` | void | Member/master gain, mute, or solo changes. | | `memberEnd` | `{id: string}` | A timeline member ends. | | `end` | void | The timeline members complete. | | `operationError` | `PlaybackOperationError` | Member/graph/cleanup failure with source, operation and error. | | `listenerError` | `PlaybackListenerError` | Subscriber failure with source, event, mode and error. | **RackTransportController** **Construction** — `new RackTransportController(rack, options?)` / `createRackTransportController(rack, options?)`. The options override the animation clock (`requestFrame` / `cancelFrame`) in tests and non-window runtimes. | Member | Kind | Description | | --- | --- | --- | | `play()` | `Promise` | Sets its active intent, invokes Rack.play, and starts state polling after successful preparation/start. Reverts state and rejects on failure. | | `pause()` | command | Pauses only when this controller's playback flag is active. | | `stop()` | command | Forwards stop and publishes the resulting time state unless destroyed. | | `seekFraction(fraction)` | command | Fractional position across the whole rack, in lockstep. | | `seek(seconds)` | command | Clamps against the longest member, then seeks each timeline member no later than its own end. Individual seek failures emit `error`. | | `snapshot` | state | The `RackTransportState` — `playing`, `progress`, `currentTime`, `duration`. | | `playing` | state | The controller's playback flag, set before awaiting Rack.play; it is not an audible-onset measurement. | | `on(event, listener)` | subscription | `timeupdate` · `transportchange` · `end` (each carries the state) and `error`. Returns the unsubscribe. | | `destroy()` | lifecycle | Idempotent. Stops the rack if active through this controller, drops polling and subscriptions, and retains the borrowed rack. Attempts cleanup before rethrowing a cleanup failure. | `RackTransportOptions.requestFrame` and `.cancelFrame` replace the polling scheduler; defaults use animation frames with a timeout fallback. This polling updates UI state and does not schedule musical sound. `RackTransportState` contains `playing`, `progress` (0–1), `currentTime`, and `duration` (transport seconds), using the longest timeline member. Empty racks have zero position and duration. Commands through a different owner do not automatically update this controller's playback flag. `RackTransportEvents` contains `timeupdate`, `transportchange`, and `end` with that state, plus `error: unknown`. Listener failures use the shared playback diagnostic path. Destroyed controllers ignore transport commands.
    ## Related [`` and ``]() compose Rack with [mixer UI](). [ScorePlayer]() owns timeline playback and shared error payload types; [InteractivePlayer]() owns `AdvanceOptions` and `BeatNote`. [Effect]() owns the master/member recipes. --- # ScorePlayer > The push engine: hand it a Score and a timbre and it schedules and plays the whole piece on its own clock. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/headless/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. `ScorePlayer` owns an autonomous clock: hand it a `Score` and a timbre and it schedules + plays the whole piece, emitting `cursor` / `end` events. This is the engine behind the `` element. For a one-call code action, `await playScore(score, options)` creates, starts and returns that same controllable player; `playScoreFromUrl()` also performs the load. Keep the returned player so you can pause, seek and eventually `dispose()` it. `play()` prepares an async `Sound` before it schedules the first note; `preload()` is available when you want that warm-up earlier. A supplied synth is borrowed by default, so `dispose()` invokes only a route cleanup returned by `connect()` and never its global `disconnect()` / `dispose()` unless you choose `synthOwnership: 'owned'`. The [TonePlayer integration](#toneplayer) below covers applications that already own a Tone.js transport. It retains its own timing and ownership contract. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/headless/play/score-player/) ## Import ```ts import {ScorePlayer, Sound} from '@webmusic/score/play/headless'; const player = new ScorePlayer(score, { synth: Sound.oscillator(), synthOwnership: 'owned', }); const off = player.on('timeupdate', ({progress}) => paintProgress(progress)); await player.play(); // call from a user gesture when audio needs permission // On unmount or replacement: off(); player.dispose(); ``` `Player` is an alias of this class; `PlayerOptions` and `PlayerEvents` alias `ScorePlayerOptions` and `ScorePlayerEvents`. The source contract is the `ScorePlayer` class and these exported types in `@webmusic/score/play/headless`; see the [Play API]() for the capability's other entry points. ## Borrow data and playback state `player.playback` exposes the [shared nonvisual source](). Its subscription immediately supplies current data, position and active note occurrences, including when a consumer attaches while paused or already playing. Unsubscribing leaves the player alive. The immutable score is fixed for this player's lifetime; a new player is a new source. Use `player.playback.seekNominal(seconds)` for an observed command that rejects a failed restart. The older direct `seek`/`seekNominal` methods retain their compatibility behavior: runtime restart failures use the operation-error channel and may resolve with playback paused. All new bindings name their time axis. ## Time and resource roles The player borrows its immutable `Score`. It owns its scheduler, cursor notifications, output/effect nodes and default oscillator. An injected `audioContext` or `destination.context` is borrowed; supplying both requires the same context. If neither is supplied, the player creates and closes its own context. `context` is a lazy getter: reading it can create the graph. `seconds`, `duration` and `seek()` use rate-scaled transport seconds. `nominalSeconds`, `seekNominal()` and the position axis of `clock` use the original score timeline. `currentTime` is a musical `TimePosition`, not a number. A future `play(when)` arms a start at an absolute AudioContext time; the clock reports `holding` at the resume position until that time. A past `when` starts immediately. Browser timer cadence and backend capabilities still affect scheduling; this is not a wall-clock accuracy guarantee. Custom synths opt into audio-clock lookahead through `supportsScheduledCancellation: true`, unique non-null handles from `noteOn()`, and both `noteOffById()` and `cancelScheduledNote()`. Other backends receive notes just in time. The full backend contract is on [Synth backends]().
    API **Construction and options — `ScorePlayerOptions`** | Member | Default / return | Contract | | --- | --- | --- | | `new ScorePlayer(score, options?)` · `createScorePlayer(score, options?)` | `ScorePlayer` | Constructs without starting playback. | | `createScorePlayerFromUrl(url, options?)` | `Promise` | Loads a score, then constructs; adds an optional `format` to the player options. | | `playScore(score, options?)` · `playScoreFromUrl(url, options?)` | `Promise` | Creates, starts and returns the player. The URL helper also accepts `format`. | | `audioContext` · `destination` | Created context / its destination | Borrowed context and output node; context identities must match. | | `synth` · `synthOwnership` | Owned oscillator / `'borrowed'` for a supplied synth | A supplied `HeadlessSynth` is disposed only with explicit `'owned'` ownership. | | `tempo` | Score tempo | BPM override relative to the score's initial tempo. | | `effect` · `reverb` | Dry output | An explicit `Effect` takes precedence; `reverb` accepts `ReverbOptions` or `false`. See [Effect](). | | `cursorIntervalMs` | `50` | Cursor/time-update notification cadence in milliseconds. | | `lookaheadSeconds` | `0.1` | Scheduling horizon; effective minimum `0.02`. | | `schedulerIntervalMs` | `25` | Scheduler tick interval; effective minimum `5` ms. | | `createTickSource(intervalMs)` | Kernel tick-source factory | Inject a scheduler source; the player owns the returned source's lifecycle. | | `expandRepeats` | `false` | Expand score repeat structure before playback. | **Commands** | Member | Return | Contract | | --- | --- | --- | | `preload()` | `Promise` | Builds the timeline/graph and awaits sample readiness for sounding pitches, including part transposition; use `play()` to start and resume audio. | | `play(when?)` | `Promise` | Prepares audio and starts or arms playback at absolute AudioContext seconds. | | `pause()` · `stop()` | `void` | Pause preserves the point; stop resets to the beginning. | | `seek(seconds, when?)` · `seekFraction(fraction, when?)` · `seekNominal(seconds, when?)` | `Promise` | Seek in transport seconds, fraction, or nominal score seconds. While playing, optional `when` schedules the restart; paused seeks ignore it. | | `scrub(deltaSeconds)` | `Promise` | Relative transport seek. | | `setTempo(bpm)` · `setRate(rate)` | `void` | Change playback speed; retain the nominal musical point. | | `setVolume(value)` · `setPan(value)` | `void` | Master linear gain (minimum zero) and stereo pan (clamped to −1…1). | | `setLoop(startSeconds, endSeconds)` · `clearLoop()` | `void` | Set an ordered, clamped region in current transport seconds, or remove it; an empty region clears the loop. | **State** | Member | Type / meaning | | --- | --- | | `playback` | Borrowed `ScorePlaybackSource`: resolved score, coherent snapshot/subscription and optional nominal seek; see [shared contract](). Reading it does not create an audio graph. | | `context` | Lazily realised `AudioContext`. | | `clock` | Read-only transport clock; nominal score-second position against AudioContext time. | | `currentTime` | Musical `TimePosition`. | | `seconds` · `nominalSeconds` | Current transport seconds and nominal score seconds respectively. | | `durationSeconds` · `duration` | Rate-scaled transport duration, in seconds. | | `rate` · `progress` · `isPlaying()` | Tempo scale, fraction of the piece, and playback state. | **Events — `on(name, listener)` returns an unsubscribe function** | Event | Payload | | --- | --- | | `cursor` | `TimePosition`. | | `noteOn` · `noteOff` | The score `Note`. | | `end` | The completed `Score`. | | `timeupdate` | `PlayerTimeUpdate`: `nominalSeconds`, `transportSeconds`, `transportDurationSeconds`, `progress`; deprecated `seconds` and `duration` alias the transport fields. | | `operationError` | Contained backend/graph/lifecycle failure: `kind`, `source`, `operation`, `error`. | | `listenerError` | Subscriber failure: `kind`, `source`, `event`, `mode`, `error`. Distinct from an operational failure. | **Lifecycle** `dispose(): void` is idempotent. It stops scheduling, releases owned nodes and resources, and closes an internally created context. For a borrowed synth it calls only the cleanup returned by that player's `connect()` route. Keep and call subscription cleanups when a view detaches before the player does. ### Offline rendering helpers These stateless helpers are exported by both `@webmusic/score/play` and `@webmusic/score/play/headless`. They use an `OfflineAudioContext`, not the live player's clock, and await sample preparation before rendering. A synth must route into the renderer's destination through `connect()` to be captured. Self-routing Tone/Spessa backends do not meet that requirement; use a context-adaptable `Sound` or compatible backend. ```ts import {renderScoreToWav, Sound} from '@webmusic/score/play/headless'; const wav = await renderScoreToWav(score, { synth: Sound.oscillator(), synthOwnership: 'owned', sampleRate: 48_000, }); ``` | Helper | Return / contract | | --- | --- | | `renderScoreToBuffer(score, options?)` | `Promise` with scheduled score notes and an optional effect tail. | | `renderScoreToWav(score, options?)` | `Promise` containing a 16-bit PCM WAV. | | `bufferToWav(buffer)` | `Blob` encoding an existing buffer as 16-bit PCM WAV; no audio graph or playback lifecycle. | **`RenderOptions`** | Option | Default | Contract | | --- | --- | --- | | `synth` | Owned oscillator | Compatible `HeadlessSynth` or `Sound` to connect into the offline graph. | | `synthOwnership` | `'borrowed'` for a supplied synth | `'owned'` transfers teardown; borrowed synths receive only their returned route cleanup. | | `effect` · `reverb` | Dry | Explicit `Effect` takes precedence over legacy `false` / `ReverbOptions`. | | `tempo` | Score tempo | BPM override. | | `sampleRate` | `44100` | Output samples per second. | | `channels` | `2` | Output channel count. | | `tailSeconds` | `1.5` with an effect, otherwise `0.15` | Extra time after the score for release/effect tails. | The renderer owns the offline graph for the operation. It releases effect nodes and its synth route after rendering; a borrowed synth remains the caller's responsibility. Setup, preparation and rendering failures also release the acquired graph. Cleanup attempts every owned resource even if another cleanup throws; the initiating failure takes precedence over cleanup errors. Native and offline preparation use the same sounding-pitch projection, retaining note ids and other note data without changing the Score. The resulting `AudioBuffer`/`Blob` needs no `dispose()`. If an application creates an object URL for the Blob, it owns that URL and must revoke it when finished. ### TonePlayer `TonePlayer` adapts a [Tone.js](https://tonejs.github.io/) Transport so a `Score` plays through Tone's clock and instruments (requires the optional `tone` peer). #### TonePlayer Import ```ts import * as Tone from 'tone'; import { TonePlayer } from '@webmusic/score/play/headless'; const instrument = new Tone.PolySynth(Tone.Synth).toDestination(); const synth = { noteOn(midi, velocity, time) { instrument.triggerAttack(Tone.Frequency(midi, 'midi').toNote(), time, velocity / 127); }, noteOff(midi, time) { instrument.triggerRelease(Tone.Frequency(midi, 'midi').toNote(), time); }, }; // Use shared mode because this host owns Tone.Transport.start(). const player = new TonePlayer(score, { tone: Tone, synth, transportOwnership: 'shared', }); await player.play(); Tone.getTransport().start(); // Later, release this player and the instrument owned by this example. const dispose = () => { player.dispose(); instrument.dispose(); }; ``` #### TonePlayer Time and transport ownership `TonePlayer` supports the rate-aware driver transport surface (`seconds`, `duration`, `progress`, `seekFraction`, `scrub`, `setRate`), but it deliberately does not expose ScorePlayer's loop or `timeupdate` contract. Use `ScorePlayer` when those controls are required. Its default `transportOwnership: 'owned'` preserves standalone controls; use `transportOwnership: 'shared'` when a host already owns the injected global `Tone.Transport` and must remain its sole controller. A supplied synth is borrowed by default, including one passed later to `setSynth(synth, ownership)`; opt into `'owned'` only when the player should dispose that backend. In shared mode, real Tone transports notify the player about host `start` / `pause` / `stop` events, so the player clears only its own voices, schedule entries, and cursor when the host pauses or stops. `cancel()` and a direct host seek have no lifecycle event; call `await player.rearmSharedTransport(localSeconds?)` after either. It never changes the host transport, and the optional local seconds argument selects the score point to schedule after an external seek. Custom injected transports that do not expose `on` / `off` remain supported, but require this explicit rearm step after host lifecycle changes too. The adapter maps nominal score seconds to rate-scaled local transport seconds. It schedules callbacks on the injected Tone transport; it does not publish a Kernel `TransportClockReader`. A shared-mode `play()` arms the player's entries but does not start the host transport. Neither mode's returned Promise means the score has finished. Use a user gesture when the injected `tone.start()` must unlock audio. **Construction and TonePlayerOptions** | Member | Default | Contract | | --- | --- | --- | | `new TonePlayer(score, options)` | — | Builds a performed-score timeline and resolves the injected transport. Throws if no usable transport is supplied. | | `tone` | Required | `ToneLike` namespace or structural adapter, described below. | | `synth` | None | `SynthBackend` receiving scheduled note attacks/releases; no automatic instrument is created. | | `synthOwnership` | `'borrowed'` for supplied synth | `'owned'` allows backend disposal when replaced or when the player is disposed. | | `transportOwnership` | `'owned'` | `ToneTransportOwnership`: owned controls the transport; shared schedules only this player's work. | | `tempo` | Score's initial tempo | Initial tempo scale, retaining internal tempo changes. | | `expandRepeats` | `false` | Expand the Score's repeat structure before scheduling. | **Commands and state** | Member | Kind | Contract | | --- | --- | --- | | `play()` | `Promise` | Unlocks audio when needed and schedules work; owned mode may start the host transport. Rejects after disposal or on unlock failure. | | `pause()` | `void` | Releases this player's active voices and pauses its local pass; owned mode also pauses the transport. | | `stop()` | `void` | Clears this player's schedule and resets its position; only owned mode stops/rewinds the transport. | | `seek(seconds)` | `void` | Clamps to local duration and rebuilds affected schedule entries; shared mode leaves host seconds unchanged. | | `seekNominal(seconds)` | `void` | Converts nominal score seconds by the current rate, then seeks. | | `scrub(deltaSeconds)` | `void` | Relative seek in rate-scaled seconds. | | `seekFraction(fraction)` | `void` | Clamps to 0–1 and seeks within local duration. | | `rearmSharedTransport(positionSeconds?)` | `Promise` | Shared mode only; rebuilds entries after host cancellation/repositioning. Omission preserves the current local position. | | `setTempo(bpm)` | `void` | Changes the local tempo scale while preserving nominal position; does not set host `Transport.bpm`. Non-finite, nonpositive or unchanged resulting scales are ignored. | | `setRate(rate)` | `void` | Scale relative to the score's initial tempo; nonpositive values select 1. Infinite resulting scales are ignored. Use finite positive values; no bounded rate-range validation is provided. | | `setSynth(synth, ownership?)` | `void` | Releases active voices through their original backend, installs the new backend, and disposes a distinct previous owned backend. Ownership defaults to borrowed. | | `rate` | Readonly number | Current tempo scale. | | `seconds` | Readonly number | Local, rate-scaled seconds, clamped to duration. | | `nominalSeconds` | Readonly number | `seconds * rate`, on the original score axis. | | `duration` / `durationSeconds` | Readonly number | Rate-scaled duration; the latter is an alias. | | `progress` | Readonly number | Fraction through local duration, zero for empty duration. | | `currentTime` | Readonly `TimePosition` | Score location derived from the nominal position. | | `isPlaying()` | boolean | In shared mode requires both an armed local pass and a started host transport. | | `on(event, listener)` | Unsubscribe function | Subscribe to `TonePlayerEvents`. | | `dispose()` | `void` | Idempotently clears this player's entries, voices, polling, host listeners, and owned synth. Does not close the host audio context or dispose a borrowed synth. | **TonePlayerEvents** | Event | Payload | Timing | | --- | --- | --- | | `cursor` | `TimePosition` | Visual cursor polling and explicit position updates; not an audio scheduling deadline. | | `noteOn` / `noteOff` | `Note` | This player's scheduled note lifecycle. | | `end` | `Score` | Completion of this player's scheduled material. | | `operationError` | `PlaybackOperationError` | Backend/lifecycle failure with `kind`, `source`, `operation`, and `error`. | | `listenerError` | `PlaybackListenerError` | Subscriber failure with `kind`, `source`, `event`, `mode`, and `error`. | The error payload types are shared with [ScorePlayer](). There is no `timeupdate` event or public loop setter. Disposal removes owned work even in owned mode; it is not a general `Tone.Transport.stop()` or context-close operation. Lifecycle commands may synchronously emit `noteOff`. If a callback issues a newer play, seek, rate change, synth replacement or disposal, the older command does not overwrite that newer intent afterward. Rebuilding commands invalidate their old schedule before releasing a captured list of old voices; every old voice still receives its cleanup, while newly scheduled voices remain separate. An already running player can re-arm its invalidated pass without taking control of a shared host transport. Audio unlocking has a separate lifetime: pause, stop, rearm and disposal cancel an older pending `play()` continuation; seek and tempo changes keep that pending play and determine the position/rate it eventually schedules. After disposal, pause/stop/seek and tempo/rate controls are inert; `play()`, `rearmSharedTransport()` and `setSynth()` continue to reject or throw as appropriate to their return types. **Injected contracts** | Type | Members | Responsibility | | --- | --- | --- | | `ToneLike` | `start(): Promise`; optional `now()`, `getContext()`, `getTransport()`, `Transport` | Unlock audio and provide a transport through `getTransport()` or the fallback property. Time fallbacks support cleanup against the audio reference clock. | | `ToneTransportLike` | `bpm.value`, `seconds`, `state`, `start`, `pause`, `stop`, `schedule(callback, time)`, `clear(id)`, `cancel(after?)` | Supply transport position and scheduling. Schedule callbacks receive audio-reference time. | | `ToneTransportLike.on` / `.off` | Optional start/pause/stop listeners | Observe host lifecycle in shared mode. Without both, the application explicitly re-arms after host changes. | #### TonePlayer Related [ScorePlayer]() provides the native scheduler and loop/timeupdate contract. [Transport drivers]() consume the shared rate-aware transport surface. TonePlayer has no dedicated public Element; an application can bind its state to [transport UI]().
    ## Related - [ScorePlayer Web Component]() — element composition. - [PlayerController]() — transport UI state. - [Sound]() and [Effect]() — graph recipes and ownership. - [InteractivePlayer]() — host-driven beat timing. --- # Sound > The timbre namespace — oscillator, FM, wavetable, noise, samplers and the optional SF2 and Tone backends behind one shape. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/headless/play/sound/ 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. `Sound` wraps oscillator, FM, wavetable, noise, sample-map, SFZ, SoundFont, MIDI, and custom backends behind the `HeadlessSynth` contract. A factory creates a lazy timbre; connecting it creates its private gain bus and backend. Supply it as a [`ScorePlayer`]() synth or an [`InteractivePlayer`]() voice. The [synth backend reference](#synth-backends) below also covers direct synths, range samplers, modulation, metering and the synth-panel audio graph. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/headless/play/sound/) ## Import ```ts import {ScorePlayer, Sound} from '@webmusic/score/play/headless'; const player = new ScorePlayer(score, { synth: Sound.oscillator({type: 'triangle'}), synthOwnership: 'owned', }); await player.preload(); await player.play(); // Starts playback; does not wait for the piece to finish. // Keep the player while playing; call this when its session is removed. const dispose = () => player.dispose(); ``` ## Routing, readiness, and ownership One `Sound` has one active route. A second `connect()` rejects while the first is active. Create separate Sounds for simultaneous players or voices; `Sound.layer()` also requires distinct child instances. The context comes from the destination and is borrowed, never closed by `Sound`. `connect()` starts asynchronous setup. Both `ready` and `preload()` reject before connection. After connection, `ready` observes the current load; `preload()` can retry a failed setup. Prefer the player's `preload()` when a player owns the route. Sample fetch/decode, unavailable peers, MIDI access, and worklet preparation can reject; constructing a recipe does not prove readiness. A player borrows an explicitly supplied synth by default. Use `synthOwnership: 'owned'` for a Sound created for that player's lifetime. `Sound.from()` and `Sound.custom()` do not introduce a separate borrowing option: explicit `Sound.dispose()` can call the supplied backend or instrument's global `dispose()`, and `disconnect()` can call its global `disconnect()`. Use a factory when independent backend instances are needed. The callback returned by `connect()` releases that route without invoking global backend disposal. For a custom backend that returns no route cleanup, a released Sound cannot reconnect until its backend has been globally disconnected or disposed. Built-in recipes can create fresh backends on a later connection; an adopted instance must itself remain reusable. ## Time and environment `Sound` has no transport, cursor, or event emitter. A caller supplies MIDI pitch, velocity in 0–127, an absolute context time in seconds, and a duration in seconds. Scheduling and cancellation are backend capabilities, described under [Synth backends](). Connecting requires compatible Web Audio nodes. Sample URLs require `fetch` and audio decoding. `soundfont2()` loads the optional `spessasynth_lib` 4.x peer when the sound connects; install it with `npm install spessasynth_lib@^4` so your bundler can resolve the lazy module. The AudioWorklet is a separate asset: provide `workletUrl` or `registerWorklet` in every environment, including Vite. `from()` accepts an already-created Tone-style instrument and does not import Tone. `midiOut()` needs an injected output or available Web MIDI access; it sends notes to that output rather than producing local audio. `midiOut()` returns an opaque handle per attack. Its automatic end and an explicit `noteOffById()` share one release, preventing duplicate note-offs; `retimeScheduledNote()` can extend or shorten that gate. Timers wait for the borrowed audio clock, while explicit releases at the current time still quiet hardware during suspension. Closing the clock or releasing the route retires pending sends and attempts every sounding release. MIDI 1.0 device behavior for overlapping equal pitches is device-dependent; the adapter preserves the matching message count but cannot assign a hardware voice id.
    API ### Construction The constructor is private. All eleven factories return `Sound`. | Member | Options | Description | | --- | --- | --- | | `Sound.oscillator(options?, sound?)` | `OscillatorSynthOptions`, `SoundOptions` | Polyphonic oscillator with filter and envelope. Backend options are owned by the [oscillator reference](). | | `Sound.samples(map, options?, sound?)` | MIDI-keyed `SoundfontSample` map; remaining `SoundfontSynthOptions`; `SoundOptions` | Exact-note sample map, with the backend's fallback for missing samples. See [sample loading](). | | `Sound.sfz(url, options?)` | `SfzOptions` | Fetch an SFZ definition and resolve its sample regions into a ranged sampler. | | `Sound.soundfont2(source, options?)` | URL string or `ArrayBuffer`; `Soundfont2Options` | Prepare an SF2/SF3 worklet instrument. | | `Sound.from(instrument, sound?)` | `HeadlessSynth` or `ToneInstrumentLike`; `SoundOptions` | Wrap an existing instrument, including its global cleanup methods. | | `Sound.custom(implementation, sound?)` | `HeadlessSynth` or `(context: AudioContext) => HeadlessSynth`; `SoundOptions` | Supply a backend instance or create one for each connection. | | `Sound.layer(...sounds)` | Distinct `Sound` instances | Fan each note into the children. Repeated child identities throw. Global cleanup reaches all children. | | `Sound.fm(options?)` | `FmOptions` | Two-operator frequency modulation. | | `Sound.wavetable(options?)` | `WavetableOptions` | Web Audio `PeriodicWave` synthesis. | | `Sound.noise(options?)` | `NoiseOptions` | White noise, optionally band-pass filtered at the note pitch. | | `Sound.midiOut(options?)` | `MidiOutOptions` | Send MIDI note messages to a borrowed output. | ### Options and supporting types | Type / member | Default | Description | | --- | --- | --- | | `SoundOptions.gain` | `1` | Initial linear bus gain, independent of a backend's gain option. | | `FmOptions` | `ratio: 2`, `index: 200`, both oscillator types `'sine'`, attack `0.005`, release `0.12` | `ratio` is modulator/carrier frequency ratio; `index` is frequency deviation in Hz. `carrierType`, `modulatorType`, `attackSeconds`, `releaseSeconds`, and inherited `gain` complete the options. | | `WavetableOptions` | `real: [0, 1, 0.5, 0.25, 0.12]`, zero-filled `imag`, attack `0.008`, release `0.1` | `real`/`imag` are cosine/sine partial arrays; also accepts `attackSeconds`, `releaseSeconds`, and `gain`. | | `NoiseOptions` | `pitched: true`, `Q: 8`, attack `0.001`, release `0.12` | Filter selection/resonance, `attackSeconds`, `releaseSeconds`, and `gain`. | | `Soundfont2Options` | `channel: 0`, `program: 0`, `gain: 1` | MIDI channel 0–15 and program 0–127; optional `workletUrl: string \| URL` or `registerWorklet(context)` callback, and `maxSoundFontBytes` (128 MiB). | | `MidiOutOptions` | `channel: 0`, first available output | `output?: MidiOutputLike`; otherwise requests Web MIDI access. Channel is masked to four bits. Inherited bus `gain` does not scale MIDI messages. | | `MidiOutputLike` | — | Structural `send(data: number[] \| Uint8Array, timestamp?)` output; the wrapper never closes it. | | `ToneInstrumentLike` | — | Optional `triggerAttack`, `triggerRelease`, `connect`, `disconnect`, and `dispose` methods. Attack receives a note name and normalized 0–1 velocity. | | `SfzOptions.attackSeconds` / `releaseSeconds` / `gain` | `0.004` / `0.12` / `1` | Sample envelope and Sound bus gain. | | `SfzOptions.maxSfzBytes` | `1_000_000` bytes | Definition response limit. | | `SfzOptions.maxZones` / `maxConcurrentLoads` | `512` / `4` | Maximum regions and concurrent sample fetch/decode jobs. | | `SfzOptions.maxSampleBytes` / `maxTotalSampleBytes` | 32 / 128 MiB | Per-sample and total unique source-byte limits. | | `SfzOptions.maxDecodedSampleBytes` / `maxTotalDecodedSampleBytes` | 64 / 256 MiB | Per-sample and total decoded PCM limits. | Resource limits must be positive safe integers. Limit violations throw `RangeError`; asynchronous load failures reject readiness. SFZ support is the implemented region/key/velocity subset, not every opcode in the format. [`SampleZone`]() and its selection helpers are documented with `RangeSampler`. ### Commands, state, and lifecycle | Member | Kind | Description | | --- | --- | --- | | `connect(destination)` | command | Creates the route and begins loading; returns its idempotent route cleanup callback. Synchronous graph failures throw. | | `preload(): Promise` | command | Waits for connected setup/backend loading; retries after a previous load failure. | | `ready: Promise` | state | Current loading result; rejects when unconnected or when the last setup failed. | | `setGain(value): this` | command | Sets the bus gain immediately, clamping negative values to zero; no upper clamp. | | `noteOn(midi, velocity, time, durationSeconds)` | command | Forwards the attack; exposes its handle only if the backend supports exact-handle release. Before connection it returns `undefined`. | | `noteOff(midi, time)` | command | Forwards pitch release when available. | | `noteOffById(handle, time)` | command | Forwards exact-voice release when available. | | `supportsScheduledCancellation` | state | True only when the connected backend opts in and supplies cancellation and exact release. False before connection. | | `cancelScheduledNote(handle, time)` | command | Forwards cancellation of a scheduled attack when supported. | | `retimeScheduledNote(handle, time)` | command | Forwards the new logical note-off time when supported. | | `disconnect()` | lifecycle | Releases the route and invokes backend-wide disconnection when provided. | | `dispose()` | lifecycle | Releases the route and calls backend `dispose()`, falling back to `disconnect()`. Custom cleanup errors can propagate. | The wrapper has no state subscription or transport events. Optional forwarding methods are inert when no backend is connected; they do not queue attacks for future readiness. ### Synth backends Use these exports to own a synth graph directly. [`Sound`]() provides the lazy wrapper; the concrete synth constructors below allocate against a context immediately. LFO, meter, and panel controllers solve separate graph and control tasks and do not create a transport or UI. #### Synth backends Import ```ts import {createOscillatorSynth} from '@webmusic/score/play/headless'; // context is supplied and resumed by the application. const synth = createOscillatorSynth(context, {type: 'sawtooth', cutoff: 8000}); const detach = synth.connect(context.destination); const start = context.currentTime; const handle = synth.noteOn(60, 90, start, 0.5); synth.noteOffById(handle, start + 0.5); // Retain this cleanup until the voice/session is finished. const dispose = () => { detach(); synth.dispose(); }; ``` #### Synth backends Synth contract `HeadlessSynth` is a structural interface; `SynthBackend` is its alias. `PreloadableHeadlessSynth` additionally permits `preload(notes?)`. `SynthOwnership` is `'borrowed' | 'owned'`, selected by the consuming player. A returned `SynthRouteCleanup` removes only the route made by one `connect()`; it does not replace global backend disposal. All note times below are absolute `AudioContext.currentTime` seconds, durations are seconds, pitches are MIDI note numbers, and velocities use 0–127. A synth has no playback cursor or score clock. Web Audio source backends can schedule against the context clock; MIDI/timer adapters need different dispatch behavior. #### Synth backends LfoController `LfoController` owns a repeating control-rate callback, not an AudioNode oscillator. It can drive any borrowed `LfoTarget` through `apply(value)`. Modulation is centered between the target's `min` and `max`, with amplitude scaled by depth; the compatibility `value` field does not set the center. ```ts import {createLfoController} from '@webmusic/score/play/headless'; const lfo = createLfoController({ rate: 2, target: {min: 200, max: 4000, apply: (value) => { filter.frequency.value = value; }}, }); lfo.start(); // Later, when the modulation owner is removed: const dispose = () => lfo.dispose(); ``` #### Synth backends LevelMeterController `LevelMeterController` is the score-side meter over the kernel's analyser tap: give it a context and it builds a transparent `input → analyser → output` graph you splice anywhere; give it an existing `analyser` and it only borrows. It never runs a loop of its own — **you** call `readLevel()` from your own animation frame, which keeps the paint rate the UI's decision rather than the engine's. ```ts import {createLevelMeterController} from '@webmusic/score/play/headless'; const meter = createLevelMeterController({context}); source.connect(meter.input!); meter.output!.connect(context.destination); let frame = 0; const paint = () => { const {level, peak, peakHold} = meter.readLevel(); draw(level, peak, peakHold, meter.readSpectrum(28)); frame = requestAnimationFrame(paint); }; paint(); // Keep the cleanup with the owner of this graph and drawing loop. const dispose = () => { cancelAnimationFrame(frame); source.disconnect(meter.input!); meter.dispose(); }; ``` #### Synth backends SynthPanelAudioGraph `SynthPanelAudioGraph` owns the audio graph [``]() assembles: effect recipes and an optional EQ, wired between stable boundary gains. `rebuild()` constructs a replacement before retiring the previous graph. A failed construction preserves the previous graph; same-context rebuilds retain `input` and `output`, so caller connections remain valid. With no active effects/EQ, those boundaries carry a dry bypass. Construction or wiring failures release every completed candidate while preserving the prior route, including saved values on reused custom effect parameters. It does not itself create a meter, LFO, or instrument. It is exported for a caller building an equivalent panel; for a timbre, see [`Sound`](). ```ts import {DEFAULT_EQ_BANDS, SynthPanelAudioGraph} from '@webmusic/score/play/headless'; const graph = new SynthPanelAudioGraph(); graph.rebuild({context, effects: [], bands: DEFAULT_EQ_BANDS, sections: ['sound', 'eq'], effectValues: []}); const input = graph.input!; source.connect(input); graph.output!.connect(context.destination); graph.rebuild({context, effects: [], bands: DEFAULT_EQ_BANDS, sections: ['sound'], effectValues: []}); // Later, when the panel graph is removed: const dispose = () => { source.disconnect(graph.input!); graph.teardown(); }; ``` ##### Synth backends Shared commands and lifecycle | Member | Contract | | --- | --- | | `noteOn(midi, velocity, time, durationSeconds): unknown` | Required attack operation; may return an opaque voice handle. | | `connect?(destination)` | Optional route creation; may return `SynthRouteCleanup`. | | `disconnect?()` | Optional backend-wide route disconnection. | | `noteOff?(midi, time)` | Optional pitch-based release. | | `noteOffById?(handle, time)` | Optional exact-voice release, needed when overlapping equal pitches must remain independent. | | `supportsScheduledCancellation?` | Explicit opt-in to player lookahead. True requires a unique, non-null attack handle, exact release, and cancellation that can silence an attack before it sounds. | | `cancelScheduledNote?(handle, time)` | Retracts a scheduled voice; stronger than allowing it to play a release tail after a future onset. | | `retimeScheduledNote?(handle, time)` | Moves an active voice's logical note-off; the backend accounts for its release tail. | | `preload?(notes?)` | Optional `PreloadableHeadlessSynth` readiness. Native/offline playback supplies sounding-pitch notes with original ids and other data; direct callers supply the pitches they intend to play. | | `dispose?()` | Optional backend-wide final cleanup. A player does not call it on a borrowed synth. | These interfaces define no events, subscriptions, or universal `ready` getter. See each concrete backend for the subset implemented. ##### Synth backends Oscillators and exact-note samples | Construction / option | Default | Description | | --- | --- | --- | | `new OscillatorSynth(context, options?)` / `createOscillatorSynth(context, options?)` | — | Borrow a context; create a polyphonic oscillator output bus. | | `OscillatorSynthOptions.type` | `'triangle'` | Web Audio oscillator type. | | `attackSeconds` / `releaseSeconds` | `0.012` / `0.08` | Per-voice envelope timing. | | `gain` / `detune` | `0.16` / `0` | Voice gain and cents of detuning. | | `cutoff` / `resonance` | `20000` / `0.7` | Low-pass cutoff in Hz and Q. | | `setParam(key, value)` | — | Oscillator-only option update; changes subsequent attacks. | | `new SoundfontSynth(context, options?)` / `createSoundfontSynth(context, options?)` | — | Exact-MIDI samples with a fallback instrument. This is a sample map, not an SF2 loader. | | `SoundfontSynthOptions.samples` | No map | `Record`. A sample is a URL string, `ArrayBuffer`, or `AudioBuffer`. | | `resolveSample(midi)` | Absent | Returns a `SoundfontSample` or `undefined`; takes precedence over the map. | | `fallback` | New `OscillatorSynth` | Used when a pitch has no loaded sample. Its route is connected to the sample bus; the sample synth calls its `dispose()` during teardown. Do not supply a shared fallback unless that ownership is intended. | | `gain` / `attackSeconds` / `releaseSeconds` | `1` / `0.004` / `0.08` | Sample envelope settings. | | `maxConcurrentLoads` | `4` | Concurrent fetch/decode jobs. | | `maxSampleBytes` / `maxTotalSampleBytes` | 32 / 128 MiB | Per-sample and aggregate source-byte limits. | | `maxDecodedSampleBytes` / `maxTotalDecodedSampleBytes` | 64 / 256 MiB | Per-sample and retained PCM limits. | | `preload(notes?)` | All configured MIDI keys | Sample-only readiness. With notes, loads their distinct pitches; repeated pending requests for a pitch share work. | Loading limits must be positive safe integers. Fetch/decode and limits can reject preload. Missing/unloaded samples use the fallback; `noteOn()` itself does not wait for a fetch. A custom `resolveSample` without a map needs an explicit note list to preload pitches. Both classes implement `connect`, `disconnect`, `noteOn`, `noteOff`, `noteOffById`, `cancelScheduledNote`, `retimeScheduledNote`, and `dispose`. Oscillator cancellation is always supported; the sample synth advertises it only when its fallback also supports exact cancellation. Both own their voices and stop remaining sources on idempotent disposal; they never close the context. After disposal, connection/attack and sample preload reject or throw. ##### Synth backends RangeSampler `new RangeSampler(context, options?)` borrows a context and accepts [`SfzOptions`](). It uses the envelope and sample-load limits; `gain` and `maxSfzBytes` belong to the outer `Sound.sfz()` wrapper and definition-loading step. | Member | Kind | Description | | --- | --- | --- | | `loadZones(zones: SampleZone[], context): Promise` | command | Fetches and decodes resolved sample URLs. Pass the sampler's context. Snapshots input descriptors; replaces the zone set only when the current load succeeds. | | `connect`, `disconnect` | routing | Output connection and exact route cleanup, or global output disconnection. | | `noteOn(midi, velocity, time, durationSeconds)` | command | Selects a region and pitch-shifts from its root key; returns a numeric handle or `undefined` when no key range covers the pitch. | | `noteOff`, `noteOffById`, `cancelScheduledNote`, `retimeScheduledNote` | command | Pitch/exact release, scheduled cancellation, and logical note-off retiming. | | `supportsScheduledCancellation` | state | Always `true`. | | `dispose()` | lifecycle | Idempotently aborts owned fetches, invalidates loading, stops owned voices, clears samples, and disconnects output. | | `SampleZone` | type | `sample` URL; inclusive `loKey`, `hiKey`, `loVel`, `hiVel`; `rootKey`; optional `tuneCents` and `volume` in dB. | | `chooseZone(zones, midi, velocity, roundRobin?)` | helper | Selects matching key/velocity ranges; falls back to key-only matches. Equally matching candidates rotate using the supplied `Map`. Retain that map across calls for round-robin selection. | | `playbackRateFor(rootKey, midi, tuneCents = 0)` | helper | Pure ratio: `2 ** ((midi - rootKey) / 12) * 2 ** (tuneCents / 1200)`. | A newer load or disposal invalidates the previous load and aborts its fetches; superseded work cannot commit zones, and its promise may resolve without a replacement. A failure in the current generation rejects and cancels sibling fetches. There is no loading-state event or `ready` getter on `RangeSampler`. ##### Synth backends SpessaSynthSynth `new SpessaSynthSynth(instance, channel = 0, context?)` wraps an already-created, already-routed spessasynth instance. It neither imports the optional peer nor constructs its worklet; use `Sound.soundfont2()` when starting from an SF2/SF3 source. The supplied instance remains caller-owned. | Member | Kind | Description | | --- | --- | --- | | `noteOn(pitch, velocity, time)` | command | Schedules the instance's `noteOn(channel, pitch, velocity)` with a timer. | | `noteOff(pitch, time)` | command | Schedules its pitch release. Direct callers must supply note-offs; there is no duration-based release argument on this class. | | `dispose()` | lifecycle | Cancels owned timers and attempts matching note-offs for attacks already sent. Does not dispose the supplied instance or close its context. Repeated calls retry note-offs that failed. | Timing uses the explicit context, then the instance's `context` or `targetNode.context`; with no clock it dispatches on a zero-delay timer. It has no `connect`, route output, or exact scheduled-cancellation capability, so it cannot be used as a routable offline capture synth. Notes are ignored after disposal. When a context is available, timer callbacks wait until its time reaches the requested timestamp; suspension does not advance future notes. An explicit release at the current time still quiets the instrument while suspended. Timer wake-up and external-device latency remain unbounded by this adapter. ##### Synth backends LfoController | Member | Kind | Default / contract | | --- | --- | --- | | `new LfoController(options?)` / `createLfoController(options?)` | construction | Creates a stopped controller. Options extend `LfoControllerConfig`. | | `shape` | option | `'sine'`; `LfoShape` is `'sine' \| 'triangle' \| 'square' \| 'saw'`. Invalid shapes retain the fallback. | | `rate` / `depth` / `phase` | option | `1` Hz / `0.6` / `0`. Rate clamps to 0.05–12; depth to 0–1; phase wraps to `[0, 1)`. Nonfinite values use the prior/default value. | | `target` | option | Optional borrowed `LfoTarget`: `min`, `max`, `apply(value)`, optional compatibility `value` and descriptive `unit`. | | `now` | option | Seconds; defaults to `Date.now() / 1000`. Supply a monotonic clock when required. | | `requestFrame` / `cancelFrame` | option | Injectable `LfoRequestFrame` / `LfoCancelFrame`; defaults to a 16 ms timer, not browser rAF. `LfoFrameCallback` receives a timestamp, but phase uses `now()`. | | `onError(error)` | option | Receives contained failures from the active frame/target run, including returned Promise rejections. | | `configure(config)` | command | Updates `LfoControllerConfig`: shape/rate/depth/phase/target. Does not apply the target immediately. An explicitly present `target: undefined` detaches it. | | `setTarget(target)` | command | Replace or detach the borrowed target. | | `start()` / `stop()` / `setRunning(boolean)` | command | Start or stop the reusable run. Stop retains phase/configuration and cancels the pending callback. Repeated calls in the same state do nothing. | | `snapshot()` | state | Fresh `LfoControllerState`: shape, rate, depth, phase, running. | | `subscribe(notify)` | subscription | Changes and frame updates; no initial notification. Returns an unsubscribe callback. | | `dispose()` / `destroy()` | lifecycle | Final, idempotent cleanup of callbacks/subscribers. Never destroys the target; subsequent commands are inert. | | `lfoWave(shape, phase)` / `LFO_SHAPES` | helper | Pure waveform value for phase in the conventional 0–1 cycle, and supported shapes. The helper itself does not wrap phase. | The frame advances phase, applies the target, publishes state, then schedules the next callback. It is a control-rate driver, not an audio-rate oscillator or beat-synchronized transport. Synchronous command/subscriber failures can throw; an active frame failure stops that run and reports through `onError`. There is no named event emitter. ##### Synth backends LevelMeterController **Construction** — `new LevelMeterController(options?)` / `createLevelMeterController(options?)`: `context` (build an owned tap) or `analyser` (borrow one), plus `fftSize` (1024) and `smoothingTimeConstant` (0.8) for an owned analyser. Supplying both a context and an analyser throws `TypeError`. With neither, no graph is created and reads throw because there is no analyser. The context is always borrowed. | Member | Kind | Description | | --- | --- | --- | | `readLevel()` | command | One `LevelMeterFrame`, taken now: normalized `level`, `peak`, and decaying `peakHold`. Decay advances per read, so polling frequency affects it. | | `readSpectrum(bars?)` | command | Normalized `Float32Array` spectrum bands (default 28, minimum 4). | | `input` · `output` | state | The splice points of an owned tap; `undefined` when borrowing. | | `analyser` | state | The analyser itself, for a caller that wants raw bins. | | `dispose()` | lifecycle | Idempotent. Releases an owned tap; a borrowed analyser is never disconnected. | `LevelMeterControllerOptions` and `LevelMeterFrame` are the construction/read types above. It has no subscriptions or timer; stop the caller's polling before disposal. Reads after disposal throw. The controller suppresses cleanup errors from the underlying owned tap. ##### Synth backends SynthPanelAudioGraph | Member | Kind | Description | | --- | --- | --- | | `new SynthPanelAudioGraph()` | construction | Creates an empty coordinator with no nodes. | | `rebuild(options)` | command | Atomically replaces the graph from `SynthPanelAudioOptions`. With no context, calls teardown. Nested rebuilds throw. | | `options.context` | input | Required field, `AudioContext \| undefined`; borrowed and never closed. A changed context replaces the boundary nodes, requiring caller reconnection. | | `options.effects` / `effectValues` | input | Required effect recipes and corresponding records of saved numeric parameter values. Built only when `sections` includes `'effects'`; matching `EffectNode.params` receive the values. | | `options.bands` | input | Required EQ band descriptors; used only with section `'eq'`. | | `options.sections` | input | Required `SynthPanelSection[]`: `'sound'`, `'effects'`, `'envelope'`, `'eq'`, `'lfo'`, `'macros'`. Only effects/EQ cause this graph owner to build processing nodes. | | `teardown()` | command | Releases the current graph. Idempotent, and safe before any build. | | `input` · `output` | state | Current boundary nodes; `undefined` before construction and after teardown. | | `effectNodes` · `equalizer` | state | Current effect bundles and [`EqController`](); initially `[]` and `undefined`. Treat these as owned by the graph. | Teardown releases all graph-owned nodes/effects/EQ and can surface a cleanup failure after attempting the remaining cleanup. It is reusable through another rebuild and has no `dispose()` alias or event subscription. Construction failures throw; failures retiring an already-replaced graph are reported through the playback-operation diagnostic path without undoing the replacement. #### Synth backends Related [``]() controls supplied timbres and composes `LfoController` and `SynthPanelAudioGraph`; [`Sound`]() composes the synth backends. [`Effect` and `EqController`]() own post-processing and EQ contracts. A meter can be inserted into a caller-owned Web Audio route and polled by any UI without introducing another musical clock.
    ## Related [`ScorePlayer`](), [`InteractivePlayer`](), and [``](). Use [`Effect`]() for processing after the timbre. --- # createPitchView > Held-pitch state with optional playback attachment for application-owned keyboard, staff and fretboard views. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/headless/view/pitch-view/ 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. `createPitchView(options?)` maintains current MIDI pitches, either from manual note events or a borrowed playback source. Its state can drive a keyboard, staff or fretboard without constructing a visual surface. It needs no Score for manual input and owns no audio resource or clock. It composes the counted-note behavior of [createActiveNoteTracker]() with source attachment and cleanup. [``]() uses this model behind its browser adapter and UIKit presenter. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/headless/view/pitch-view/) ## Import ```ts import {createPitchView} from '@webmusic/score/view/headless'; import {currentStaffMarks, currentFretMarks} from '@webmusic/score/view'; const view = createPitchView(); const unsubscribe = view.subscribe(({activeMidis}) => { console.log(currentStaffMarks(activeMidis, 'flat')); console.log(currentFretMarks(activeMidis)); }); view.noteOn(60); view.noteOn(64); view.noteOff(60); const dispose = () => { unsubscribe(); view.dispose(); }; ``` For native playback, pass `{playback: player.playback}` at construction or call `setPlayback(player.playback)`. The caller owns and releases the player. The source supplies the initial snapshot and subsequent updates, including paused held notes and overlapping equal pitches. ## State ownership Manual notes are counted: two attacks at MIDI 60 need two releases before the pitch disappears. A native snapshot is a complete replacement of that manual state, deduplicated by occurrence identity before projecting its unique MIDI set. Non-ready, stopped and ended snapshots clear activity. Older revisions cannot resurrect released notes; source replacement clears the previous scope. `clear()` empties current notes while retaining the source subscription. A later source snapshot can populate notes again. To switch permanently to manual control, call `setPlayback(undefined)` first.
    API **Construction** — `createPitchView(options?: PitchViewOptions): PitchView` | Member | Kind | Default | Description | | --- | --- | --- | --- | | `playback` | option | none | Borrowed `ScorePlaybackSource`. Subscribed and hydrated at construction. | **Commands** | Member | Kind | Description | | --- | --- | --- | | `noteOn(midi)` | command | Adds one held occurrence and returns current `PitchViewState`. MIDI must be an integer in `0…127`; invalid input throws `RangeError`. | | `noteOff(midi)` | command | Releases one occurrence and returns state; an unheld valid MIDI does not remove other pitches. Same validation as `noteOn`. | | `clear()` | command | Clears held notes, retains playback attachment and returns state. | | `setPlayback(source?)` | command | Releases the old subscription, clears prior activity and hydrates the new source. Omit to detach. Returns state. | | `updatePlayback(snapshot)` | command | Applies a `ScorePlaybackSnapshot` without attaching a source; suitable for a caller-owned adapter. Returns state. | **State** | Member | Kind | Description | | --- | --- | --- | | `state` | state | Current `PitchViewState`. | | `state.activeMidis` | state | Read-only unique ascending held MIDI values. Initially empty without input. | | `state.playback` | state | Latest accepted `ScorePlaybackSnapshot`, or `undefined` without playback input. | **Events and lifecycle** | Member | Kind | Description | | --- | --- | --- | | `subscribe(listener)` | subscription | Observes subsequent `PitchViewState` changes through `PitchViewListener`, without an initial callback. Read `state` when mounting; returns an unsubscribe function. | | `dispose()` | lifecycle | Idempotently releases the borrowed source subscription and listeners. Does not stop or dispose the source. | `updatePlayback` requires nonnegative safe-integer `revision` and `sourceRevision` values. Active occurrence IDs must be nonempty and identify one integer MIDI pitch; conflicting IDs or invalid notes throw `RangeError` before changing state. Repeated IDs for the same pitch count once. Listener failures propagate to the caller without rolling back committed state; a reentrant state change supersedes remaining older notifications. Mutations and new subscriptions after disposal throw. The caller must detach external manual-note input listeners when its view is released. ### Active-note tracker `createActiveNoteTracker()` tracks which MIDI notes are held. That is all it does, and the narrowness is the point: it needs no `Score`, so a source that has no sequence to match against — a MIDI keyboard, ``, a step sequencer — can drive it exactly like a player can. For automatic playback attachment and reusable staff/fretboard projections, use [createPitchView](). It owns the source subscription and composes this smaller tracker. That is the difference from [`createScoreView`](), whose `noteOn` matches an attack against a sequence onset and therefore only accepts events that carry one. Reach for the tracker when the source is live and for the view when the source is a score. It retains held-note counts and subscriptions, but owns no audio, score, timer, DOM element or input binding. There is no `dispose()` method; detach the caller's input listeners and call subscription cleanups before dropping the tracker. #### Tracker Import ```ts import {createActiveNoteTracker} from '@webmusic/score/view/headless'; const held = createActiveNoteTracker(); const off = held.subscribe(({activeMidis}) => paintKeys(activeMidis)); held.noteOn(60); held.noteOff(60); off(); ``` #### Tracker State is immutable and sorted `activeMidis` is frozen and sorted ascending, so a renderer can diff two snapshots or index into them without copying first. Every command returns the new state as well as publishing it, so a caller that only wants the result of one call can skip `subscribe`. Out-of-range input throws rather than being ignored: a MIDI value must be a safe integer in 0…127. Notes are reference-counted: `noteOn` twice then `noteOff` once leaves that pitch active. Feed a matching release for every attack, and filter keyboard auto-repeat in the input adapter if it should not count as another voice. **Construction** — `createActiveNoteTracker()`, no options. **Commands** | Member | Kind | Description | | --- | --- | --- | | `noteOn(midi)` | command | Increments the held count; returns new state. Throws `RangeError` unless MIDI is a safe integer in 0…127. | | `noteOff(midi)` | command | Decrements the count and removes the pitch at zero; returns new state. An unheld release leaves the set unchanged but still publishes. Validates the same MIDI range. | | `clear()` | command | Releases everything; returns the new state. | **State** | Member | Kind | Description | | --- | --- | --- | | `state` | state | The current `ActiveNoteState`. | | `state.activeMidis` | state | Frozen, ascending array of held MIDI numbers. | **Events** | Member | Kind | Description | | --- | --- | --- | | `subscribe(listener)` | subscription | Called synchronously with new state after every command, with no immediate initial callback. Returns an unsubscribe function. | The source contract is `createActiveNoteTracker`, `ActiveNoteTracker`, `ActiveNoteState` and `ActiveNoteListener` at `@webmusic/score/view/headless`. Read `state` when mounting, then subscribe to updates. Callback failures propagate to the command caller; there is no separate error event or background clock. `clear()` resets counts and emits empty state but does not detach subscribers. If a subscriber changes state synchronously, the newer publication supersedes the remaining callbacks from the older one. A subscriber removed before its turn is skipped. Commands return the latest state after callbacks finish; a thrown callback does not roll back the committed held-note counts. #### Tracker Related **Composed by.** [``]() — Score / View, which is why that element lights up from playback and from live MIDI input alike. [createScoreView]() tracks actual sequence notes. [Live trackers]() add chord/key analysis to note input.
    ## Related [Current-pitch projections]() supply written staff marks and exact fret positions from this state; those helpers do not infer chords, choose fingerings or own state. [UIKit pitch presenters]() own size, appearance and interaction. [createScoreView]() owns a score’s note sequence and timeline rather than current-pitch input alone. --- # createScoreMapView > Whole-score map state and quarter-note navigation with optional borrowed playback. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/headless/view/score-map-view/ 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. `createScoreMapView(options?)` combines the plain [`createScoreMap`]() projection with a current position, source attachment and navigation outcomes. It owns derived map data and subscriptions, while the caller owns playback and presentation. It performs no analysis, engraving, file loading or audio scheduling. Use it for a navigator or density strip in a custom interface. [createScoreView]() instead maintains individual notes and visible time windows for score renderers. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/headless/view/score-map-view/) ## Import This example borrows a caller-supplied `Score` named `score` for local inspection. ```ts import {createScoreMapView} from '@webmusic/score/view/headless'; const view = createScoreMapView({score, maxCells: 32}); const unsubscribe = view.subscribe((state) => { console.log(state.map?.cells, state.quarters, state.pending); }); const outcome = await view.seekQuarters(4); if (outcome.status === 'failed') console.error(outcome.error); const dispose = () => { unsubscribe(); view.dispose(); }; ``` To follow playback and navigate its owner, construct with `{playback: player.playback}`. An explicit `score` takes precedence over borrowed data. Calling `setScore(undefined)` restores source following. If explicit and borrowed Score identities differ, `readiness` becomes `'mismatched'` and bound navigation fails without seeking that player. ## Navigation and adapters The map’s cells and marks use quarter notes. `seekQuarters()` clamps a finite requested position to the score extent, converts it through the tempo map to nominal seconds and calls the native source’s `seekNominal` command. Rate-scaled transport conversion remains the playback owner’s responsibility. Without a native source, an optional `seekNominal` callback can adapt an application-owned command. Feed authoritative positions back through `setPosition(nominalSeconds)`. Without either command path, seeking changes only local map state. This allows static inspection without allocating a player. A command returns `Promise`: `committed`, `superseded`, or `failed` with an `error`. Results also include current `nominalSeconds` and `quarters`. Source replacement, a newer request or disposal supersedes an old pending request. Newer source observations win over requested positions; a failed command does not overwrite the cursor with an optimistic position.
    API **Construction** — `createScoreMapView(options?: ScoreMapViewOptions): ScoreMapView` | Member | Kind | Default | Description | | --- | --- | --- | --- | | `score` | option | borrow source | Explicit immutable Score. | | `playback` | option | none | Borrowed `ScorePlaybackSource`, hydrated on attachment. | | `seekNominal` | option | none | `(seconds: number) => void \| Promise` command adapter when no native source is attached. Native source commands take precedence. | | `part` | option | all parts | Part ID or name used to filter density counts; timeline coverage remains whole-score. | | `maxCells` | option | `64` | Finite cell budget, rounded and clamped to at least one. | | `maxMarks` | option | `24` | Finite ruler budget, rounded and clamped to at least one. Major landmarks survive thinning, so this is not a hard limit. | Nonfinite budgets throw `RangeError`, including construction before data arrives. The last three options are `ScoreMapOptions`; map fields and projection rules are owned by the [map data reference](). **Commands** | Member | Kind | Description | | --- | --- | --- | | `setScore(score \| undefined)` | command | Replaces explicit data or restores source borrowing. Invalidates pending seeks and resets/reconciles position. | | `setPlayback(source \| undefined)` | command | Detaches prior observation, resets prior source state and hydrates the new source. Neither source is disposed. | | `configure(options)` | command | Replaces the `ScoreMapOptions` bag and recomputes map data. Omitted fields return to defaults; this does not replace the score or player. | | `setPosition(seconds)` | command | Applies an authoritative nominal position, clamped to the score extent. Nonfinite input throws `RangeError`. | | `seekQuarters(quarters)` | command | Returns a `ScoreMapSeekOutcome` promise. Nonfinite input, missing data, unavailable/mismatched source or absent native seek support produce `failed`. | **`ScoreMapViewState`** | Member | Kind | Description | | --- | --- | --- | | `score` | state | Effective Score, or `undefined` without input. | | `map` | state | Read-only map with `durationQuarters`, `cells` and `marks`, or `undefined` without input. | | `nominalSeconds` | state | Current nominal score position; initially `0`. | | `quarters` | state | Current position converted through the effective score; initially `0`. | | `readiness` | state | `ScorePlaybackReadiness`, `'mismatched'`, or `'unbound'` with no native source. An unbound local map can still navigate. | | `pending` | state | Whether the current navigation request is awaiting completion. | Read these values through `view.state`. **Events and lifecycle** | Member | Kind | Description | | --- | --- | --- | | `subscribe(listener)` | subscription | Immediately emits current state, then changes, through `ScoreMapViewListener`. Returns an unsubscribe function. Reentrant changes supersede older notifications. | | `dispose()` | lifecycle | Idempotently releases observation and listeners and supersedes pending commands. Never stops or disposes playback. | State setters, configuration and new subscriptions after disposal throw; `seekQuarters` instead returns a failed outcome. If releasing an old source throws during replacement, the map publishes cleared unbound state and propagates the error without attaching the new source; retry attachment explicitly. An initial subscription failure also clears borrowed state before propagating. Disposal completes locally before releasing the subscription, so a failing cleanup cannot restore a live view. Listener exceptions propagate to their caller; an initial subscription callback that throws is removed. No background event or animation loop is created.
    ## Related [``]() composes this state with a browser player adapter and timeline presenter. [createAnalysisFollower]() adds score analysis to a borrowed timeline. [UI Kit timeline]() provides presentation independently of these musical contracts. --- # createScoreView > A code-only view model over a Score: which notes are visible, which are sounding, and where the playhead is — with no surface of its own. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/headless/view/score-view/ 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. `createScoreView(score, options?)` is the state a score view needs, without the view. It flattens the `Score` into a note sequence once, then answers two questions as the piece plays: **what is inside the viewport**, and **what is sounding right now**. Your framework subscribes and draws. Reach for it when you own the surface — a React tree, a canvas engine, a native renderer. If you want WebMusic to draw as well, the [`/render` adapters]() draw from the same Score, and [``]() is both of them behind one tag. It **borrows** the `Score` and its cached immutable flattened sequence, and owns its snapshots, active note set and subscriptions. There is no audio, rendering surface or clock. `subscribe()` returns an unsubscribe function; `dispose()` releases this view model's active state and listeners. Caller-installed player/input bindings must be detached separately. A binding supplied through `options.playback` is owned by the view and is detached by `dispose()`; the player remains borrowed. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/headless/view/score-view/) ## Import ```ts import {createScoreView} from '@webmusic/score/view/headless'; const view = createScoreView(score, { type: 'staff', viewport: {startTime: 0, endTime: 8}, }); const off = view.subscribe(({visibleNotes, activeNotes}) => { renderWithMyFramework({visibleNotes, activeNotes}); }); view.noteOn(60, 0); // a player attacked middle C at t=0 view.seek(4); off(); view.dispose(); ``` ## Notation and annotations Selecting `type: 'staff'` does not engrave notation inside this object. `sequence`, `visibleNotes` and `activeNotes` describe sounding notes. The staff render adapter additionally reads the original `score` for written durations, rests, voices, beams, tuplets, timed clefs and expressive directions. A custom notation renderer should also use that Score data; the flattened sequence alone cannot reproduce the source's notation. Glyph layout and VexFlow remain on `/render`, with no DOM or engraving dependency in this Headless object. The renderer's [`showAnnotations` option]() controls expressive directions without changing this view model's state or playback position. It is not a `createScoreView` construction option. ## Viewport and virtualization `setViewport(start, end)` narrows the window; `clearViewport()` widens it back to the whole piece. Two fields answer the same question at different costs: `visibleRange` is an index range into `sequence.notes` — start inclusive, end exclusive — that a virtualizing renderer can slice directly, while `visibleNotes` is the exact set whose sounding span intersects the window. Take the range when you are windowing thousands of notes and the exact set when you are drawing a handful. The exact viewport predicate is `note.endTime > start && note.startTime <= end`: a note starting at the right edge is visible, while one ending at the left edge is excluded. Sounding activity uses the separate half-open predicate `note.startTime <= time && time < note.endTime`. Changing a drawing mode or viewport preserves time and active-note identity. Resize belongs to your renderer; the model accepts time windows, not pixel dimensions or pointer coordinates. ## Follow a playback source Pass `{playback: player.playback}` with the same immutable Score used by the player. The view applies the initial snapshot immediately and follows nominal position and active-note identity. For application-managed observation, call `updatePlayback(snapshot)` with values from the [shared source](). The view's Score is fixed: source replacement requires a new view for the new Score. A snapshot with a different Score cannot silently apply its notes to this sequence. This binding reads state only; `view.seek()` remains local projection, while playback navigation must use the source's authorized seek command. Unavailable, disposed or mismatched snapshots clear highlights and retain the last local position. A compatible snapshot with `nominalSeconds: null` likewise retains position, while still applying its note identities. Part ID and note ID form an exact pair; equal pitches in other parts are not substituted. ## Active notes `noteOn(midi, startTimeSeconds)` matches an attack against the sequence, so the highlight is a real note rather than a pitch. That match is why the second argument exists: the same pitch can sound twice in one bar. `noteOn` activates all matching sequence notes at that pitch/onset and returns the first match. The MIDI value here is the sequence's written pitch. Native playback observation uses part/note identity and remains unambiguous for transposing instruments. `noteOff(midi, onset?)` releases matching notes; without an onset it releases all active notes of that pitch. `clearActiveNotes()` releases all, and `end()` is what a player's `end` event should call. A live source — a MIDI keyboard, a step sequencer — has no sequence onset to match against. Use [`createActiveNoteTracker`]() for those; it keys on MIDI alone. All time arguments are **nominal score seconds**, on the sequence's time axis. Use a player's nominal position/onset when playback rate changes; rate-scaled transport seconds describe a different axis. `seek()` derives the notes sounding at the requested point, while `noteOn` consumes an exact sequence onset (within a one-microsecond matching tolerance). The score is fixed for the lifetime of a view; create a new model for an edited score.
    API **Construction** — `createScoreView(score: Score, options?: ScoreViewOptions)` | Member | Kind | Default | Description | | --- | --- | --- | --- | | `type` | option | `'piano-roll'` | `'piano-roll'`, `'staff'` or `'waterfall'`; picks how a renderer should read the sequence. | | `viewport` | option | whole piece | `{startTime, endTime}` in seconds. | | `playback` | option | none | Borrow a `ScorePlaybackSource`; synchronously apply the initial snapshot and follow updates without DOM. | **Commands** | Member | Kind | Description | | --- | --- | --- | | `setType(type)` | command | Switches the drawing mode; returns the new state. | | `setViewport(start, end)` | command | Narrows the window; returns the new state. | | `clearViewport()` | command | Widens back to the whole piece; returns `ScoreViewState`. | | `seek(seconds)` | command | Clamps finite seconds to the score, derives sounding notes, and returns new state. | | `noteOn(midi, startTimeSeconds)` | command | Activates matching notes, advances the playhead and publishes; returns the first `ScoreSequenceNote`, or `undefined` if none matches. | | `noteOff(midi, startTimeSeconds?)` | command | Releases matching notes; without onset releases all active notes of that pitch. Returns `void`. | | `clearActiveNotes()` | command | Releases every sounding note; returns `void`. | | `end()` | command | Clears active notes and places the playhead at sequence end; returns `void`. | | `reset()` | command | Clears active notes and sets time to zero, retaining the current type and viewport; returns `void`. | | `updatePlayback(snapshot)` | command | Apply a `ScorePlaybackSnapshot` for this exact Score identity, using nominal time and part/note identity. Incompatible or unavailable data clears active highlights. Returns the new state. | **State** | Member | Kind | Description | | --- | --- | --- | | `score` | state | The borrowed `Score`. | | `sequence` | state | The flattened sounding-note `ScoreNoteSequence`, computed once; full written notation remains on `score`. | | `state` | state | The current immutable `ScoreViewState`. | | `state.type` · `state.sequence` | state | Selected drawing mode and the model's flattened sequence. | | `state.currentTime` | state | Playhead in seconds. | | `state.activeNotes` | state | Sequence notes currently sounding. | | `state.visibleRange` | state | `{start, end}` index range into `sequence.notes`. | | `state.visibleNotes` | state | Notes intersecting the viewport. | | `state.viewport` | state | The active window, or `undefined` for the whole piece. | **Events** | Member | Kind | Description | | --- | --- | --- | | `subscribe(listener)` | subscription | Called synchronously with new state after each state command, with no initial callback. Returns an unsubscribe function. | Read `state` on initial mount. The return types differ by command as shown above; void-returning commands still publish their resulting state. `ScoreViewListener` receives one `ScoreViewState`; callback failures propagate to the command caller and have no separate error event. If a listener changes state synchronously, that newer publication supersedes the remaining callbacks from the older one. A listener removed before its turn is skipped. State-returning commands return the latest state after these callbacks; their state change remains committed if a callback throws. **Validation and lifecycle** MIDI arguments require safe integers in 0…127. Note-on/off onsets must be finite non-negative seconds. A viewport requires finite `0 <= startTime <= endTime`. Unsupported view types throw `RangeError`. Non-finite compatible playback positions throw `RangeError` before changing local time or active notes. An initial playback subscription failure propagates from construction; the source is responsible for releasing a subscription whose synchronous initial delivery throws, as required by its subscription contract. `dispose(): void` is idempotent; it clears active notes and listeners without publishing, while the last state remains readable. Later state commands and new subscriptions throw. It does not dispose the score or an external player. Local disposal completes before calling the borrowed subscription's cleanup. If that cleanup throws, its error propagates once, the view stays disposed, and late source callbacks are ignored.
    The source contracts are `createScoreView`, `ScoreView`, `ScoreViewOptions`, `ScoreViewState`, `ScoreViewTimeRange`, `ScoreViewType` and `ScoreViewListener` at `@webmusic/score/view/headless`. Sequence data types are described by the [View API](). ## Related **Composed by.** [``]() — Score / View, which pairs it with the `/render` adapters and a [`@webmusic/ui/stage`]() surface. [createActiveNoteTracker]() handles live pitches that have no score onset. [createScoreMapView]() follows replacement scores and delegates navigation; this model’s `seek()` updates local state. [createPitchView]() adds playback attachment to score-independent held-pitch state. --- # Score > Symbolic music — capabilities, installation, registration and public references for Score. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/ 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. Score is one package, `@webmusic/score`, for **symbolic** music: notes with pitches, durations and voices, read from MIDI, MusicXML, MXL or ABC. 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. For a working player with notation and a keyboard, start with [Quick Start](). This page covers package selection and setup. ## Capabilities | Capability | What it covers | Forms | | --- | --- | --- | | **play** | Turn a `Score` into sound: push and pull engines, timbres, effect chains, racks, offline WAV render, capture of performed note events into a `Score`. | [Web Components]() · [Headless]() · [API]() | | **analyze** | Ask a `Score` questions: key, chords, roman numerals, motifs, rhythm figures, voice-leading issues, distributions and summaries. | [Web Components]() · [Headless]() · [API]() | | **view** | Draw a `Score`: piano roll, staff, waterfall, engraved notation, whole-piece maps and thumbnails. | [Web Components]() · [Headless]() · [API]() | | **io** | Format detection and MIDI, MusicXML, MXL and ABC parsing/serialization, with worker-backed parsing. | [API]() | | **react** | The same capabilities as React components and hooks. | [API]() | The package root is the model itself — `Score`, `Rational`, `Pitch`, `Duration`, `TimeMap`, queries and JSON — and it touches no browser API. See the [package subpath table]() for every published entry. ## What Score is not Score does not decode recorded audio or analyze PCM waveforms. It synthesizes notes and can export their sound as WAV. Audio-file processing and cross-domain Bridge coordination remain accepted future directions; those packages and their public references have not been migrated into this first-release checkout. ## Install :::note[Package availability] Registry installs and CDN resources require the relevant package versions to be published. If npm returns `E404`, use the [local checkout installation](#from-a-local-checkout) below. Building the documentation site does not publish those packages. ::: ### From npm Once the packages are available in your registry, run this in your application: ```bash npm install @webmusic/score @webmusic/kernel @webmusic/ui ``` ### From a local checkout Use Node.js 22 or later. From the root of a [`main` checkout](https://github.com/koperative-lab/WebMusic/tree/main), install the locked dependencies, build the packages and create their installable archives: ```bash npm ci npm run build:packages npm pack -w @webmusic/kernel -w @webmusic/ui -w @webmusic/score ``` `npm pack` writes three `.tgz` files to the repository root and prints their filenames. In your existing ES-module bundler application's directory, install all three together. Replace `VERSION` with the version in those filenames and `../WebMusic` with the relative path to your checkout: ```bash npm install "../WebMusic/webmusic-kernel-VERSION.tgz" "../WebMusic/webmusic-ui-VERSION.tgz" "../WebMusic/webmusic-score-VERSION.tgz" ``` The archives provide the local `@webmusic` packages; npm still installs their third-party dependencies from your configured registry. Continue with the [bundler registration](#with-a-bundler) below or the [Quick Start player](). ### Entries and optional peers `io`, `play`, `analyze`, `view`, and `react` are export subpaths of that one installed package; use their public import paths rather than installing them separately. `@webmusic/kernel` is required. Add optional peers for the entries and features you use; importing a visual Element requires `@webmusic/ui`. | Peer | When it is needed | | --- | --- | | `@webmusic/ui` | Score Web Components and renderers that compose UI presenters. | | `tone` | An application-supplied Tone.js transport or instrument. | | `spessasynth_lib` | `.sf2` / `.sf3` SoundFont playback. | | `opensheetmusicdisplay` | Detailed sheet engraving through OSMD. | | `react` | The `/react` components and hooks. | The built-in oscillator needs no SoundFont peer. SoundFont playback also needs a served AudioWorklet. Resource preparation, worklet URLs and ownership details belong to the [Play API]() and [View API]() references. ## Drop-in Web Components Web Components combine Score behavior with controls or visual presenters. Register the tags needed by your page, supply their data and resources, and configure them through their public properties and attributes. The [Web Components inventory]() lists the tags, registration functions and shared theming. ### With a bundler Importing `@webmusic/score/play/auto` registers all **Play** elements at their default names. It does not register Analyze or View elements. Use their `@webmusic/score/analyze/element` and `@webmusic/score/view/element` entries and call the registration functions listed in the inventory. ### Custom registration Import an individual `define*` function from the capability's `/element` entry to register a selected tag or choose a custom tag name. The group registration functions register their default names; `defineAllElements()` does not accept custom names. See the [Play](), [Analyze]() and [View]() registration references. ### From a CDN Only Play provides a prebuilt browser IIFE through `@webmusic/score/play/global`, emitted as `dist/play/auto.global.js`. Loading that published bundle registers the Play elements and exposes `WebMusicScorePlay` on `window`. This path needs no bundler, but the requested package version and its assets must be available from the CDN. The ESM entries retain package imports; a plain browser module URL needs an application-provided resolution strategy. Use the bundler setup above for the three-component [Quick Start](). ## Playback Use [ScorePlayer]() when your application owns the controls. The same Headless engine powers the player Element and exposes commands, state and lifecycle hooks. The [Headless inventory]() routes to playback, analysis and view objects; their owning pages contain the examples and complete contracts. ## API + UI Use APIs for data operations and independent UI presenters for application-owned controls. See the [root API]() for models and entry points, and [Transport]() for a presenter binding. The [shared playback source]() connects views and analysis to an existing player's data, state and position. ## React The [React entry]() provides components and hooks over the same Score models and playback contracts. Framework integration does not replace the underlying data model or resource ownership rules. ## Next Steps - [Quick Start]() for the small player, notation and keyboard example. - [Web Components]() for every tag, its owning page and shared theming. - [Headless]() for engines, sessions and their export ownership. - [Core API]() for the score model, builder, time map and events. - [I/O API]() for MIDI, MusicXML, MXL and ABC import/export. - [Play API]() for playback, rendering, recording, sounds and effects. - [Analyze API]() for musical analysis. - [View API]() for visualization and layout primitives. - [React API]() for providers, hooks and view bindings. --- # @webmusic/ui > The @webmusic/ui root: the twenty-five published presenter entries, their shared contracts and deliberate differences, and the release boundary. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/api/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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/ui` is the domain-neutral presenter layer for Score and custom interfaces: DOM presenters and semantic styling hooks with **no** dependency on kernel, score or audio, and no custom-element registration. Every entry below is an import path of the one package, not a separate npm package. This page is the root reference — the entry list, the shape they share, and what the release boundary admits. Positioning and theming live on the [UI Kit overview](); the browse surface is the [Catalog](); each entry's own State, Binding, Options, Handle, tokens and parts live on its page. ## Install :::note Use an available published version, or the monorepo workspaces for local development. ::: ```bash npm install @webmusic/ui ``` It has no dependencies and no required peers. Score declares it as an **optional** peer: package roots and `/headless` entries never reach it, so a headless-only install does not pull it in. ## Package subpaths The root re-exports all twenty-five; import a subpath to keep the rest out of your bundle. | Entry | Class | What it presents | | --- | --- | --- | | [`@webmusic/ui/analysis`]() | Views and analysis | Score and audio analysis result views rendered from plain result objects. | | [`@webmusic/ui/envelope`]() | Parameters and modulation | Accessible ADSR editor over a structural envelope binding. | | [`@webmusic/ui/eq`]() | Parameters and modulation | Accessible graphic-EQ presenter over a structural band/response binding. | | [`@webmusic/ui/harmony`]() | Views and analysis | Flow lane, nameplate, chip strip and wheel: read-outs that show an analysis happening. | | [`@webmusic/ui/level-analyzer`]() | Views and analysis | Live RMS and sample-peak history with threshold, freeze and peak-hold controls. | | [`@webmusic/ui/lfo`]() | Parameters and modulation | Accessible LFO controls and waveform over a structural modulation binding. | | [`@webmusic/ui/macro`]() | Parameters and modulation | Macro control with target readouts, composed over the parameter rack. | | [`@webmusic/ui/meter`]() | Mixing and capture | Level/spectrum presenter and structural meter binding; pulls and `redraw()`s. | | [`@webmusic/ui/minimap`]() | Transport and navigation | DPR-aware overview canvas with an accessible viewport range brush. | | [`@webmusic/ui/mixer`]() | Mixing and capture | Master/channel fader presenter over a structural mixer binding. | | [`@webmusic/ui/note`]() | Note input | Piano/grid/chord note-surface presenter over a structural note binding. | | [`@webmusic/ui/oscilloscope`]() | Views and analysis | Trigger-aligned time-domain waveform with timebase, freeze and probe controls. | | [`@webmusic/ui/panel`]() | Layout and feedback | Semantic section panel that owns section and presenter-slot hosts. | | [`@webmusic/ui/parameter`]() | Parameters and modulation | `mountParameterRack()`, its structural binding and side-effect-free style text. | | [`@webmusic/ui/pitch`]() | Views and analysis | Keyboard, grand stave and fretboard read-outs coloured by the role each pitch is given. | | [`@webmusic/ui/playlist`]() | Transport and navigation | Transport-and-track-list presenter over a structural playlist binding. | | [`@webmusic/ui/recorder`]() | Mixing and capture | Record/playback/export presenter over a structural recorder binding. | | [`@webmusic/ui/stage`]() | Layout and feedback | Framed rendering surface whose binding paints a caller-owned view. | | [`@webmusic/ui/status`]() | Layout and feedback | Generic loading, empty and error status surface over structural state. | | [`@webmusic/ui/spectrum-analyzer`]() | Views and analysis | Log-frequency FFT plot with a probe, freeze and peak hold. | | [`@webmusic/ui/timeline`]() | Transport and navigation | Numeric ruler, regions, ranges, playhead and seek surface. | | [`@webmusic/ui/track-list`]() | Transport and navigation | Accessible named-row index with one current item. | | [`@webmusic/ui/transient-analyzer`]() | Views and analysis | Caller-supplied attack strength and hit history with sensitivity, freeze and clear actions. | | [`@webmusic/ui/transport`]() | Transport and navigation | Structural transport binding plus `mountTransport()` and its named `controls`. | | [`@webmusic/ui/workbench`]() | Layout and feedback | The shell an analysis view sits in: tabs, a stage, docks, a status bar, one clock. | `@webmusic/ui` (the root) is the stable aggregate of the twenty-five. Presenter styles are exported by their owning entries; there is no separate stylesheet subpath. ## The shared presenter shape Most entries export three things: a `mount*()` function, the structural binding interface it consumes, and the same stylesheet text the mount installs, as a side-effect-free string constant. ```ts const handle = mountX(host, binding, options); handle.element; // the presenter's root node inside the host handle.update(); // re-read binding.snapshot() and repaint handle.destroy(); // remove nodes, listeners and the subscription ``` For ordinary snapshot-based mounts: - **Host.** `host` is an `HTMLElement` or an open `ShadowRoot`. The mount appends its presenter markup and normally installs its exported style text, then removes what it created on destroy. `stylesheet: false` skips stylesheet installation where supported. - **Structural binding.** The binding is a plain object the caller writes: `snapshot()` returns current state, commands such as `setValue()` or `play()` perform changes and may return `void` or a `Promise`, and `subscribe(notify)` returns an unsubscribe function. The presenter repaints from a fresh snapshot after each command settles. `subscribe` is optional on most bindings; `TransportBinding` requires it. - **One presenter per host, per entry.** Each module keeps a `WeakMap` from host to handle. Mounting the same kind of presenter on a host again destroys the previous handle first, so a host never accumulates duplicate racks or transports. - **Idempotent destroy.** `destroy()` is safe to call repeatedly; cleanup failures are collected and reported instead of thrown. - **`onError`.** Where supported, options accept `onError?: (error: unknown) => void`. Snapshot, command, subscription and cleanup failures handled by that presenter are routed there; entries without this option leave caller actions responsible for their own failures. Check the owning entry before relying on this callback. - **DOM-safe at import.** Every entry evaluates in Node/SSR: importing reads no browser global, and mounting uses `host.ownerDocument` — including `@webmusic/ui/analysis`, whose render helpers create nodes in the caller-supplied root's owner document (`createAnalysisRoot(document?)` is the one place a document can be passed directly). The following entry-specific shapes serve different lifetimes. They are not interchangeable bindings: | Entry | Difference | | --- | --- | | [meter]() | Pulls frame data through `readLevel` or `readSpectrum`; exposes `redraw()` and optional animation. | | [level-analyzer]() | Receives action callbacks and caller-paced `update(state)` rather than a snapshot/subscription binding. | | [note]() | Uses a note-surface snapshot and optional synchronous interaction ports for note holds and release. | | [oscilloscope]() | Uses a snapshot and commands, with optional subscription; the caller supplies each sampled trace and source identity. | | [transient-analyzer]() | Accepts an `HTMLElement` and action callbacks, then caller-paced `update(state)`; it has no mount options, subscription or `onError` callback. | | [analysis]() | Renders caller-owned report output through one-shot helpers; the caller replaces/removes it and updates any retained painter or playhead. | | [stage]() | Uses `render(surface)` for retained DOM or `draw(frame)` for canvas; its surface-slider helper augments an existing element. | | [panel]() | Receives section descriptors rather than a state binding; returns stable slots whose child mounts the caller owns. | Each owning page describes its complete state, cleanup and styling contract. ## Release boundary Import presenters through the explicit subpaths above or the root barrel. The first-release UI package includes all entries listed here, including those without a current Score Element consumer. Audio and Bridge packages continue on `dev`; UI bindings remain independent of either domain package. Package entries and their supported features are defined by their references, not by a historical component count or the number of current Element consumers. --- # Catalog > Browse the twenty-one @webmusic/ui presenters by UI function, and see which Element composes which. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/catalog/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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 UI Kit publishes presenters, not Custom Elements. Pick a functional group below, then mount the presenter against any compatible Headless object, store, controller or binding. Each card links to the entry's own page. The groups describe UI responsibilities; they do not change npm import paths. Presenters with no current Score or Audio Element consumer are independently usable. | Presenter | Group | Purpose | | --- | --- | --- | | [`@webmusic/ui/level-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/level-analyzer/) | views-analysis | Live dBFS RMS and sample-peak history with hold, threshold, and freeze controls. | | [`@webmusic/ui/spectrum-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/spectrum-analyzer/) | views-analysis | Log-frequency spectrum with keyboard and pointer probing, freeze, and peak hold. | | [`@webmusic/ui/oscilloscope`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/oscilloscope/) | views-analysis | Trigger-aligned waveform window with timebase, freeze and time-amplitude probe controls. | | [`@webmusic/ui/transient-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/transient-analyzer/) | views-analysis | Recent attack-strength and hit history with sensitivity, freeze and clear actions. | | [`@webmusic/ui/transport`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/transport/) | transport-time | Accessible play/pause, seek and elapsed-time controls backed by caller-owned state. | | [`@webmusic/ui/timeline`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/timeline/) | transport-time | A domain-neutral ruler with regions, loop, selection, playhead and optional seeking. | | [`@webmusic/ui/minimap`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/minimap/) | transport-time | A DPR-aware overview canvas with a draggable and keyboard-accessible range brush. | | [`@webmusic/ui/playlist`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/playlist/) | transport-time | Playlist transport, track selection and per-entry loading or error state. | | [`@webmusic/ui/parameter`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/parameter/) | parameters-gestures | An accessible rotary-control rack with stable per-item input and node accessors. | | [`@webmusic/ui/macro`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/macro/) | parameters-gestures | A macro control or compound macro rack with read-only destination values. | | [`@webmusic/ui/panel`](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/panel/) | layout-feedback | A compound section-and-slot skeleton for composing specialized presenters. | | [`@webmusic/ui/envelope`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/envelope/) | parameters-gestures | An accessible ADSR editor with pointer handles and equivalent range inputs. | | [`@webmusic/ui/lfo`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/lfo/) | parameters-gestures | Oscillator run, shape, rate and depth controls with a live phase waveform. | | [`@webmusic/ui/eq`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/eq/) | parameters-gestures | An equalizer response curve whose draggable bands commit through a structural binding. | | [`@webmusic/ui/mixer`](https://koperative-lab.github.io/WebMusic/uikit/mixing-capture/mixer/) | mixing-capture | Master and channel faders with optional mute, solo and transport commands. | | [`@webmusic/ui/meter`](https://koperative-lab.github.io/WebMusic/uikit/mixing-capture/meter/) | mixing-capture | A self-driven level or spectrum meter that pulls per-frame data from its binding. | | [`@webmusic/ui/recorder`](https://koperative-lab.github.io/WebMusic/uikit/mixing-capture/recorder/) | mixing-capture | Record, playback and export controls with live input level and take status. | | [`@webmusic/ui/note`](https://koperative-lab.github.io/WebMusic/uikit/notes/note/) | notes | Piano, grid and chord layouts with reusable pointer, chord and QWERTY interaction. | | [`@webmusic/ui/stage`](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/stage/) | layout-feedback | Generic DOM and canvas stages with DPR, resize, animation and surface-slider lifecycles. | | [`@webmusic/ui/status`](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/status/) | layout-feedback | A shared ready, loading, empty or error surface with appropriate live-region behavior. | | [`@webmusic/ui/track-list`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/track-list/) | transport-time | An accessible index of named, keyboard-operable rows with one current item. | | [`@webmusic/ui/analysis`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/analysis/) | views-analysis | One-shot cards, timelines, summaries and a reusable analysis playhead controller. | | [`@webmusic/ui/pitch`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/pitch/) | views-analysis | Keyboard, staff and fretboard read-outs that press, spell and slide with their input. | | [`@webmusic/ui/harmony`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/harmony/) | views-analysis | A conveyor, a nameplate, a ranked strip and a wheel that show an analysis happening rather than listing what it concluded. | | [`@webmusic/ui/workbench`](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/workbench/) | layout-feedback | The shell an analysis view sits in: a tablist, a stage, a rail of docks, a status bar, and one reading of the clock per frame handed to everything mounted inside it. |
    How Score and Audio Elements compose these presenters These relationships show which presenter to reuse when replacing part of a Score or Audio Element. Declaration-only Elements have no visible UI, so the presenter column is not applicable to them. | Element | Headless / model | UI composition | Status | | --- | --- | --- | --- | | [``](https://koperative-lab.github.io/WebMusic/score/element/play/score-player/) | ScorePlayer / RackTransportController | @webmusic/ui transport · seek · preset/rack shell | composed | | [``](https://koperative-lab.github.io/WebMusic/score/element/play/score-rack-control/) | Rack | @webmusic/ui mixer · master/member faders · labels | composed | | [``](https://koperative-lab.github.io/WebMusic/score/element/play/score-rack-control/) | Rack member (score + Sound) | Declaration only; renders nothing — documented on the desk it belongs to | behavior | | [``](https://koperative-lab.github.io/WebMusic/score/element/play/score-recorder/) | ScoreRecorderSession + recordedNotesToScore | @webmusic/ui recorder · record/play/export · status | composed | | [``](https://koperative-lab.github.io/WebMusic/score/element/play/score-note-input/) | note-input-model · piano/grid/chord/QWERTY pitch mapping | @webmusic/ui note surface · presenter-owned pointer/chord/QWERTY interaction | composed | | [``](https://koperative-lab.github.io/WebMusic/score/element/play/score-synth-panel/) | LfoController + EqController + SynthPanelAudioGraph + parameter descriptors | @webmusic/ui section panel · parameter rack · macro rack · envelope · EQ · LFO | composed | | [``](https://koperative-lab.github.io/WebMusic/score/element/view/score-view/) | createScoreView + createScoreMap + createPianoRollLayout + player binding | @webmusic/ui stage · timeline · five score surfaces | composed | | [``](https://koperative-lab.github.io/WebMusic/score/element/view/score-pitch-view/) | ActiveNoteTracker + pitch projections + player binding | @webmusic/ui pitch · keyboard/staff/fretboard | composed | | [``](https://koperative-lab.github.io/WebMusic/score/element/view/score-sheet-view/) | OSMD renderer binding + MusicXML serializer | @webmusic/ui stage · status; optional OSMD engraving surface | composed | | [``](https://koperative-lab.github.io/WebMusic/score/element/analyze/score-live-chord-analysis/) | LiveChordTracker + sounding chord spelling | @webmusic/ui harmony · current-chord interpretation | composed | | [``](https://koperative-lab.github.io/WebMusic/score/element/analyze/score-chord-analysis/) | AnalysisSession + chord segmentation + key-relative probe | @webmusic/ui harmony · selectable harmonic lane | composed | | [``](https://koperative-lab.github.io/WebMusic/audio/element/play/audio-player/) | AudioPlayer + selected clip/queue/mix backend | @webmusic/ui transport · seek · time · status | composed | | [``](https://koperative-lab.github.io/WebMusic/audio/element/play/audio-playlist/) | AudioPlaylist | @webmusic/ui playlist · queue selection · per-entry state | composed | | [``](https://koperative-lab.github.io/WebMusic/audio/element/play/audio-mixer/) | AudioMixer | @webmusic/ui mixer · master/member strips · mute/solo | composed | | [``](https://koperative-lab.github.io/WebMusic/audio/element/play/audio-recorder/) | AudioRecorder | @webmusic/ui recorder · record · live meter · status | composed | | [``](https://koperative-lab.github.io/WebMusic/audio/element/view/audio-view/) | AudioTimeline + renderer binding | @webmusic/ui stage · SurfaceSlider · status; waveform/spectrogram/meter surface | composed | | [``](https://koperative-lab.github.io/WebMusic/audio/element/view/audio-live-view/) | LiveViewController + LiveScrollBuffer over a borrowed analyser | @webmusic/ui CanvasStage · RAF/DPR/status; live render adapter | composed | | [``](https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-level-analyzer/) | createRealtimeAnalyzer + borrowed player analyser | @webmusic/ui level-analyzer · sampled dynamics · threshold · freeze | composed | | [``](https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-meter/) | AudioMeterController + owned/borrowed analyser | @webmusic/ui meter · quick level/peak/spectrum monitor | composed | | [``](https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-oscilloscope/) | createRealtimeAnalyzer + borrowed player analyser | @webmusic/ui oscilloscope · trigger · timebase · freeze · probe | composed | | [``](https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-spectrum-analyzer/) | createRealtimeAnalyzer + borrowed player analyser | @webmusic/ui spectrum-analyzer · log FFT · frequency probe · peak hold | composed | | [``](https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-transient-analyzer/) | createTransientDetector + borrowed player analyser | @webmusic/ui transient-analyzer · attack strength · threshold · freeze | composed |
    --- # UI Kit > Independent, accessible DOM presenters for application-owned music state and WebMusic components. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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/ui` supplies music controls, readouts and layout surfaces that you can mount into your own interface. A presenter reads a small binding to your state, routes interaction to your commands, and owns the DOM and cleanup for its view. The package has no runtime dependencies or required peers. It creates no audio resources, registers no Custom Elements and requires no framework or theme provider. Score's visual Elements compose these same presenters with their musical behavior. ## Use UI Kit directly Install the npm package in your application: ```bash npm install @webmusic/ui ``` Add a host to your HTML: ```html
    ``` In a TypeScript module processed by your application's bundler, mount a parameter control against a value your application owns: ```ts import {mountParameterRack} from '@webmusic/ui/parameter'; const host = document.querySelector('#level')!; let level = 50; const handle = mountParameterRack(host, { snapshot: () => ({ parameters: [ {id: 'level', label: 'Level', value: level, min: 0, max: 100, step: 1, unit: '%'}, ], }), setValue: (_id, value) => { level = value; }, }); // When your route or component unmounts: // handle.destroy(); ``` The control displays **Level 50 %** and supports pointer and keyboard input. `snapshot()` supplies the current value and bounds; `setValue()` updates the application state. The presenter reads the snapshot again after that command. The example changes a value only. To control sound, connect the command to an application-owned player, synth or other engine. When something else changes the value, call `handle.update()` or provide the binding's optional `subscribe(notify)` so the view can refresh. Call `handle.destroy()` when removing the view; it releases its DOM, listeners and subscription without disposing your state or engine. See [parameter]() for the complete contract. ## Choose an integration | Integration | Use it when | Start here | | --- | --- | --- | | Web Components | You want ready-made tags that combine musical behavior with UI. | [Quick Start]() and [Score setup]() | | Headless | Your application supplies every visual and reuses WebMusic's musical behavior. | [Score Headless]() | | API + UI | You want independent presenters bound to a store, controller or your own model. | The example above and the [presenter Catalog]() | These choices can coexist. Subclassing an exported Score Element is an advanced customization of the Web Component path; it is optional. Direct presenter mounts need no Element class, tag registration or `@webmusic/score` dependency. ## Browse by function | Group | Use it for | | --- | --- | | [Transport and navigation]() | Playback, seeking, visible ranges, playlists and row selection. | | [Parameters and modulation]() | Parameter racks, macros, envelopes, LFOs and EQ curves. | | [Mixing and capture]() | Mixer faders, level/spectrum meters and recording controls. | | [Note input]() | Playable piano, grid and chord surfaces. | | [Views and analysis]() | Passive pitch and harmony readouts, reports and analysis output. | | [Layout and feedback]() | Panels, render hosts, workbench shells and loading/error feedback. | [note]() owns input and note-release behavior; [pitch]() displays supplied pitches. [analysis]() supplies report renderers; [harmony]() supplies changing analytical readouts. Choose by interaction and lifetime even when two surfaces have a similar appearance. ## Theme with CSS custom properties Applications theme presenters through inherited `--wm-*` custom properties: ```css :root { --wm-component-background: #fff; --wm-component-border: 1px solid #d8d8d8; --wm-component-radius: 0; --wm-foreground: #171717; --wm-foreground-muted: #666; --wm-accent: #111; --wm-accent-foreground: #fff; --wm-font-family: system-ui, sans-serif; } ``` Changing a custom property does not remount a presenter, reset playback or rebuild an audio graph. No theme provider, registry or JavaScript runtime is required. Presenters expose a shared outer-surface contract. Most default to a neutral card; Harmony, Pitch and a bare Workbench default to an unframed transparent surface. These four inherited tokens customize either form without changing the binding or remounting: | Token | Typical fallback | Controls | | --- | --- | --- | | `--wm-component-background` | `--wm-surface`, then presenter default | Outer background; accepts `transparent`. | | `--wm-component-border` | `1px solid #d8d8d8`, or `0` for an unframed surface | Complete border shorthand, including `0` or `none`. | | `--wm-component-padding` | `.6rem`, or the presenter's existing inset | Space between the outer surface and its content. | | `--wm-component-radius` | `0` | Outer component corner radius. | Use the global names for a coherent application surface, or the corresponding `--wm--surface-background`, `-border`, `-padding`, and `-radius` names for one presenter (for example `--wm-transport-surface-background`). Older component variables such as `--wm-stage-background` and `--wm-meter-background` remain supported for their existing inner viewport or compatibility role. `--wm-surface`, `--wm-border`, and `--wm-control-radius` remain semantic palette fallbacks. `--wm-control-border` is the shared line color for non-primary inner controls; it falls back through `--wm-border` to `#d8d8d8`. Primary actions, active states, and focus rings keep their own stronger tokens instead of inheriting that soft line. The `--wm-component-*` quartet is the direct outer-surface API. Set `--wm-foreground` for text and drawing ink, `--wm-foreground-muted` for secondary text, `--wm-accent` for active fills and `--wm-accent-foreground` for their labels. Surface tokens never intentionally provide selected text colour. In particular, transparent Analyze backgrounds no longer make selected labels transparent; set the explicit accent foreground to customize them. Musical palettes (`--wm-degree-*`, `--wm-progression-tone-*`), inverse surfaces, note-key faces, and loading overlays retain their dedicated tokens so independent visual roles remain legible. Envelope, EQ and LFO graphs pair `--wm-surface-inverse` with `--wm-foreground-on-inverse`; generic surface and foreground values do not recolor their inverse plots. Set both inverse tokens when changing that palette. Use `--wm-font-family` for UI text and `--wm-font-mono` for fixed-width readouts; presenter-specific font overrides retain precedence. For one transparent outer frame, set `--wm-component-background: transparent`. To also clear ordinary inner surfaces, set `--wm-surface: transparent`; an explicitly configured inner or inverse surface still takes precedence. `--wm-border` is a **colour** for inner edges, while `--wm-component-border` is a complete outer-border declaration. When a Workbench or another presenter already owns the outer frame, pass `surface: 'none'` to nested Harmony or Pitch mounts. It suppresses only their outer background, border, padding and radius, on both stylesheet paths; their musical marks, controls and palette remain active. This prevents duplicate borders and translucent backgrounds being painted twice. Workbench's public `frame` part is the painted box; its root remains the unpainted query container. `stylesheet: false` always skips installation, including owned child presenters. Harmony, Pitch, Workbench and Transport retain inline declarations; other presenters expect the caller to supply the exported stylesheet or replacement CSS. Pseudo states, focus rules, native control pseudo elements and container queries require stylesheet rules even when inline geometry is supplied. Private `--wui-*` properties are implementation or data values; new themes should use `--wm-*`. ### Control geometry Shared control tokens establish the default size and radius for row controls such as transport buttons and seek controls. Presenter-specific overrides and the control's function can require different geometry. | Token | Default | Controls | | --- | --- | --- | | `--wm-control-size` | `2rem` | Shared row-control height where the presenter supports it. | | `--wm-control-radius` | `0` | Shared control corner radius; square is the default. | A vertical fader's length sets its travel, a rotary dial retains its circular shape, and note keys preserve their musical layout. Consult the presenter's Styling section for its size overrides and minimum dimensions. A drawing surface is not a control either. An envelope, an EQ curve, an LFO waveform, a piano board and a timeline lane need room, and how much room is an editorial choice, so they share one scale instead of each carrying its own number: | Token | Default | Used by | | --- | --- | --- | | `--wm-surface-sm` | `72px` | LFO waveform, timeline lane. | | `--wm-surface-md` | `144px` | Envelope, EQ curve, piano board. | | `--wm-surface-lg` | `216px` | Available for a taller drawing; no presenter defaults to it. | Set a tier to re-proportion every drawing at once, or give one presenter any length you like through its own `--wm--height` — the tier is only the default behind it. ## Customization depth Customization is progressive rather than all-or-nothing: | Need | Use | | --- | --- | | One coherent application theme | inherited semantic `--wm-*` custom properties | | Component-specific adjustment | `--wm-transport-*`, `--wm-parameter-*`, `--wm-envelope-*`, `--wm-lfo-*`, `--wm-meter-*`, `--wm-stage-*`, `--wm-status-*`, `--wm-minimap-*` properties | | Style internal Shadow DOM nodes | documented `::part(...)` hooks | | Preserve an existing product skin | legacy variables/classes remain compatibility aliases during migration | | Mount a presenter yourself | a `mount*()` function and a structural binding | | Change markup or interaction | compose different presenters, use Headless with your own UI, or customize an Element mount hook | | Change domain behavior/API | subclass the controller/binding hooks, or compose the Headless object directly | CSS custom properties cross open Shadow DOM boundaries. Stable `part` names expose selected internal nodes without treating all private markup as public. Arbitrary markup customization belongs in the subclass or Headless path instead of accumulating a large matrix of Element attributes. Each presenter's **Styling** section owns its tokens, parts and named handle accessors. Use those documented hooks when styling a specific control; private `wui-*__*` selectors and DOM order are implementation details. ## Layer contract | Layer | Owns | Must not own | | --- | --- | --- | | **Headless** | clocks, players, mixers, analysis sessions, audio graphs, domain state and resource ownership | DOM nodes, CSS, Custom Element registration | | **UI presenter** | accessible markup, input/listener wiring, readout formatting, UI timers/RAF and deterministic cleanup | Domain resources, fetching, scheduling or analysis algorithms | | **Concrete Element** | public tag API, attribute/property/event mapping, direct composition, owned-versus-borrowed choice | duplicate domain state or a generic adapter hierarchy | | **Kernel lifecycle base** | one connected-lifetime cleanup scope and attribute helpers | Shadow DOM, theming, presenter or domain knowledge | | **Renderer** | domain drawings such as staff, piano roll, waveform, spectrogram and waterfall | generic transport state or package resource lifecycle | The direction stays one way: Headless never imports UI or Element code; UI never imports a domain; the concrete Element may import both. ## What remains outside the Kit - MIDI/MusicXML parsing, scheduling, tempo maps and audio graphs stay in Core/Headless. - Piano-roll, staff, waveform, waterfall and spectrogram algorithms stay in domain renderers. - Canvas/WebGL performance policy and long-audio virtualization stay with View. - Framework packages own framework DOM; they may reuse Headless contracts and CSS variables. - Documentation chrome stays in `apps/doc` and is never imported by a published package. ## Where the rest lives | You want | Page | | --- | --- | | To browse the twenty-one presenters, and see which Element composes which | [Catalog]() | | The shape every presenter follows — host, mount, handle, `destroy()`, `onError`, SSR safety | [`@webmusic/ui`]() | | One presenter's State, Binding, Options, Handle, tokens and parts | its page under the class it belongs to | --- # @webmusic/ui/panel > Creates a labelled section-and-slot skeleton for composing specialized presenters. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/panel/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountSectionPanel()` creates a presenter-owned root, semantic sections and stable slot elements. The caller supplies section descriptors and mounts specialized presenters into the returned slots; the panel owns no domain state or child lifecycle. Use it when [parameter](), [envelope](), [LFO]() or other presenters need one shared frame. [``]() composes the same pattern; mount this entry when your application owns the section list and children. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/panel/) ## Import ```ts import {mountSectionPanel} from '@webmusic/ui/panel'; const handle = mountSectionPanel( host, [ {id: 'voice', label: 'Voice parameters'}, {id: 'envelope', label: 'Amplitude envelope'}, ], {label: 'Synth voice controls'}, ); mountVoice(handle.slot('voice')); mountEnvelope(handle.slot('envelope')); ``` ## Section normalization and child ownership A section may be a string id or a `SectionPanelSection`. Ids are stringified and trimmed; empty ids and later duplicates are skipped, so `slots` always has one entry per normalized id in input order. Descriptor-level classes and parts are added after the panel-level values. The panel does not destroy anything mounted into a slot. Destroy child presenters first, then the panel, as the Live Demo does. This entry also has no binding or `update()` because its section topology is fixed at mount. Remount it to change sections or Options. The [shared presenter shape]() covers common host ownership.
    API `mountSectionPanel(host, sections, options?)` accepts an `HTMLElement` or `ShadowRoot` and returns a `SectionPanelHandle`. **State and binding** The presenter has no `State` or `Binding`. Its required `sections` argument is mount-time structure, not mutable state. **Sections** | Member | Kind | Description | |---|---|---| | `sections` | `readonly (SectionPanelSection \| string)[]` | Required ordered descriptors; string entries are shorthand for `{id}`. | | `sections[].id` | `string` | Stable lookup key stored on `data-section` / `data-slot` and used by `slot(id)`. | | `sections[].label?` | `string` | Adds an accessible name to the semantic `
    `. | | `sections[].classNames?.section` | `string` | Adds descriptor-specific classes to this section. | | `sections[].classNames?.slot` | `string` | Adds descriptor-specific classes to this slot. | | `sections[].parts?.section` | `string` | Adds descriptor-specific part tokens to this section. | | `sections[].parts?.slot` | `string` | Adds descriptor-specific part tokens to this slot. | **Options** | Member | Kind | Default | Description | |---|---|---|---| | `label` | `string` | — | When truthy, adds `role="group"` and this `aria-label` to the root. | | `classNames` | `SectionPanelClassNames` | `{}` | Adds compatibility classes to `root`, every `section`, and every `slot`. | | `parts` | `SectionPanelParts` | `{}` | Adds part tokens to `root`, every `section`, and every `slot`. | | `styleText` | `string` | — | Appends caller compatibility rules after the canonical panel stylesheet in the same style node. | | `stylesheet` | `boolean` | `true` | Installs the presenter's stylesheet unless explicitly `false`, for a light-DOM host that cannot inject CSS into its page. | **Handle** | Member | Kind | Description | |---|---|---| | `element` | `HTMLElement` | Presenter-owned `.wui-section-panel` root. | | `slots` | `ReadonlyMap` | Stable map in normalized section order; cleared by `destroy()`. | | `slot(id)` | `(string) => HTMLElement \| undefined` | Looks up a current slot without exposing its wrapper structure. | | `destroy()` | `() => void` | Idempotently clears the map and removes root and installed stylesheet; child resources remain caller-owned. | **Accessibility.** Labelled sections are semantic `
    ` elements with `aria-label`. An `options.label` gives the root `role="group"`; without it the root stays a neutral `
    `. Slots add no roles of their own and never overwrite accessibility attributes on child presenter nodes.
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. The presenter installs `sectionPanelStyle` by default. `styleText` appends caller compatibility rules in the same style node. Set `stylesheet: false` to omit that node, including `styleText`, and supply the exported stylesheet or your own CSS from the host. The root, sections and slots can shrink within flex/grid containers; child presenters remain responsible for their own readable geometry. Use `--wm-panel-surface-border: 0` and `--wm-panel-surface-padding: 0` for a composition whose children already provide their own surfaces. **Tokens** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-panel-gap` | literal | `.7rem` | Vertical gap between sections. | | `--wm-panel-padding` | literal | `.6rem` | Root-inset compatibility fallback. | | `--wm-panel-border` | `1px solid var(--wm-border)` | `1px solid #d8d8d8` | Complete root border shorthand. | | `--wm-border` | literal | `#d8d8d8` | Border-color fallback inside the default shorthand. | | `--wm-panel-radius` | `--wm-control-radius` | `0` | Root corners. | | `--wm-control-radius` | literal | `0` | Shared radius fallback. | | `--wm-panel-background` | `--wm-surface` | `#fff` | Root surface. | | `--wm-surface` | literal | `#fff` | Shared surface fallback. | | `--wm-panel-foreground` | `--wm-foreground` | `#444` | Root text/current color. | | `--wm-foreground` | literal | `#444` | Shared foreground fallback. | | `--wm-panel-font` | `.8rem var(--wm-font-family)` | `.8rem system-ui, sans-serif` | Complete root font shorthand. | | `--wm-font-family` | literal | `system-ui, sans-serif` | Font-family fallback inside the default shorthand. | **Classes** `classNames` and descriptor-level classes are additive; canonical classes stay present. | Node | Canonical class | Global key | Descriptor key | |---|---|---|---| | Root | `.wui-section-panel` | `classNames.root` | — | | Section | `.wui-section-panel__section` | `classNames.section` | `section.classNames.section` | | Slot | `.wui-section-panel__slot` | `classNames.slot` | `section.classNames.slot` | **Parts** | Node | Canonical part | Global key | Descriptor key | |---|---|---|---| | Root | `root` | `parts.root` | — | | Section | `section` | `parts.section` | `section.parts.section` | | Slot | `slot` | `parts.slot` | `section.parts.slot` | Canonical, global and descriptor tokens are de-duplicated before the `part` attribute is written.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/score/element/play/score-synth-panel/) ## Related presenters - [`@webmusic/ui/stage`](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/stage/) - [`@webmusic/ui/status`](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/status/) - [`@webmusic/ui/workbench`](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/workbench/) --- # @webmusic/ui/stage > Generic retained and canvas render hosts with DPR, resize, animation, status, and surface-slider support. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/stage/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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/ui/stage` supplies three domain-neutral presentation lifecycles. `mountStage()` gives caller rendering a retained DOM surface, `mountCanvasStage()` owns canvas sizing and redraws, and `mountSurfaceSlider()` adds pointer and keyboard slider behavior to an existing element. Use [status]() for a standalone loading or failure surface and [analysis]() for ready-made analytical output. Stage is the lower level choice when the caller still owns the domain pixels or DOM. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/stage/) ## Import ```ts import {mountCanvasStage} from '@webmusic/ui/stage'; const handle = mountCanvasStage(host, { draw: ({context, width, height}) => drawWaveform(context, width, height), subscribe: (notify) => model.subscribe(notify), }); ``` ## Three mounting models `mountStage()` clears and re-runs `binding.render(surface)` on every update, calling the previous render cleanup first. If rendering, cleanup, or synchronous subscription notification mounts a replacement on the same host, the outgoing stage stops updating and immediately releases any cleanup returned after replacement. `mountCanvasStage()` retains one canvas, sizes its backing store for the device pixel ratio, and calls `draw(frame)` after resize, notification, explicit update, or every animation frame when `animate` is enabled. `mountSurfaceSlider()` creates no pixels and installs no stylesheet. It augments the supplied element with slider semantics and delegates domain hit-testing to `valueAt(point)`. `commit()` is the only write port. All three follow the [shared presenter ownership contract]().
    API The module exports three related contracts; the canvas presenter shown above has no snapshot State because its caller draws each frame directly. **Retained stage binding** | Member | Kind | Description | |---|---|---| | `render(surface)` | `(HTMLElement) => void \| (() => void)` | Required renderer. Its optional cleanup runs before the next render and during destruction. | | `subscribe?(notify)` | `(notify: () => void) => () => void` | Publishes when the retained surface must be rendered again. | **Retained stage options** | Member | Kind | Default | Description | |---|---|---|---| | `label` | `string` | — | Adds `role="region"` and an accessible name to the root. | | `fill` | `boolean` | `false` | Makes the stage and its surface fill a host with definite height. | | `surfaceMinWidth` | `string` | `0` | Minimum CSS width of the inner drawing. Combine with `overflow: 'auto'` to preserve readable content inside a narrower host. It does not resize the host. | | `background` | `string` | Stage theme (`transparent`) | CSS background color of the retained viewport. Applies behind the whole scroll viewport so long drawings retain their background after scrolling. | | `overflow` | `'hidden' \| 'auto' \| 'visible' \| 'clip' \| 'scroll'` | `hidden` | How the stage crops content that exceeds it. Scrolling is a mounting decision, not a theme, so it is an option rather than a `--wm-stage-overflow` write the caller has to know about. | | `classNames` | `StageClassNames` | `{}` | Adds compatibility classes for `root` and `surface`. | | `parts` | `StageParts` | `{}` | Adds part tokens for `root` and `surface`. | | `onError` | `(error: unknown) => void` | — | Receives render, subscription, and cleanup failures. | **Retained stage handle** | Member | Kind | Description | |---|---|---| | `element` | `HTMLElement` | Presenter-owned `.wui-stage` root. | | `surface` | `HTMLElement` | Stable `.wui-stage__surface` passed to `render()`. | | `update()` | `() => void` | Releases the prior render, clears the surface, and calls `render()` again. | | `destroy()` | `() => void` | Releases rendering and subscription resources, then removes the root and style. | **Surface-slider State** | Member | Kind | Description | |---|---|---| | `minimum` | `number` | Lower value bound; non-finite input becomes `0`. | | `maximum` | `number` | Upper bound, normalized to be at least `minimum`. | | `value` | `number` | Current value, clamped to the normalized bounds. | | `disabled?` | `boolean` | Strict `true` suppresses pointer and keyboard commits. | **Surface-slider binding** | Member | Kind | Description | |---|---|---| | `snapshot()` | `() => SurfaceSliderState` | Required authoritative slider state. | | `gesture?(phase)` | `('start' \| 'end' \| 'cancel') => void` | Pointer lifecycle: start before commands, end after the final release commit, cancel when pending work must be discarded. Keyboard commands do not start a pointer gesture. | | `valueAt(point)` | `(SurfaceSliderPoint) => number` | Required domain mapping. The point includes client/local coordinates, pointer id, bounds and surface. During a pointer gesture, `point.gesture` supplies the stable press origin and relative travel described below. | | `commit(value)` | `(number) => void` | Required write port receiving a finite, clamped pointer or keyboard value. | | `preview(value)` | `(number) => void` | Optional. With `commitOn: 'release'` this receives every in-flight drag position, so the caller can paint the scrub while the domain sees exactly one write. | | `subscribe?(notify)` | `(notify: () => void) => () => void` | Publishes authoritative State changes. | **Surface-slider options** | Member | Kind | Default | Description | |---|---|---|---| | `label` | `string` | `'Value'` | Accessible name when the element has neither `aria-label` nor `aria-labelledby`. | | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Sets ARIA orientation and Arrow-key direction. | | `pointerTarget` | `HTMLElement` | mounted element | Receives pointer listeners and capture while ARIA remains on the mounted element. | | `pointerMode` | `'absolute' \| 'drag' \| 'click' \| 'none'` | `'absolute'` | Absolute commits on press and movement. Drag waits for 4 CSS pixels of travel; a stationary release remains a click. Click commits only a stationary release. None retains keyboard/ARIA without pointer listeners or touch styling. | | `cursor` | `string` | — | Idle pointer cursor, such as `grab`. Ignored in pointer none mode. | | `dragCursor` | `string` | `cursor` | Cursor during an active pointer gesture, such as `grabbing`. | | `keyboardStep` | `number \| (state, event) => number` | span ÷ 100, or `1` | Amount committed for each Arrow key. The callback receives the keyboard event too, so a modifier can select a coarser step — the transport uses `Shift` for a long seek. | | `commitOn` | `'move' \| 'release'` | `'move'` | When a pointer drag reaches `commit()`. `'release'` previews the drag and commits once, on pointerup — one seek per scrub rather than one per move. A cancelled drag repaints from the owner's state and commits nothing. | | `formatValue` | `(value, state) => string \| undefined` | — | Supplies `aria-valuetext`; `undefined` removes it. | | `onError` | `(error: unknown) => void` | — | Receives snapshot, hit-test, commit, formatting, subscription, and pointer-capture failures. | **Surface-slider handle** | Member | Kind | Description | |---|---|---| | `element` | `HTMLElement` | Caller element carrying role, focus, and ARIA. | | `surface` | `HTMLElement` | Pointer surface: `pointerTarget` or the mounted element. | | `update()` | `() => void` | Re-reads State and applies ARIA values; disabling cancels an active gesture. | | `cancelGesture()` | `() => void` | Cancels active pointer work and discards a release-only preview. Idempotent; leaves keyboard and future gestures available. | | `destroy()` | `() => void` | Removes listeners and restores attributes and touch behavior that the presenter assigned. | A pointer point's optional `gesture` is a readonly snapshot with `startClientX`, `startClientY`, `startValue`, `deltaX`, `deltaY` and `moved`. Origins use the current State at press; movement uses CSS client pixels and stays independent of later layout, viewport or State changes. `moved` becomes true after 4 CSS pixels of travel and stays true even if the pointer returns to its origin. The presenter owns that geometry; the caller owns its units and direction. For example, an Audio caller can return `point.gesture.startValue - point.gesture.deltaX / zoom` from `valueAt` to move earlier as a track is dragged right. Snapshot the intended zoom at gesture start if it may change while dragging. Only one primary pointer owns a gesture. Additional pointers cannot steal it. Pointer cancellation, lost capture, host/window blur, disable, explicit `cancelGesture()`, destruction and same-host replacement cancel pending work. A cancel does not undo values already committed by a continuous gesture; it prevents a later release from committing and lets the caller discard queued work. Release-only preview always maps the actual release coordinates before its single commit. Continuous input commits a changed final coordinate without repeating an unchanged final point. Errors from borrowed callbacks reach `onError`; reentrant destruction stops remaining pointer commands. `commit()` is optimistic only until the owner publishes State: a synchronous `update()` during the callback preserves the owner's accepted value, including clamping or wraparound, instead of overwriting it with the requested value. A caller scheduling writes on animation frames can flush its last pending value in `gesture('end')` and drop it in `gesture('cancel')`; the presenter creates no musical clock or playback commands. Configured cursors are restored on gesture completion/cancellation and release. A caller replacing the inline cursor while mounted keeps that override. Pointer none mode never takes cursor or touch-action ownership. The presenter manages `tabindex` only when it assigned one: a disabled slider leaves the tab order and rejoins it when re-enabled. An author-supplied `tabindex` is that author's focus policy and is never rewritten. **Canvas binding and frame** | Member | Kind | Description | |---|---|---| | `draw(frame)` | `(CanvasStageFrame) => void` | Required drawing callback. The frame carries `context`, `canvas`, CSS-pixel `width` / `height`, `pixelRatio`, and frame `time`. | | `status?()` | `() => StatusState` | Supplies a ready, loading, empty, or error overlay. | | `subscribe?(notify)` | `(notify: () => void) => () => void` | Requests a redraw from the caller's source. | **Canvas options** | Member | Kind | Default | Description | |---|---|---|---| | `label` | `string` | — | Adds `role="img"` and an accessible name to the stage root. | | `animate` | `boolean` | `false` | Continuously redraws through `requestAnimationFrame`. | | `fallbackWidth` | `number` | `300` | CSS-pixel width used when the stage has no positive measured width. | | `fallbackHeight` | `number` | `150` | CSS-pixel height used when the stage has no positive measured height. | | `classNames` | `CanvasStageClassNames` | `{}` | Adds compatibility classes for `root`, `canvas`, and `status`. | | `parts` | `CanvasStageParts` | `{}` | Adds part tokens for `root`, `canvas`, and `status`. | | `onError` | `(error: unknown) => void` | — | Receives draw, status, subscription, resize, and cleanup failures. | | `stylesheet` | `boolean` | `true` | Installs the presenter's stylesheet unless explicitly `false`, for a light-DOM host that cannot inject CSS into its page. | **Canvas handle** | Member | Kind | Description | |---|---|---| | `element` | `HTMLElement` | Presenter-owned `.wui-stage--canvas` root. | | `canvas` | `HTMLCanvasElement` | DPR-sized drawing surface. | | `update()` | `() => void` | Redraws now and starts animation when configured. | | `redraw()` | `() => void` | Draws exactly one frame. | | `destroy()` | `() => void` | Cancels animation and resize observation, destroys status, and removes owned DOM. | **Accessibility.** A labelled retained stage is a named region. A labelled canvas stage is a named image while its canvas is decorative; a supplied status is exposed through the semantics documented by [status](). Surface sliders preserve pre-existing role, focus and naming attributes, expose min/max/now/disabled values, support Arrow keys plus Home and End, and restore only presenter-assigned attributes on destroy.
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. `mountStage()` and `mountCanvasStage()` install the shared stage stylesheet by default. Set `stylesheet: false` to omit it; the canvas stage also omits its nested status stylesheet. The host must then supply `stageStyle`, any needed `statusStyle`, or its own CSS. The presenter root uses the shared [`--wm-component-*` surface](), or `--wm-stage-surface-*` for a stage-only override. The `--wm-stage-*` properties below style the full-bleed inner drawing viewport. A surface slider installs no visual style because its element and pixels remain caller-owned. **Tokens** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-stage-background` | literal | `transparent` | Drawing background; `background` also covers the retained root scrollport. | | `--wm-stage-foreground` | `--wm-foreground` → literal | `#444` | Stage foreground context. | | `--wm-stage-border` | literal | `0` | Inner drawing-viewport border. | | `--wm-stage-radius` | `--wm-control-radius` | `0` | Inner drawing-viewport corner radius. | | `--wm-stage-overflow` | literal | `hidden` | Retained root scrollport behavior; canvas stages apply it to their inner viewport. | | `--wm-stage-surface-min-width` | literal | `0` | Minimum width of the retained inner drawing. `surfaceMinWidth` sets this token on the stage root. | A canvas status overlay mounts the status presenter's embedded variant. It fills the inner viewport without drawing a second box, while its message still accepts the relevant [`--wm-status-*` text tokens](). **Classes** | Node | Canonical class | Present when | `classNames` key | |---|---|---|---| | Root | `.wui-stage` | Always | `root` | | Fill modifier | `.wui-stage--fill` | Retained `fill: true` | — | | Retained surface | `.wui-stage__surface` | `mountStage()` | `surface` | | Canvas modifier | `.wui-stage--canvas` | `mountCanvasStage()` | — | | Canvas surface | `.wui-stage__surface.wui-stage__canvas-surface` | `mountCanvasStage()` | — | | Canvas | `.wui-stage__canvas` | `mountCanvasStage()` | `canvas` | | Status layer | `.wui-stage__status` | Canvas binding has `status()` | `status` | | Status mount host | `.wui-status-host` | Canvas binding has `status()` | — | **Parts** | Node | Canonical part | Present when | `parts` key | |---|---|---|---| | Root | `root` | Retained or canvas stage | `root` | | Retained surface | `surface` | `mountStage()` | `surface` | | Canvas | `canvas surface` | `mountCanvasStage()` | `canvas` adds tokens beside both canonical names | | Status layer | `status` | Canvas binding has `status()` | `status` |
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/score/element/view/score-view/) - [``](https://koperative-lab.github.io/WebMusic/score/element/view/score-sheet-view/) - [``](https://koperative-lab.github.io/WebMusic/audio/element/view/audio-view/) - [``](https://koperative-lab.github.io/WebMusic/audio/element/view/audio-live-view/) ## Related presenters - [`@webmusic/ui/panel`](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/panel/) - [`@webmusic/ui/status`](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/status/) - [`@webmusic/ui/workbench`](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/workbench/) --- # @webmusic/ui/status > Shared ready, loading, empty, and error feedback with live-region semantics. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/status/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountStatus()` paints caller-owned `StatusState` as a shared loading, empty, or failure surface. The `ready` state hides the surface, so a consumer can leave it mounted across the entire loading lifecycle without branching its DOM. Use it directly beside caller content, or supply the same State through [`CanvasStageBinding.status()`]() when feedback must overlay a canvas. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/status/) ## Import ```ts import {mountStatus} from '@webmusic/ui/status'; const handle = mountStatus(host, { snapshot: () => store.status, subscribe: (notify) => store.subscribe(notify), }); ``` ## State ownership The binding stays authoritative: notification and `handle.update()` both re-read `snapshot()`. Changing `kind` changes visibility and live-region behavior; changing `message` only changes the painted copy. The rest of mounting and cleanup follows the [shared presenter shape]().
    API **State** | Member | Kind | Description | |---|---|---| | `kind` | `'ready' \| 'loading' \| 'empty' \| 'error'` | Selects hidden ready, polite loading / empty, or assertive error behavior. Any other runtime value is treated as `ready`. | | `message?` | `string` | Overrides the built-in message for the selected kind. | Without `message`, loading says “Loading…”, empty says “Nothing to show.”, error says “Something went wrong.”, and ready has no text. **Binding** | Member | Kind | Description | |---|---|---| | `snapshot()` | `() => StatusState` | Required pull port read on mount, notification, and explicit update. | | `subscribe?(notify)` | `(notify: () => void) => () => void` | Optional publication source; its disposer runs during destruction. | **Options** | Member | Kind | Default | Description | |---|---|---|---| | `classNames` | `StatusClassNames` | `{}` | Adds compatibility classes for `root` and `message`. | | `parts` | `StatusParts` | `{}` | Adds part tokens for `root` and `message`. | | `onError` | `(error: unknown) => void` | — | Receives snapshot, subscription, and cleanup failures; errors thrown by the sink do not escape. | | `stylesheet` | `boolean` | `true` | Installs the presenter's stylesheet unless explicitly `false`, for a light-DOM host that cannot inject CSS into its page. | **Handle** | Member | Kind | Description | |---|---|---| | `element` | `HTMLElement` | Presenter-owned `.wui-status` root. | | `message` | `HTMLSpanElement` | Stable `.wui-status__message` text node. | | `update()` | `() => void` | Re-reads State and repaints visibility, semantics, and copy. | | `destroy()` | `() => void` | Idempotently releases the subscription and removes owned root and style nodes. | **Accessibility.** Loading and empty states use `role="status"` with polite announcements; loading additionally sets `aria-busy="true"`. Error uses `role="alert"` with assertive announcements. Ready removes live-region attributes and hides the root. The presenter never moves focus and does not mutate unrelated caller DOM.
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. The root uses the shared [`--wm-component-*` surface](), or `--wm-status-surface-*` for a status-only override. The older status-specific box tokens below remain compatibility fallbacks when no shared surface token overrides them. When a stage mounts the presenter as its loading or empty overlay, it adds `.wui-status--embedded`: that variant fills the inner viewport but removes its own background, border, padding, and radius so the stage remains the only outer frame. Long messages wrap within the available width, including unbroken URLs and error identifiers. **Tokens** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-status-height` | literal | `3rem` | Minimum surface height. | | `--wm-status-padding` | literal | `.6rem` | Internal spacing compatibility fallback. | | `--wm-status-border` | literal | `1px solid #d8d8d8` | Normal border compatibility fallback. | | `--wm-status-radius` | `--wm-control-radius` → literal | `0` | Corners. | | `--wm-status-background` | `--wm-surface` → literal | `#fff` | Surface background compatibility fallback. | | `--wm-status-foreground` | `--wm-foreground-muted` → literal | `#777` | Loading and empty text. | | `--wm-status-error` | `--wm-danger` → literal | `#b42318` | Error text; error state changes the existing border to solid without replacing its color. | | `--wm-font-family` | literal | `system-ui, sans-serif` | Message typeface. | **Classes** | Node | Canonical class | `classNames` key | |---|---|---| | Root | `.wui-status` | `root` | | Embedded modifier | `.wui-status--embedded` | `root` | | Message | `.wui-status__message` | `message` | The root also carries `data-kind="ready|loading|empty|error"` for state-aware styling. **Parts** | Node | Canonical part | `parts` key | |---|---|---| | Root | `root` | `root` | | Message | `message` | `message` | `classNames` and `parts` append hooks beside these canonical names; they never replace them.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/score/element/view/score-view/) - [``](https://koperative-lab.github.io/WebMusic/score/element/view/score-sheet-view/) - [``](https://koperative-lab.github.io/WebMusic/audio/element/view/audio-view/) - [``](https://koperative-lab.github.io/WebMusic/audio/element/view/audio-live-view/) ## Related presenters - [`@webmusic/ui/panel`](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/panel/) - [`@webmusic/ui/stage`](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/stage/) - [`@webmusic/ui/workbench`](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/workbench/) --- # @webmusic/ui/workbench > The shell an analysis view sits in: an optional tablist, a stage, a rail of docks and a status bar, plus the one reading of the clock per frame that everything mounted inside it shares. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/workbench/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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/ui/workbench` is one mount, `mountWorkbench`, and it draws no content at all. It draws a frame around content: a header, an optional tablist, a stage, a rail of docks, a strip under them and a status bar — and then hands the caller `handle.stage`, `handle.dock(id)` and `handle.index` as **hosts** and never looks inside any of them again. That is what lets one shell serve six unrelated views. A view is `{id, label}` and a dock is `{id, label, placement}`, so the word for what a dock *contains* does not appear anywhere in the module, and it could not: a shell that reached into [pitch]() or [harmony]() would make the whole kit one chunk. The shell also supplies a **presentation clock**. It subscribes to the shared frame loop used by participating harmony surfaces, calls `binding.now()` once per frame, and hands every subscriber the same `FrameTick` through `handle.clock`. The caller owns the authoritative playback position, rate and seek commands; this clock distributes that caller's reading and does not schedule audio or inject a shared transport into playback engines. The frame loop runs only while the shell has a clock subscriber, a `binding.now` implementation, a `playing` or `listening` phase, and resolved `continuous` motion. With `motion: 'auto'`, an ancestor's `data-motion` and then the reduced-motion preference determine the motion mode. Explicit modes override that resolution. Without a continuous loop, caller notifications and `tick()` still publish a reading to subscribers. The rule the module lives or dies by is that **`update()` is idempotent and never rebuilds a slot node**. Every dock holds a presenter that took that node, and rebuilding the node unmounts it silently. So the stage, the index and every dock body are created once and afterwards only ever have attributes written on them; tabs and dock sections are reconciled by id and reordered rather than replaced. Under a clock that rule gains a sharper meaning — an `update()` called per frame would unmount every child presenter sixty times a second — so the frame path and the snapshot path never meet: the clock only ever calls `tick()`. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/workbench/) ## Import ```ts import {mountFlowLane} from '@webmusic/ui/harmony'; import {mountKeyboard} from '@webmusic/ui/pitch'; import {mountWorkbench} from '@webmusic/ui/workbench'; const shell = mountWorkbench( host, { // Content. Pulled on domain events — never per frame. snapshot: () => ({ views: [ {id: 'chords', label: 'Chords', docks: ['keyboard']}, {id: 'key', label: 'Key', docks: []}, ], activeViewId: store.view, docks: [{id: 'keyboard', label: 'Keyboard', placement: 'rail'}], dockVisibility: {keyboard: store.keyboardOpen}, title: 'Analysis', density: 'comfortable', phase: store.playing ? 'playing' : 'idle', // `message` is the live region; `detail` deliberately is not. status: {message: 'Following the performance.', detail: `bar ${store.bar}`}, }), // THIS FRAME'S ONE POSITION, in the caller's own units, on an axis the // shell has never been told the meaning of. now: (frame) => store.positionAt(frame.time), epoch: () => store.seekCount, activateView: (id) => store.show(id), toggleDock: (id, next) => store.fold(id, next), subscribe: (notify) => store.subscribe(notify), }, { label: 'The analysis workbench', // The shell can refuse to hand out a hidden slot; it cannot evict a tenant // it does not own. This is where it tells you to. onDockVisibility: (id, visible) => { if (!visible) tenants.get(id)?.destroy(); }, }, ); // The stage is a HOST. The lane takes the shell's clock and becomes a // passenger on it rather than opening a second loop over the same axis. const lane = mountFlowLane(shell.stage, laneBinding, {clock: shell.clock, surface: 'none'}); // `undefined` means folded away, not asked for by this view, or the shell is // destroyed. Do not mount: a presenter in a box nobody can see still runs, and // still holds the clock. const rail = shell.dock('keyboard'); if (rail) tenants.set('keyboard', mountKeyboard(rail, keyboardBinding)); shell.index.replaceChildren(...rows); // the caller's list. The shell never empties it. shell.clock.read().now; // this frame's reading, already taken shell.tick(); // drive one frame by hand shell.update(); // re-read the snapshot. The clock never calls this. shell.destroy(); ```
    API One mount, following the kit's shape: `mountWorkbench(host, binding, options?)`, a binding that answers `snapshot()` and may offer `subscribe()`, and a handle whose `destroy()` is idempotent. Mounting twice on one host replaces the first shell, and the replaced shell's slots go with it. | Export | Kind | Description | |---|---|---| | `mountWorkbench(host, binding, options?)` | `WorkbenchHandle` | The shell. | | `workbenchStyle` | `string` | The stylesheet it installs, as side-effect-free text. | `FrameClock`, `FrameTick` and `MotionMode` are re-exported here, so a caller wiring a slot to `handle.clock` needs no second import. **State** | `WorkbenchState` | Kind | Default | Description | |---|---|---|---| | `views` | `readonly WorkbenchView[]` | — | `{id, label, description?, docks?}`. `docks` is the ids this view wants; omit it and the view takes every declared dock. | | `activeViewId` | `string` | the first view | Which tab is selected. | | `docks` | `readonly WorkbenchDock[]` | — | `{id, label, placement?, toggleable?}`. Required — a `bare` caller passes `[]`. **Stable for the life of the mount:** a dock that leaves this array takes its slot node away, and with it whatever the caller mounted inside, so the shell reports that through `onError` rather than losing it in silence. `WorkbenchView.docks` is how a view offers fewer of them. | | `dockVisibility` | `Record` | all visible | Which docks are folded open. `false` folds the body away and `dock(id)` stops answering at the same instant. | | `title` / `subtitle` | `string` | — | The identity block, hidden when both are absent. | | `density` | `'comfortable' \| 'compact'` | `'comfortable'` | Sets the shell's density and inheritable tokens. Pitch tenants with their own token roots need an explicit `density` option and remount when that option changes. | | `scheme` | `'light' \| 'dark'` | inherit | Declared rather than sniffed: a shell in a dark card on a light page is a fact its host knows and it does not. | | `phase` | `WorkbenchPhase` | `'idle'` | `'idle' \| 'listening' \| 'playing' \| 'empty' \| 'error'`. Published as `data-phase`, **and it gates the clock**. | | `status` | `{message?, detail?}` | — | `message` is a live region; `detail` is not. | `placement` is `'rail'` or `'strip'` — a position on the grid, not a kind of thing. `toggleable: false` (or a binding with no `toggleDock`) removes the switch and leaves the section open. **Phase gates the loop, and that is not a style preference.** Only `playing` and `listening` run a frame loop. Everything else is a still shell that still answers the pointer and the keyboard — parked is not blank, and a workbench you can drag through is more alive than one spinning a loop over nothing. `empty` and `error` additionally print `status.message` over the stage. | `WorkbenchBinding` | Description | |---|---| | `snapshot()` | Content. Pulled on domain events, **not** per frame. | | `now?(frame)` | This frame's one position, in the caller's own units. Omitting it means the shell runs no frame loop at all. It takes `{time, delta}` and returns a number, so interpolation between sparse samples stays with the only layer that knows the rate. | | `epoch?()` | A discontinuity counter, passed straight through to `FrameTick.epoch`. A presenter snaps when it changes and interpolates when it does not, which is what stops a backwards seek being tweened across material nobody heard. | | `activateView?(id)` | A tab was chosen. | | `toggleDock?(id, next)` | A dock's switch was thrown. Its presence is what makes the switches exist. | | `subscribe?(notify)` | | | `WorkbenchOptions` | Kind | Default | Description | |---|---|---|---| | `chrome` | `'full' \| 'bare'` | `'full'` | `bare` is the unframed stage alone — no default border or padding, no tabs, no docks, no status bar — which is how a sibling element wraps the card it already has and inherits this skin. `dock(id)` answers `undefined` for every id. | | `navigation` | `boolean` | `true` | Set `false` to omit view tabs in an application-owned layout; full-chrome dock controls and status remain available. The stage uses a named region instead of tab-panel semantics. | | `motion` | `MotionMode` | `'auto'` | `'auto'` follows `prefers-reduced-motion` **live**: an OS toggle lands at once, because no media query can stop a transform a frame loop is writing. | | `label` | `string` | — | Names the shell as a `role="group"` and names its tablist. | | `classNames` / `parts` | | — | `{root?, header?, tablist?, tab?, stage?, rail?, strip?, status?, index?}`. `parts` additionally accepts `frame?` for the painted box and `note?` for its empty/error sentence. | | `onDockVisibility` | `(id, visible) => void` | — | A dock started or stopped answering — `dock(id)` flipping between a node and `undefined`, whatever the reason. Reports only a **change**, and never before `mountWorkbench` has returned. | | `onError` | `(error) => void` | — | A faulty snapshot, a throwing `now()` or `epoch()`, a dock dropped with a presenter still in it, and anything a slot's own draw callback throws. | | `stylesheet` | `boolean` | `true` | Install the exported sheet, or paint the identical declarations onto the nodes. | **Handle** | `WorkbenchHandle` | Description | |---|---| | `element` | The shell's root. | | `stage` | The current view's mount point. **Never rebuilt.** | | `dock(id)` | A dock's mount point, or `undefined` for an unknown id, a dock this view did not ask for, one the viewer folded away, and every id under `bare` chrome. | | `index` | A visually hidden `
      `, a **direct child of the host and a sibling of the shell's frame**. Its children are the caller's, emptied by nobody. | | `statusMessage` | The live region's own node, for a caller that would rather write it than snapshot it — and `undefined` under `bare` chrome, which builds no status bar. A detached node would be worse than none: text written to it disappears with no error at all. | | `clock` | `{subscribe(draw), read()}` — the one clock every slot in this shell reads. | | `tick()` | Drive one frame by hand. The deterministic seam a test drives instead of waiting on a real rAF. | | `update()` | Re-read the snapshot and reconcile. **The clock never calls this.** | | `destroy()` | Idempotent. Leaves none of **its own** — no armed frame, no media listener, no node — and `dock(id)` answers `undefined` from then on. It cannot take down a tenant it does not own; that is what `onDockVisibility` and your own teardown are for. | **Why `dock(id)` refuses, and why that is only half of it.** A caller must not be able to mount into something nobody can see: the presenter would run, take a clock subscription and paint into a hidden box for the life of the page. But the refusal covers what has *not* been mounted yet — a tenant already in a dock that folds keeps its subscription and goes on drawing, and the shell does not own it, so it cannot evict it. That is what `onDockVisibility` is for: the shell says the slot closed, and the caller takes the tenant down. The demo above does exactly that — fold the keyboard and the keyboard presenter is destroyed; open it and a fresh one is mounted. What folds is the **body only**. The head and its switch stay, because hiding the switch along with the thing it switches leaves the viewer no way back — unless there was never a switch to begin with (`toggleable: false`, or a binding with no `toggleDock`), in which case the whole section goes, since a label over nothing with no control to open it is not a way back either. **Accessibility.** The tablist is `role="tablist"` with a roving tabindex and **manual** activation: arrows move focus and choose nothing, `Enter` and `Space` activate, `Home` and `End` go to the ends, and the strip wraps. Automatic activation would run one analysis projection per tab as a viewer sweeps across the strip — three of them between two key repeats, and choosing the view already on screen is not an activation at all. The stop follows the focused tab while the strip has focus and snaps back to the selected one the moment focus leaves, so tabbing in lands on the view actually there; a view arriving mid-sweep does not move the focus. Focus stays on the tab after an activation; the panel is reachable with `Tab` and carries a tab stop of its own, because a panel whose only content is a caller's presenter may have nothing focusable in it at all. Each dock's switch is `role="switch"` with `aria-checked`, named by its dock label — and where the binding has no `toggleDock` to receive the answer it is hidden **and** disabled, because a switch a reader can reach, is told is on, and throws to no effect is worse than no switch. The status message is the shell's **one** live region; `detail` carries none, because a caller is expected to put a position there and a polite region changing at that rate would read the piece out loud for as long as it lasts.
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. The shell installs `workbenchStyle`, or paints the same declarations onto the nodes when `stylesheet: false`. Both paths are generated from one record. Every colour is a chain ending in a literal, never a bare `var()`: a bare custom property is invalid at computed-value time on a host carrying no tokens, which paints a control invisible — the one failure a colour system may not have. **The layout is container queries, never a media query.** This shell is routinely put in a 16rem-tall box on a documentation page; the viewport's width is not a fact about it, and asking the viewport gives a two-column shell inside a 300px column. The root is the query container (`container-name: wui-workbench`) and an inner `.wui-workbench__frame` carries the grid, because an element cannot answer a query about itself. Below 720px the rail drops under the stage and becomes a horizontal shelf — the same docks, the same order, laid along the other axis, and nothing dropped, because a control that vanishes when the box is small is a control the viewer cannot find. Below 440px the identity block goes and the status bar wraps. **720 and 440 are widths of the container's content box**, which is why the card's border and padding sit on the frame and not on the root. A padded query container fires every breakpoint late, and by an amount that moves whenever a host retunes `--wui-harmony-pad` — a breakpoint that depends on a spacing token is a breakpoint nobody can document. Those two queries are the **documented cost of `stylesheet: false`**, alongside pseudo-state and focus rules. A declaration record has one slot per property, so the collapsed grids are literal CSS. But `data-rail="false"` collapsing to one column is not a responsive rewrite — it is the shell saying what is *true* — so the inline path writes that grid itself, from the same tuple, rather than leaving a permanent empty rail column; with the sheet on, `data-rail` does it with no JavaScript touching a style at all. The token layer is painted on both paths too, for the shell and descendants that inherit those tokens. **The shell carries inheritable tokens.** Its sheet opens with the shared harmony token block. Pitch presenters also install a local token layer for standalone use: forward the shell's density to their mount options and remount those tenants when it changes. The demo does this for the keyboard and staff. Their default `scheme: 'inherit'` follows the containing shell. For motion, pitch and harmony surfaces with `motion: 'auto'` ask their nearest ancestor's `data-motion` first. Explicit child motion options can override that inherited choice. The public `frame` part (customizable with `parts.frame`) carries background, border, padding and radius through `--wm-workbench-surface-background`, `-border`, `-padding`, `-radius` and then the shared `--wm-component-*` quartet. Bare chrome has zero border/padding defaults but still accepts explicit surface tokens. Only the frame paints a background, so alpha is not doubled. Use `surface: 'none'` on nested Harmony/Pitch mounts when this frame owns their surface. New palettes use `--wm-harmony-foreground`, `--wm-harmony-muted`, `--wm-harmony-background`, `--wm-harmony-accent`, `--wm-harmony-on-accent`, `--wm-harmony-rule` and `--wm-harmony-track`, which reach the shared semantic palette. Accent text is independent from the background. **Control geometry** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-workbench-height` | `--wm-control-size` → `--wui-row-h` | `2rem` | Minimum tab height and dock-switch height. Switch width scales with its height. | | `--wm-workbench-radius` | `--wm-control-radius` | `0` | Tab and switch corners. | | `--wm-workbench-thumb-radius` | `--wm-control-radius` | `0` | Dock-switch thumb corners. | **Compatibility and state tokens** The older `--wui-*` names below remain supported where previously documented. They are implementation/state values; prefer the public surface and palette tokens for new themes. | Property | Default | Controls | |---|---|---| | `--wui-workbench-rail` | `clamp(228px, 28%, 312px)` | The rail's width. A percentage of the **shell**, which is why it is a `clamp` and not a breakpoint. | | `--wui-workbench-dock-min` | `240px` | The width a dock is never squeezed below once the rail has become a shelf. Below it the shelf scrolls, which is the honest answer to *these do not fit*. | | `--wui-harmony-accent` / `--wui-harmony-on-accent` | ink / independent contrast ink | The selected tab, the switches and the phase dot. | | `--wui-harmony-surface` / `--wui-harmony-line` / `--wui-harmony-track` | transparent / rule / bed | The shell's ground, its rules, and a switch's unlit track. | | `--wui-harmony-ink` / `--wui-harmony-ink-muted` | inherit / muted | Text, and the subtitle, dock labels and status detail. | | `--wui-harmony-font` / `--wui-harmony-font-mono` | inherit | Prose, and the fixed-width status detail. | | `--wui-harmony-size-title` / `-body` / `-label` / `-micro` | `1rem` … `.7rem` | The type ladder, resized as one by `density`. | | `--wui-harmony-space-1` / `-space-2` | `4px` / `8px` | The gaps. | | `--wui-harmony-radius` | `0` | Dock and decorative surface corners; tabs and switches use the control tokens above. | | `--wui-harmony-motion-ease` / `--wui-harmony-motion-chip` | `ease-out` / `120ms` | The tab and switch transitions. | | `--wui-harmony-flow-now` | accent | The line in the empty-stage skeleton, shared with the flow lane so the shell's placeholder and the surface that replaces it stand in the same place. | | `--wui-harmony-border` / `--wui-harmony-pad` | rule / `.75rem` | The card's box, declared on the frame rather than on the query container. | **Classes and state attributes** | Node | Class | Carries | |---|---|---| | Root / grid | `.wui-workbench`, `.wui-workbench__frame` | `data-chrome`, `data-motion`, `data-phase`, `data-density`, `data-scheme`, `data-view`, `data-rail` on the root. | | Header | `.wui-workbench__header`, `__identity`, `__title`, `__subtitle` | Hidden as a block when neither title nor subtitle is given. | | Tabs | `.wui-workbench__tablist`, `.wui-workbench__tab` | `data-view` and `aria-selected` on a tab. | | Panel and stage | `.wui-workbench__main`, `__stage`, `__note` | The note carries `data-phase` and appears only for `empty` and `error`. An `:empty` stage in `idle` or `listening` draws a faint ruler and a dimmed line where material will arrive — pure CSS, no node and no tick, gone the instant anything is mounted into it. A workbench that renders literally nothing is the one state a reader cannot tell from a mistake. | | Docks | `.wui-workbench__rail`, `__strip`, `__dock`, `__dock-head`, `__dock-label`, `__dock-toggle`, `__dock-knob`, `__dock-body` | `data-dock` and `data-visible` on a section; `aria-checked` on the switch. | | Status | `.wui-workbench__status`, `__dot`, `__message`, `__detail` | The dot's colour comes from the root's `data-phase`. | | Semantic twin | `.wui-workbench__index` | An `
      ` outside the frame. Its children, and their `aria-current`, are the caller's. | The one animation in the shell is `wui-workbench-breathe`, a pulse on the dot while `phase` is `listening`. It is deliberate: listening is a state with no material to move, and a pulse is the only honest way to say *still waiting* without pretending to be a position. Every other transition here is a colour change measured in `--wui-harmony-motion-chip`. The `parts` option adds `part` tokens for a shadow-DOM host; `classNames` adds classes without replacing the structural ones. Neither is required.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/score/element/analyze/score-chord-analysis/) - [``](https://koperative-lab.github.io/WebMusic/score/element/analyze/score-live-chord-analysis/) ## Related presenters - [`@webmusic/ui/panel`](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/panel/) - [`@webmusic/ui/stage`](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/stage/) - [`@webmusic/ui/status`](https://koperative-lab.github.io/WebMusic/uikit/layout-feedback/status/) --- # @webmusic/ui/meter > Renders a level or spectrum meter by pulling normalized samples from a structural binding. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/mixing-capture/meter/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountMeter()` renders either one horizontal level with a held-peak marker or a bank of spectrum bars. Unlike snapshot presenters, it pulls current values from `MeterBinding` on its own animation frame; the caller keeps the audio graph and sample buffers. Use [mixer]() when the surface must also change channel gains, or [recorder]() when a compact input level belongs to a capture workflow. Mount this presenter directly for a custom graph, scheduler, or containing layout. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/mixing-capture/meter/) ## Import ```ts import {mountMeter} from '@webmusic/ui/meter'; const handle = mountMeter(host, { readLevel: () => levels.current, readSpectrum: (bars) => analyser.readNormalizedBins(bars), }); ``` ## Pulling and redraw Meter deliberately has no `snapshot()` or `subscribe()`. With `animate !== false`, it schedules an animation frame, calls `redraw()`, and schedules the next frame until destroyed. Level mode calls `readLevel()`; spectrum mode calls `readSpectrum(barCount)` and reads one array-like value per created bar. Binding values are clamped to `0…1`, and missing spectrum entries become `0`. With `animate: false`, mount performs one synchronous redraw. Call `handle.redraw()` whenever new data is available. Mode and bar count determine the DOM at mount time and require a remount to change; only the accessible label has an in-place `updateLabel()` method. The rest of the host and lifecycle behavior follows the [shared presenter shape]().
    API `mountMeter(host, binding, options?)` accepts an `HTMLElement` or `ShadowRoot` and returns a `MeterHandle`. Non-finite samples become `0`; every sample is then clamped to `0…1`. **State returned by `readLevel()`** | Member | Kind | Description | |---|---|---| | `level` | `number` | Required current fill. It also supplies the accessible percentage. | | `peak?` | `number` | Optional instantaneous peak used when `peakHold` is absent. | | `peakHold?` | `number` | Optional decaying held-peak position. Fallback order is `peakHold ?? peak ?? level`. | **Binding** | Member | Kind | Description | |---|---|---| | `readLevel()` | `() => MeterLevelState` | Required pull port read on every level-mode redraw. Throws are sent to `onError` and leave the last successful paint in place. | | `readSpectrum(bars)` | `(number) => ArrayLike` | Required pull port read on every spectrum redraw. Receives the normalized mount-time bar count. The largest painted bar drives the accessible value. | **Options** | Member | Kind | Default | Description | |---|---|---|---| | `mode` | `'level' \| 'spectrum'` | `'level'` | Builds the track/fill/peak structure or the spectrum/bar structure. | | `bars` | `number` | `28` | Spectrum bar count. Values are floored with a minimum of `4`; non-finite values fall back to `28`. | | `label` | `string` | `'Audio level'` / `'Spectrum'` | Root accessible name; its default follows mode. | | `animate` | `boolean` | `true` | `false` suppresses the frame loop and performs one initial redraw. | | `height` | `number \| string` | — | Inline height override. Numbers become non-negative pixel lengths; strings are assigned as supplied. | | `color` | `string` | — | Writes inline `--wui-meter-fill`, ahead of stylesheet tokens. | | `peakColor` | `string` | — | Writes inline `--wui-meter-peak`. It has no visible effect in spectrum mode. | | `backgroundColor` | `string` | — | Writes inline `--wui-meter-background`. | | `classNames` | `MeterClassNames` | `{}` | Adds compatibility classes for `root`, `track`, `fill`, `peak`, `spectrum`, and `bar`. | | `parts` | `MeterParts` | `{}` | Adds part tokens for the same six node kinds. | | `onError` | `(error: unknown) => void` | — | Receives binding-read and cleanup errors. | | `stylesheet` | `boolean` | `true` | Installs the presenter's stylesheet unless explicitly `false`, for a light-DOM host that cannot inject CSS into its page. | **Handle** | Member | Kind | Description | |---|---|---| | `element` | `HTMLElement` | Presenter-owned `.wui-meter` root. | | `redraw()` | `() => void` | Pulls the active binding method and updates geometry and ARIA; becomes a no-op after destruction. | | `updateLabel(label?)` | `(string?) => void` | Updates `aria-label` in place; `undefined` restores the mode-specific default. | | `destroy()` | `() => void` | Idempotently cancels the outstanding frame and removes the root and installed style node. Cleanup errors are reported after all cleanup is attempted. | **Accessibility.** The root has `role="meter"`, the configured or mode-default name, `aria-valuemin="0"`, and `aria-valuemax="100"`. Each redraw sets integer `aria-valuenow`; level mode exposes `% level` and spectrum mode exposes `% spectrum peak` through `aria-valuetext`. The meter is read-only and has no keyboard interaction. In forced-colors mode, the root gains a `CanvasText` border, fill uses `Highlight`, and the peak uses `CanvasText`.
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. Runtime option values win through inline `--wui-meter-height`, `--wui-meter-fill`, `--wui-meter-peak`, and `--wui-meter-background`. Without them, height, background, fill, track, and peak first accept legacy `--wameter-*` aliases where shown, then semantic `--wm-*` tokens and literals. The root also follows the shared [`--wm-component-*` surface](), with `--wm-meter-surface-*` available for a meter-only outer override. The mount installs `meterStyle` by default. Set `stylesheet: false` to omit it and supply the exported stylesheet or your own CSS from the host. **Tokens** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-meter-height` | after `--wameter-height` | `48px` | Root height when `height` is absent. | | `--wm-meter-padding` | literal fallback | `.6rem` | Inset around the track or spectrum. | | `--wm-meter-radius` | `--wm-control-radius` | `0` | Root and track radius. | | `--wm-control-radius` | literal fallback | `0` | Shared radius fallback. | | `--wm-meter-background` | `--wm-surface` after `--wameter-bg` | `#fff` | Root-background compatibility fallback. | | `--wm-surface` | literal fallback | `#fff` | Shared background fallback. | | `--wm-meter-fill` | `--wm-accent` after `--wameter-fill` | `#999` | Level fill and spectrum bars through current color. | | `--wm-accent` | literal fallback | `#999` | Shared fill fallback. | | `--wm-meter-track` | `--wm-surface-muted` after `--wameter-track` | `#f3f3f3` | Level-mode track background. | | `--wm-meter-peak` | `--wm-danger` after `--wameter-peak` | `#e0445b` | Level-mode peak marker. | | `--wm-danger` | literal fallback | `#e0445b` | Shared peak fallback. | | `--wm-meter-gap` | literal fallback | `2px` | Gap between spectrum bars. | **Classes** `classNames` adds compatibility classes beside these canonical names; it never replaces them. | Node | Canonical class | Present when | `classNames` key | |---|---|---|---| | Root | `.wui-meter` | Always | `root` | | Track | `.wui-meter__track` | Level mode | `track` | | Fill | `.wui-meter__fill` | Level mode | `fill` | | Peak marker | `.wui-meter__peak` | Level mode | `peak` | | Spectrum bank | `.wui-meter__spectrum` | Spectrum mode | `spectrum` | | Spectrum bar | `.wui-meter__bar` | Once per bar in spectrum mode | `bar` | **Parts** `parts` adds tokens beside the canonical part and removes duplicate tokens. | Node | Canonical part | Present when | `parts` key | |---|---|---|---| | Root | `root` | Always | `root` | | Track | `track` | Level mode | `track` | | Fill | `fill` | Level mode | `fill` | | Peak marker | `peak` | Level mode | `peak` | | Spectrum bank | `spectrum` | Spectrum mode | `spectrum` | | Spectrum bar | `bar` | Once per bar in spectrum mode | `bar` |
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-meter/) - [``](https://koperative-lab.github.io/WebMusic/audio/element/view/audio-view/) ## Related presenters - [`@webmusic/ui/mixer`](https://koperative-lab.github.io/WebMusic/uikit/mixing-capture/mixer/) - [`@webmusic/ui/recorder`](https://koperative-lab.github.io/WebMusic/uikit/mixing-capture/recorder/) --- # @webmusic/ui/mixer > Renders master and channel faders and routes level, mute, solo, and optional transport commands through a structural binding. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/mixing-capture/mixer/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountMixer()` renders a master fader followed by caller-supplied channel strips. It paints `MixerState`, exposes mute and solo only when their binding commands exist, and adds Play, Pause, and Stop only for the corresponding optional commands. The board fits its container. The master remains beside a horizontally scrollable channel bank, with room around its controls for focus outlines. Channel names wrap instead of being truncated; unusually long words also wrap within the label. No viewport breakpoint, resize subscription or playback restart is involved. Use [meter]() for read-only realtime levels or [recorder]() for capture workflow controls. The [``]() and [``]() elements compose this presenter; mount it directly when the caller owns channel state or needs the strips inside another layout. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/mixing-capture/mixer/) ## Import ```ts import {mountMixer} from '@webmusic/ui/mixer'; const handle = mountMixer(host, { snapshot: () => store.mixer, setMaster: (value) => engine.setMasterGain(value), setChannel: (id, value) => engine.setChannelGain(id, value), subscribe: (notify) => store.subscribe(notify), }); ``` ## Binding and command settlement The caller owns channel identity, order, values, and pressed state. A fader input or button calls the matching command; `subscribe()` or `handle.update()` then causes the presenter to pull a new authoritative snapshot. See the [shared presenter shape]() for the common host, replacement, and cleanup contract. All level, mute, solo, and transport commands may return a promise. The presenter updates once that promise fulfills; a rejection goes to `onError`. A synchronous command that changes caller-owned state should therefore publish through `subscribe()` so other mounted consumers also repaint. Optional commands control structure as well as behavior: omitting all three transport commands removes the transport row, while omitting both `setMuted` and `setSolo` removes each channel's action row.
    API `mountMixer(host, binding, options?)` accepts an `HTMLElement` or `ShadowRoot` and returns a `MixerHandle`. Fader values are clamped to `0…1`; a non-finite value paints as `0`. **State** | Member | Kind | Description | |---|---|---| | `master` | `number` | Required normalized master value painted by the first fader. The master inherits `state.disabled`. | | `channels` | `readonly MixerChannel[]` | Required ordered channel records. Updates reuse surviving strips by id, preserving drag and focus state. | | `disabled?` | `boolean` | Strict `true` disables all faders, channel actions, and optional transport buttons. | Each `MixerChannel` has this shape: | Member | Kind | Description | |---|---|---| | `id` | `string` | Stable command identifier and the strip's `data-channel-id` value. | | `label` | `string` | Visible wrapping label, full `title`, and prefix for the fader and action accessible names. | | `value` | `number` | Normalized fader value and fill percentage. | | `disabled?` | `boolean` | Strict `true` disables this channel's fader and buttons; global disabled state also wins. | | `muted?` | `boolean` | Strict `true` paints the M button pressed when `setMuted` exists. | | `solo?` | `boolean` | Strict `true` paints the S button pressed when `setSolo` exists. | **Binding** | Member | Kind | Description | |---|---|---| | `snapshot()` | `() => MixerState` | Required pull port read on mount, notification, command fulfillment, and explicit `update()`. Errors are reported without replacing the last successful DOM. | | `setMaster(value)` | `(number) => void \| Promise` | Required command receiving the master range's normalized value. | | `setChannel(id, value)` | `(string, number) => void \| Promise` | Required command receiving a channel id and its normalized range value. | | `setMuted?(id, muted)` | `(string, boolean) => void \| Promise` | Optional command. Its presence creates an M button on every non-master channel. | | `setSolo?(id)` | `(string \| null) => void \| Promise` | Optional command. Pressing an unselected S sends the id; pressing the selected S sends `null`. | | `play?()` | `() => void \| Promise` | Optional command whose presence creates the Play button. | | `pause?()` | `() => void \| Promise` | Optional command whose presence creates the Pause button. | | `stop?()` | `() => void \| Promise` | Optional command whose presence creates the Stop button. | | `subscribe?(notify)` | `(notify: () => void) => () => void` | Optional subscription. Its disposer is called during destroy or same-host replacement. | **Options** | Member | Kind | Default | Description | |---|---|---|---| | `classNames` | `MixerClassNames` | `{}` | Adds compatibility classes for `root`, `transport`, `board`, `channels`, `strip`, `master`, `fader`, `input`, `label`, `actions`, `button`, `mute`, `solo`, `play`, `pause`, and `stop`. `fader` and `input` both reach the one fader node. | | `parts` | `MixerParts` | `{}` | Adds tokens with the same keys beside the presenter's canonical part tokens. | | `onError` | `(error: unknown) => void` | — | Receives snapshot and command errors. Subscription registration/disposal and DOM cleanup errors escape to the caller. An exception thrown by this callback is not caught. | | `stylesheet` | `boolean` | `true` | Installs the presenter's stylesheet unless explicitly `false`, for a light-DOM host that cannot inject CSS into its page. | | `master` | `boolean` | `true` | Render the master strip; `false` leaves only the channel bank. | **Handle** | Member | Kind | Description | |---|---|---| | `element` | `HTMLElement` | Presenter-owned `.wui-mixer` root. | | `update()` | `() => void` | Re-reads state, repaints surviving controls and reconciles channel membership/order; becomes a no-op after destruction. | | `destroy()` | `() => void` | On its first call, marks the handle destroyed, calls the subscription disposer, then removes root and style; later calls are no-ops. A throwing disposer escapes before DOM removal. | **Accessibility.** Every fader is a vertical ARIA slider named ` volume`. Mute and solo are native buttons named `Mute
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. The installed stylesheet resolves presenter-specific tokens through shared `--wm-*` tokens and then literals. The strip's own `--wm-mixer-*` values are handed to the shared fader's `--wm-fader-*`, which paints the track; `--wui-fader-fill` is the presenter-owned fill percentage written on the control, not a theme input. Mounts install `mixerStyle` unless `stylesheet: false`; callers opting out supply that stylesheet or their own layout and state styling. `destroy()` removes only the style node installed by that mount. **Tokens** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-mixer-height` | `--wm-control-size` → `--wui-row-h` | `2rem` | Minimum height of the mute and solo buttons. | | `--wm-mixer-padding` | literal | `.6rem` | Root-inset compatibility fallback. | | `--wm-mixer-border` | `--wm-border` | `#d8d8d8` | Root border and compatibility fallback for mixer buttons. | | `--wm-mixer-button-border` | `--wm-mixer-border` → `--wm-control-border` → `--wm-border` | `#d8d8d8` | Unpressed M, S, and transport button borders. | | `--wm-control-border` | `--wm-border` | `#d8d8d8` | Shared non-primary control border fallback. | | `--wm-border` | literal fallback | `#d8d8d8` | Shared mixer border fallback. | | `--wm-mixer-radius` | `--wm-control-radius` | `0` | Root and fader-track corners. | | `--wm-control-radius` | literal fallback | `0` | Shared radius fallback. | | `--wm-font-family` | `--wm-font` | `system-ui, sans-serif` | Root, label, and control font family; `--wm-font` remains a compatibility fallback. | | `--wm-mixer-text` | `--wm-foreground` | `#444` | Labels and unpressed button text. | | `--wm-foreground` | literal fallback | `#444` | Shared text fallback. | | `--wm-mixer-button` | `--wm-surface` | `#fff` | Unpressed M, S, and transport button background. | | `--wm-surface` | literal fallback | `#fff` | Shared button-surface fallback. | | `--wm-mixer-fill` | `--wm-accent` | `#999` fader / `#111` pressed button | Fader fill and pressed M / S button background. | | `--wm-mixer-active-text` | `--wm-accent-foreground` | `#fff` | Pressed M / S button text. | | `--wm-accent-foreground` | literal fallback | `#fff` | Shared active-text fallback. | | `--wm-mixer-fader-width` | literal fallback | `2rem` | Fader track width. | | `--wm-mixer-fader-height` | literal fallback | `96px` | Fader track height. | | `--wm-mixer-track-border` | `--wm-control-border` → `--wm-border` | `#d8d8d8` | Fader track outline. | | `--wm-mixer-track` | `--wm-surface-muted` | `#f3f3f3` | Fader track background behind the fill. | | `--wm-mixer-handle` | literal fallback | `0.7rem` | Fader handle height. | | `--wm-mixer-thumb` | `--wm-foreground` | `#111` | Fader handle color. | | `--wm-focus` | `--wm-foreground` | `#111` | Fader, action-button and channel-bank focus outlines. | Each strip's level control is the kit's shared fader — an ARIA slider drawn from a track, a fill and a handle rather than a native ``, because a native slider's handle is only reachable through `::-webkit-slider-thumb` and a light-DOM host cannot install that. The painted box and the range control it used to contain are now one node, so `fader` and `input` — both class and part — land on it together. Drive it in tests with pointer or keyboard events, not by writing `.value`. **Classes** `classNames` adds compatibility classes beside the canonical classes and the fixed legacy aliases; it never replaces either. | Node | Canonical class | Present when | `classNames` key | |---|---|---|---| | Root | `.wui-mixer` | Always; also `.mixer` | `root` | | Transport row | `.wui-mixer__transport` | Any transport command exists; also `.transport` | `transport` | | Board | `.wui-mixer__board` | Always; also `.board` | `board` | | Channel scroller | `.wui-mixer__channels` | Always; also `.strips` | `channels` | | Strip | `.wui-mixer__strip` | Master and every channel; also `.strip` | `strip` | | Master modifier | `.wui-mixer__master` | Master strip; also `.master` | `master` | | Fader | `.wui-mixer__fader .wui-mixer__input .wui-fader` | Every strip; also `.fader` | `fader` and `input` both land here | | Label | `.wui-mixer__label` | Every strip; also `.name` | `label` | | M / S row | `.wui-mixer__actions` | Either channel action exists; also `.btns` | `actions` | | Any button | `.wui-mixer__button` | M, S, Play, Pause, or Stop | `button` | | M / S button | `.mute` / `.solo` | Matching command exists | `mute` / `solo` | | Transport button | `.t-play` / `.t-pause` / `.t-stop` | Matching command exists | `play` / `pause` / `stop` | **Parts** `parts` appends tokens to these canonical values and removes duplicates. | Node | Canonical part | Present when | `parts` key | |---|---|---|---| | Root | `root` | Always | `root` | | Transport row | `transport` | Any transport command exists | `transport` | | Board | `board` | Always | `board` | | Channel scroller | `channels` | Always | `channels` | | Master strip | `strip master` | `master !== false` | `strip`, `master` | | Channel strip | `strip channel` | Per channel | `strip` (`channel` is canonical-only) | | Fader box | `fader` | Every strip | `fader` | | Fader | `fader input` | Every strip | `fader` and `input` both land here | | Label | `label` | Every strip | `label` | | Action row | `actions` | Either channel action exists | `actions` | | M / S button | `button mute` / `button solo` | Matching command exists | `button`, `mute` / `solo` | | Transport button | `button play` / `button pause` / `button stop` | Matching command exists | `button`, matching action |
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/score/element/play/score-rack-control/) - [``](https://koperative-lab.github.io/WebMusic/audio/element/play/audio-mixer/) ## Related presenters - [`@webmusic/ui/meter`](https://koperative-lab.github.io/WebMusic/uikit/mixing-capture/meter/) - [`@webmusic/ui/recorder`](https://koperative-lab.github.io/WebMusic/uikit/mixing-capture/recorder/) --- # @webmusic/ui/recorder > Renders record, optional playback and export actions, input level, and take status from caller-owned capture state. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/mixing-capture/recorder/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountRecorder()` renders a required record toggle, an optional playback toggle, optional format-specific export buttons, a compact input-level bar, and a status line. It paints `RecorderState`; the caller remains responsible for media permission, capture, playback, file creation, and persistence. Use [meter]() for continuous level or spectrum monitoring without a capture workflow, or [mixer]() for channel gain and routing controls. The [``]() and [``]() elements compose this presenter; mount it directly around a custom recorder state machine. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/mixing-capture/recorder/) ## Import ```ts import {mountRecorder} from '@webmusic/ui/recorder'; const handle = mountRecorder(host, { snapshot: () => store.recorder, toggleRecording: () => recorder.toggle(), subscribe: (notify) => store.subscribe(notify), }); ``` ## Async capture commands Every command may return a promise. The presenter invokes it without owning a competing state machine, then repaints after settlement. A synchronous throw or rejection from the current command goes to `onError`; an older command can refresh accepted state but cannot report over a newer command. Settlement after destruction or replacement is ignored. Errors from `onError` are contained. Publish intermediate `busy`, `recording`, `playing`, level, and status snapshots through `subscribe()` when the UI must change while work is pending. The common binding ownership and same-host lifecycle are documented in the [shared presenter shape](). `togglePlayback` controls whether the Play button exists. Export buttons require both nonempty `exportFormats` and an `export(format)` binding. `busy` or `disabled` blocks all commands. Playback and export additionally use `canPlay` and `canExport`; an active take remains stoppable even when `canPlay` becomes false. The presenter borrows all recorder state and resources.
    API `mountRecorder(host, binding, options?)` accepts an `HTMLElement` or `ShadowRoot` and returns a `RecorderHandle`. The level is clamped to `0…1`; omitting it hides the visual meter. **State** | Member | Kind | Description | |---|---|---| | `recording` | `boolean` | Required. Selects Record versus Stop text, label, pressed state, and generated recording status. | | `busy?` | `boolean` | Strict `true` blocks every command and sets root/record `aria-busy`. | | `playing?` | `boolean` | Strict `true` applies the playback modifier, pressed state, Stop text and generated playback status. | | `level?` | `number` | Normalized input level written to `--wui-recorder-level`. Omission hides the meter. | | `recordedCount?` | `number` | Count printed by the generated `● recording… ` status; defaults there to `0`. | | `takeCount?` | `number` | Count printed by generated captured/playback status. When explicit availability is absent, any defined value enables play and export, including `0`. | | `status?` | `string` | Overrides every generated status string. | | `canPlay?` | `boolean` | Explicit playback availability. When absent, falls back to whether `takeCount` is defined. | | `canExport?` | `boolean` | Explicit availability for every export button. When absent, falls back to whether `takeCount` is defined. | | `disabled?` | `boolean` | Strict `true` blocks all commands and applies root `aria-disabled`. | Without `status`, the fallback order is recording count, playing-take text, captured-take text when `takeCount` is defined, then `Ready`. Finite counts become nonnegative integers; invalid counts are omitted. Finite levels clamp to 0–1 and invalid supplied levels paint as 0. **Binding** | Member | Kind | Description | |---|---|---| | `snapshot()` | `() => RecorderState` | Required pull port read on mount, notification, command fulfillment, and explicit `update()`. Errors are sent to `onError` and leave the last successful paint. | | `toggleRecording()` | `() => void \| Promise` | Required command called by the record toggle in both directions. | | `togglePlayback?()` | `() => void \| Promise` | Optional command. Its presence creates the playback toggle. | | `export?(format)` | `(string) => void \| Promise` | Optional command receiving the clicked `exportFormats` id. | | `subscribe?(notify)` | `(notify: () => void) => () => void` | Optional subscription. Its disposer is called during destroy or same-host replacement. | **Options** | Member | Kind | Default | Description | |---|---|---|---| | `exportFormats` | `readonly RecorderExportFormat[]` | `[]` | Snapshotted `{id, label}` strings. With `export()`, creates one button per format; its id reaches the command and its label names the download. | | `label` | `string` | `Recorder` | Accessible root group name. | | `recordLabel` / `stopRecordingLabel` | `string` | `Record` / `Stop recording` | Accessible record-toggle names. | | `playLabel` | `string` | `Play take` | Accessible playback-toggle name. | | `stopPlaybackLabel` | `string` | `playLabel` | Optional name while playing. Set it explicitly, for example to `Stop playback`, to enable a distinct stop label. | | `meterLabel` | `string` | `Input level` | Accessible progressbar name. | | `exportsLabel` | `string` | `Export take` | Accessible export-group name. | | `statusLabel` | `string` | `Recorder status` | Accessible live-status name. | | `classNames` / `parts` | `RecorderClassNames` / `RecorderParts` | `{}` | Add classes or part tokens for `root`, `record`, `play`, `meter`, `level`, `exports`, `export`, and `status`, preserving canonical hooks. | | `onError` | `(error: unknown) => void` | — | Receives snapshot/current-command errors and errors retiring a prior mount. Callback faults are contained. Current mount setup and final cleanup failures can throw after best-effort rollback or release. | | `stylesheet` | `boolean` | `true` | Installs the presenter's stylesheet unless explicitly `false`, for a light-DOM host that cannot inject CSS into its page. | **Handle** | Member | Kind | Description | |---|---|---| | `element` | `HTMLElement` | Presenter-owned `.wui-recorder` root. | | `update()` | `() => void` | Re-reads state and repaints every control; becomes a no-op after destruction. | | `destroy()` | `() => void` | Idempotently retires commands, removes listeners, disposes the subscription and removes root/style. Attempts every cleanup before rethrowing the first failure; retains borrowed recorder resources. | **Accessibility.** The root is a named group. Record and playback are native buttons whose pressed state follows capture/playback. Recording has a dynamic accessible name; playback uses `playLabel` unless an explicit `stopPlaybackLabel` supplies its playing name. Export buttons are named `Download
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. The installed stylesheet resolves presenter-specific properties through shared `--wm-*` tokens and then literals. `--wui-recorder-level` is presenter-owned output written on the level node, not a theme input. The mount installs `recorderStyle` by default; set `stylesheet: false` to omit it and supply the exported stylesheet or your own CSS from the host. Recorder has no `classNames` or `parts` option; canonical classes, fixed compatibility classes, and canonical parts are always installed. Controls and the export group wrap within the host width. Export buttons have independent spacing and retain their full labels; long labels and status text can break across lines. The optional level meter stays hidden when the binding supplies no level. Width changes use CSS and do not remount the recorder or alter recording/playback state. **Tokens** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-recorder-padding` | literal | `.6rem` | Root-inset compatibility fallback. | | `--wm-recorder-height` | `--wm-control-size` → `--wui-row-h` | `2rem` | Height of every button and of the level bar, so the row reads as one strip. | | `--wm-recorder-meter-width` | literal | `90px` | Level bar width. Its height follows the control size above. | | `--wm-recorder-border` | `--wm-border` | `#d8d8d8` | Root border. | | `--wm-control-border` | `--wm-border` | `#d8d8d8` | Shared non-primary control border fallback. | | `--wm-border` | literal | `#d8d8d8` | Shared border fallback. | | `--wm-recorder-background` | `--wm-surface` | `#fff` | Root background. | | `--wm-surface` | literal fallback | `#fff` | Shared root and button-surface fallback. | | `--wm-recorder-text` | `--wm-foreground` | `#222` | Root and inherited button text. | | `--wm-foreground` | contextual literal | `#222` text / `#666` muted | Shared foreground fallback. | | `--wm-font-family` | `--wm-font` | `system-ui, sans-serif` | Root and button font family; `--wm-font` remains a compatibility fallback. | | `--wm-recorder-radius` | `--wm-control-radius` | `0` | Root and inherited button corners. | | `--wm-control-radius` | literal fallback | `0` | Shared corner fallback. | | `--wm-recorder-button-border` | `--wm-control-border` → `--wm-border` | `#d8d8d8` | Record, playback, and export button border. | | `--wm-recorder-button` | `--wm-surface` | `#fff` | Unpressed button background. | | `--wm-recorder-accent` | literal | `#c0392b` | Pressed record background and level fill. | | `--wm-recorder-accent-text` | literal | `#fff` | Pressed record text. | | `--wm-recorder-track` | literal | `#ddd` | Compact level track background. | | `--wm-recorder-status-font` | `.76rem var(--wm-font-mono, ui-monospace, monospace)` | `.76rem ui-monospace, monospace` | Complete status font shorthand. | | `--wm-font-mono` | literal | `ui-monospace, monospace` | Status font family when no complete shorthand is supplied. | | `--wm-focus` | `--wm-focus-ring` | `Highlight` | 2px keyboard-focus outline on record, playback and export buttons. | | `--wm-recorder-muted` | `--wm-foreground-muted` | `#666` | Status text color. | | `--wm-foreground-muted` | literal fallback | `#666` | Shared muted-text fallback. | **Classes** | Node | Canonical class | Present when | Fixed compatibility class | |---|---|---|---| | Root | `.wui-recorder` | Always | `.wrap` | | Any action | `.wui-recorder__button` | Record, playback, and every export | Format id is also a class on export buttons | | Record | `.wui-recorder__record` | Always | `.rec`; `.armed` while recording | | Playback | `.wui-recorder__play` | `togglePlayback` exists | `.play`; `.on` while playing | | Meter track | `.wui-recorder__meter` | Always, hidden when level is absent | `.meter` | | Level fill | `.wui-recorder__level` | Always | `.level` | | Export container | `.wui-recorder__exports` | Export binding and formats exist | — | | Status | `.wui-recorder__status` | Always | `.status` | **Parts** | Node | Canonical part | Present when | |---|---|---| | Root | `root` | Always | | Record button | `record` | Always | | Playback button | `play` | `togglePlayback` exists | | Meter track | `meter` | Always, possibly hidden | | Level fill | `level` | Always | | Export group | `exports` | Export binding and formats exist | | Export button | `export` | One per configured export format | | Status | `status` | Always | `classNames` adds classes beside these canonical classes. `parts` adds tokens beside each canonical part, with duplicate tokens removed. Accessible names use the corresponding label options.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/score/element/play/score-recorder/) - [``](https://koperative-lab.github.io/WebMusic/audio/element/play/audio-recorder/) ## Related presenters - [`@webmusic/ui/mixer`](https://koperative-lab.github.io/WebMusic/uikit/mixing-capture/mixer/) - [`@webmusic/ui/meter`](https://koperative-lab.github.io/WebMusic/uikit/mixing-capture/meter/) --- # @webmusic/ui/note > Renders piano, grid, or chord surfaces and optionally owns pointer, chord, and QWERTY note-hold interaction. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/notes/note/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountNoteSurface()` renders one of three caller-described musical surfaces: percentage-positioned piano keys, code-addressable grid cells, or chord buttons. It paints `NoteSurfaceState`; when the binding supplies `interaction`, the presenter also owns focus, pointer capture, drag glissando, QWERTY holds, chord holds, and guaranteed note-off cleanup. [``]() already composes this presenter. Passive [``]() uses the [pitch presenter]() instead. Mount it directly when musical intent comes from a custom mapping or when the surface must share a layout with caller-owned controls. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/notes/note/) ## Import ```ts import {mountNoteSurface} from '@webmusic/ui/note'; const handle = mountNoteSurface(host, { snapshot: () => store.noteSurface, interaction: { setNote: (midi, on) => synth.setNote(midi, on), }, subscribe: (notify) => store.subscribe(notify), }); ``` ## Performance interaction With `interaction`, piano and grid boards support concurrent pointers, pointer capture, and drag glissando. Hit testing and automatic pitch-node lookup stay inside the attached board, so multiple surfaces can share a document or shadow root. Dragging outside the board releases its current pitch without playing keys on a neighboring board. Holds are reference-counted by pitch and by DOM node, so overlapping pointers sound one note-on and the final release sounds one note-off. Pointer up or cancel releases the current pitch. Chord buttons hold every MIDI pitch in their record until pointer up, cancel, leave, Space/Enter keyup, or focus loss. Multiple holders may overlap the same chord and pitch. When `keyboard` is true, the root becomes focusable and the click-to-start overlay disappears on focus. Unmodified `KeyboardEvent.code` values resolve through `midiForKey`; repeats do not retrigger a held pitch. Escape blurs and releases QWERTY notes. Z and X release all holders before requesting an octave shift, except in a `grid` with `mapOnly: true`; `canShiftOctave` can veto either request. Blur releases QWERTY holders; `releaseAll()`, remount, and destroy release every presenter holder. Ordinary updates repaint external `activeMidis` and the octave label in place: held pointer, chord and QWERTY gestures, focus, scroll position, and DOM identity survive synchronous store notifications. A key stays visibly pressed while either the caller or a presenter holder keeps it active; releasing local input does not erase a caller-owned active pitch. Changes to layout, QWERTY/map-only mode, piano geometry, grid cells/columns, or chord descriptors rebuild the board and release every presenter holder first. The presenter compares descriptor values, so new arrays with identical contents do not rebuild, and edits made in place are still detected. Releases use the old copied pitch mapping. A mapping, snapshot, or note callback that replaces or destroys the mount cannot make the outgoing mount resume a gesture or append its old DOM. Re-entrant updates are bounded to 32 passes and failures go to `onError`. Exceptions from mapping and note commands go to `onError`. The interaction callback is synchronous by design; it does not accept promises. See the [shared presenter shape]() for common ownership and same-host replacement behavior. ## Geometry helpers `pianoKeyLayout(startMidi, endMidi, labels?)` includes both endpoints, orders a reversed range from low to high, divides the full width evenly among white keys, and places black keys across the next white-key boundary at 62% of a white key's width. A range whose first or last key is black reserves half a white key at that end, so the raised key — which is centred on the boundary — still lands on the board instead of half outside it. Positions are percentages, so CSS alone can size the finished keyboard. `PointerSurface` exposes the same multi-pointer, ref-counted hold lifecycle for another board whose nodes carry `data-midi` or `data-pitch`.
    API `mountNoteSurface(host, binding, options?)` accepts an `HTMLElement` or `ShadowRoot` and returns a `NoteSurfaceHandle`. **State** | Member | Kind | Description | |---|---|---| | `layout` | `'piano' \| 'grid' \| 'chords'` | Required layout discriminator selecting which geometry array is rendered. | | `activeMidis?` | `readonly number[]` | Externally held MIDI pitches. Matching piano keys or pitched grid cells receive `.down` and `aria-pressed="true"`; presenter-held input is combined with this state. | | `keyboard?` | `boolean` | Truthy state makes the root focusable and adds the QWERTY hint, octave label, and click-to-start overlay. | | `mapOnly?` | `boolean` | Strict `true` in grid layout removes Z / X from the visible hint and suppresses octave requests. | | `octaveLabel?` | `string` | Text shown on the right of the keyboard bar; absence paints an empty string. | | `piano?` | `readonly NotePianoKey[]` | Piano geometry. Absence builds an empty piano board. | | `grid?` | `readonly NoteGridCell[]` | Grid geometry and optional physical-key mapping. Absence builds an empty grid. | | `gridColumns?` | `number` | Value used in `repeat(, 1fr)`; defaults to `7` when absent. | | `chords?` | `readonly NoteChordCell[]` | Chord labels, stable indices, and optional held pitches. Absence builds an empty chord row. | Geometry record shapes: | Record member | Kind | Description | |---|---|---| | `NotePianoKey.midi` | `number` | Pitch written to `data-midi`. | | `NotePianoKey.black?` | `boolean` | Truthy state selects raised-key geometry and styling. | | `NotePianoKey.left` | `number` | Percentage position assigned to inline `left`. | | `NotePianoKey.width` | `number` | Percentage width assigned inline. | | `NotePianoKey.label?` | `string` | Optional visible key label. | | `NoteGridCell.midi?` | `number` | Optional playable pitch. Absence creates an idle, noninteractive cell. | | `NoteGridCell.code?` | `string` | Optional physical `KeyboardEvent.code` written to `data-code`. | | `NoteGridCell.ref?` | `string` | Optional visible reference label written to `data-ref`. | | `NoteChordCell.label` | `string` | Visible chord-button text. | | `NoteChordCell.index` | `number` | Identifier written to `data-i` and used to resolve held chord data. | | `NoteChordCell.midis?` | `readonly number[]` | Pitches passed to `setNote` together while the chord is held. Non-finite entries are skipped. | **Binding** | Member | Kind | Description | |---|---|---| | `snapshot()` | `() => NoteSurfaceState` | Required pull port read at mount and update. A failed update reports the error and keeps the current root. | | `interaction?` | `NoteSurfaceInteractionBinding` | Optional command and mapping port. Without it, the presenter still builds and paints the surface but installs no performance listeners. | | `subscribe?(notify)` | `(notify: () => void) => () => void` | Optional subscription. Registration errors and disposer errors go to `onError`. | The interaction port has these members: | Member | Kind | Description | |---|---|---| | `setNote(midi, on)` | `(number, boolean) => void` | Required synchronous note-on/note-off command for pointer, chord, and QWERTY holders. | | `midiForKey?(code)` | `(string) => number \| null` | Resolves an unmodified physical key code. Missing, null, or non-finite results are ignored. | | `canShiftOctave?(delta)` | `(-1 \| 1) => boolean` | Optional guard for Z / X. Absence permits the shift. | | `requestOctaveShift?(delta)` | `(-1 \| 1) => void` | Optional shift command. Its presence enables Z / X outside map-only grids. | **Options** | Member | Kind | Default | Description | |---|---|---|---| | `onError` | `(error: unknown) => void` | — | Receives snapshot, interaction, subscription, and cleanup failures. Exceptions thrown by the error sink are swallowed. | | `stylesheet` | `boolean` | `true` | Installs the presenter's stylesheet unless explicitly `false`, for a light-DOM host that cannot inject CSS into its page. | **Handle** | Member | Kind | Description | |---|---|---| | `element` | `HTMLElement` | Getter returning the current `.wui-note` root; ordinary updates preserve it, while structural changes replace it. | | `board?` | `HTMLElement` | Getter returning the current piano, grid, or chord board. | | `releaseAll()` | `() => void` | Idempotently releases every pointer, chord, and QWERTY holder; externally active pitches remain visibly pressed. | | `update()` | `() => void` | Pulls state and repaints values in place. Only changed layout or descriptors rebuild the root and release held input. | | `destroy()` | `() => void` | Idempotently releases all notes, detaches listeners, unsubscribes, and removes the current root and installed style node. | **Utilities** | Member | Kind | Description | |---|---|---| | `isBlackKey(midi)` | `(number) => boolean` | Returns true for pitch classes 1, 3, 6, 8, and 10, including negative MIDI values. | | `pianoKeyLayout(start, end, labels?)` | `(number, number, ReadonlyMap?) => NotePianoKey[]` | Produces inclusive percentage geometry as described above. | | `new PointerSurface(options)` | `PointerSurface` | Creates reusable multi-pointer pitch interaction over a caller-supplied root, selector, and synchronous `fire(pitch, on)` callback. | | `PointerSurface.attach(board)` | `(HTMLElement) => void` | Releases and detaches any previous board, then installs pointer listeners on the new board. | | `PointerSurface.detach()` | `() => void` | Removes pointer listeners without itself emitting releases; use `releaseAll()` first when holders may exist. | | `PointerSurface.press/release(pitch, element?)` | `(number, HTMLElement?) => void` | Reference-counts a programmatic holder and its optional visual node. | | `PointerSurface.setActiveMidis(midis?)` | `(readonly number[]) => void` | Paints caller-held pitches alongside local holders without firing notes. Omit the list to clear external active state. | | `PointerSurface.releaseAll()` | `() => void` | Clears pointer, pitch, and node holders and emits one final off for each sounding pitch. | | `PointerSurfaceOptions.root` | `() => Document \| ShadowRoot \| undefined` | Root used for hit testing; hits and automatic pitch-node lookup are restricted to the attached board. | | `PointerSurfaceOptions.keySelector` | `string` | Selector for nodes carrying `data-midi` or `data-pitch`. | | `PointerSurfaceOptions.fire` | `(pitch: number, on: boolean) => void` | Synchronous edge callback. | | `PointerSurfaceOptions.downClass?` | `string` | Pressed class; defaults to `'down'`. | | `PointerSurfaceHit` | `{pitch: number; element: HTMLElement}` | Public hit record used by the pointer lifecycle. | | `noteStyle` | `string` | Canonical stylesheet text installed by `mountNoteSurface()`. | **Accessibility.** With `keyboard`, the root enters the tab order and gains a visible focus outline; focus hides the pointer-oriented start overlay. Piano keys and pitched grid cells expose `aria-pressed`, but remain pointer/QWERTY surfaces rather than individual tab stops. Idle grid cells are noninteractive. Chords are native buttons with visible names and `aria-pressed`; Space and Enter use press-on-keydown/release-on-keyup semantics instead of click toggling. Escape and blur release held QWERTY notes, and focus loss releases a held chord. Left/Right scroll the piano or grid viewport while the QWERTY root has focus, without releasing held notes. Without QWERTY, that named viewport enters the tab order for native keyboard scrolling. The presenter does not rewrite caller-owned nodes elsewhere in the host.
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. The installed `noteStyle` resolves presenter-specific tokens through the shared `--wm-*` fallbacks shown below and then literals. The mount installs `noteStyle` by default; set `stylesheet: false` to omit it and supply the exported stylesheet or your own CSS from the host. Note has no `classNames` or `parts` option; canonical classes, fixed compatibility classes, and the root and viewport parts are always installed. Piano and grid boards scroll horizontally inside the available host width instead of compressing their keys and labels. The piano keeps its percentage geometry with a minimum white-key width; the grid keeps the supplied column count with a minimum cell width. The keyboard hint wraps, and chord buttons wrap into rows with space for multiline labels. Container resizing does not rebuild the board or release held notes. Long custom piano labels are ellipsized inside their key, with the full label retained as its tooltip. White and black active-key labels use `--wm-note-label-active` → `--wm-accent-foreground`, independently from the background. The neutral active fill is `#444` with white text. Unpressed black-key labels use `--wm-note-label-alt` → `--wm-foreground-on-inverse` → `#fff`; their borders use `--wm-note-key-alt-border` → `--wm-border` → `#000`. Root text uses `--wm-note-foreground` → `--wm-foreground`. Keyboard input and held notes are unchanged by token updates. **Tokens** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-note-background` | `--wm-surface` | `#fff` | Root background; the grid itself is transparent to avoid duplicate alpha. | | `--wm-surface` | literal fallback | `#fff` | Shared root-background fallback. | | `--wm-note-focus` | `--wm-focus` | `#444` | Root keyboard focus outline. | | `--wm-focus` | literal fallback | `#444` | Shared focus fallback. | | `--wm-note-label` | `--wm-foreground-muted` → `--wm-foreground` | `#666` | Bar text, white-key labels, and grid references. | | `--wm-font` | `--wm-font-family` | `system-ui, sans-serif` | Legacy font override for the bar, start prompt, and chords. | | `--wm-font-family` | literal fallback | `system-ui, sans-serif` | Shared UI font when the legacy override is absent. | | `--wm-font-mono` | literal fallback | `ui-monospace, monospace` | Key and grid-reference labels. | | `--wm-note-overlay` | literal fallback | `rgba(255,255,255,.82)` | Click-to-start overlay. | | `--wm-note-radius` | `--wm-control-radius` | `0` | Start prompt, keys, cells, and chord corners. | | `--wm-note-accent` | `--wm-accent` | `#111` | Start-prompt background. | | `--wm-note-accent-foreground` | `--wm-accent-foreground` | `#fff` | Start-prompt text. | | `--wm-note-height` | `--wm-surface-md` | `144px` | Piano board height, on the shared drawing-surface scale. | | `--wm-note-key-min-width` | literal fallback | `2.25rem` | Minimum white-key width before the piano scrolls. | | `--wm-note-cell-min-width` | literal fallback | `2.5rem` | Minimum grid-cell width before the grid scrolls. | | `--wm-note-border` | `--wm-control-border` → `--wm-border` | `#d8d8d8` | White-key, grid-cell, and chord borders. | | `--wm-control-border` | `--wm-border` | `#d8d8d8` | Shared non-primary control border fallback. | | `--wm-note-key` | `--wm-surface` | `#fff` | White-key background. | | `--wm-note-key-alt` | `--wm-surface-inverse` | `#222` | Black-key and chord background. | | `--wm-note-active` | `--wm-accent` | `#444` | Pressed key, cell, and chord background. | | `--wm-note-gap` | literal fallback | `0.4rem` | Grid and chord gaps. | | `--wm-note-cell-height` | literal fallback | `42px` | Grid-cell minimum height and two-thirds of the chord minimum height. | | `--wm-note-surface` | `--wm-surface-muted` → `--wm-surface` | `#f3f3f3` | Idle grid-cell surface background. | | `--wm-note-label-active` | `--wm-accent-foreground` | `#fff` | Pressed grid reference and chord text. | Unpressed black-key borders and labels default to `#000` and `#fff`; the dedicated border and label tokens described above override them. **Classes** | Node | Canonical class | Present when | Fixed compatibility / state classes | |---|---|---|---| | Root | `.wui-note` | Always | `.wrap`; `.on` while focused with interaction | | Keyboard bar | `.wui-note__bar` | `keyboard` truthy | `.qwerty-bar` | | Hint | `.wui-note__hint` | `keyboard` truthy | `.hint` | | Octave text | `.wui-note__octave` | `keyboard` truthy | `.oct` | | Board host | `.wui-note__host` | Always | `.host` | | Scroll viewport | `.wui-note__viewport` | Piano and grid layouts | — | | Start overlay | `.wui-note__overlay` | `keyboard` truthy | `.overlay` | | Start prompt | `.wui-note__start` | `keyboard` truthy | `.start` | | Piano board | `.wui-note__piano` | Piano layout | `.board.kbd` | | Piano key | `.wui-note__key` | Per piano record | `.note.key`, plus `.wui-note__white.white` or `.wui-note__black.black`; `.down` while held | | Key label | `.wui-note__key-label` | Label supplied | `.keylab` | | Grid board | `.wui-note__grid` | Grid layout | `.board.grid.qwerty` | | Grid cell | `.wui-note__cell` | Per grid record | `.cell.note` when pitched or `.cell.idle` without MIDI; `.down` while held | | Grid reference | `.wui-note__reference` | Reference supplied | `.ref` | | Chord row | `.wui-note__chords` | Chords layout | `.board.chordrow` | | Chord button | `.wui-note__chord` | Per chord record | `.chord`; `.down` while held | **Parts** | Node | Canonical part | Present when | |---|---|---| | Root | `root` | Always | | Scroll viewport | `viewport` | Piano and grid layouts |
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/score/element/play/score-note-input/) ## Related presenters --- # @webmusic/ui/envelope > Renders an ADSR curve with synchronized pointer and native-range interaction over a structural binding. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/envelope/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountEnvelope()` renders an ADSR curve, filled area, three draggable SVG handles, a visual readout and four native range inputs. It paints `EnvelopeSnapshot`; the caller owns envelope and range state and commits complete `EnvelopeState` objects through `setEnvelope()`. Use [LFO]() for cyclic modulation or [parameter]() for independent scalar controls. [``]() already composes this presenter. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/envelope/) ## Import ```ts import {mountEnvelope} from '@webmusic/ui/envelope'; const handle = mountEnvelope(host, { snapshot: () => store.envelope, setEnvelope: (envelope) => store.setEnvelope(envelope), subscribe: (notify) => store.subscribe(notify), }); ``` ## Geometry and command settlement The viewbox has four equal horizontal segments. Attack occupies the first segment, decay the second, sustain holds across the third, and release extends into the fourth. Each time stage is scaled against its positive `*Max` range; sustain is clamped to `0…1`. The combined decay/sustain handle edits both axes in one pointer gesture. Pointer and native-range input paints optimistically, calls `setEnvelope()` with a new complete object, then re-reads the binding after the latest command settles. Earlier command settlements cannot overwrite a newer gesture. Subscription and explicit updates use the same 32-pass bounded repaint path. See the [shared presenter shape]() for the common lifecycle contract.
    API `mountEnvelope(host, binding, options?)` accepts an `HTMLElement` or `ShadowRoot` and returns an `EnvelopeHandle`. **State** Non-finite envelope values normalize to `0`. Non-positive or non-finite ranges use the defaults shown below; curve geometry clamps values to each visible range without mutating caller state. | Member | Kind | Description | |---|---|---| | `envelope` | `EnvelopeState` | Required ADSR value object painted and replaced as one command payload. | | `envelope.attack` | `number` | Attack seconds; visible geometry clamps to `0…attackMax`. | | `envelope.decay` | `number` | Decay seconds; visible geometry clamps to `0…decayMax`. | | `envelope.sustain` | `number` | Sustain level clamped to `0…1` for geometry and input. | | `envelope.release` | `number` | Release seconds; visible geometry clamps to `0…releaseMax`. | | `ranges` | `EnvelopeRanges` | Required positive range maxima for time-stage scaling. | | `ranges.attackMax` | `number` | Attack scale maximum; invalid values become `2`. | | `ranges.decayMax` | `number` | Decay scale maximum; invalid values become `2`. | | `ranges.releaseMax` | `number` | Release scale maximum; invalid values become `2`. | | `disabled?` | `boolean` | Strict `true` disables all range inputs and pointer editing. | **Binding** | Member | Kind | Description | |---|---|---| | `snapshot()` | `() => EnvelopeSnapshot` | Required pull port read on mount, notification, command settlement and `update()`; failures go to `onError`. | | `setEnvelope(envelope)` | `(EnvelopeState) => void \| Promise` | Required command receiving a fresh complete ADSR object after pointer or range input. | | `subscribe?(notify)` | `(notify: () => void) => () => void` | Optional external-state subscription released by `destroy()`. | **Options** | Member | Kind | Default | Description | |---|---|---|---| | `label` | `string` | `'Envelope'` | Accessible name of the root group. | | `classNames` | `EnvelopeClassNames` | `{}` | Adds compatibility classes to root, SVG, grid, area, curve, handles, readout, inputs wrapper and inputs. | | `parts` | `EnvelopeParts` | `{}` | Adds tokens beside the canonical parts for the same node set. | | `onError` | `(error: unknown) => void` | — | Receives snapshot, command, subscription, stabilization and cleanup failures. | | `stylesheet` | `boolean` | `true` | Installs the presenter's stylesheet unless explicitly `false`, for a light-DOM host that cannot inject CSS into its page. | **Handle** | Member | Kind | Description | |---|---|---| | `element` | `HTMLElement` | Presenter-owned `.wui-envelope` root. | | `update()` | `() => void` | Re-reads and repaints the binding through the bounded update loop. | | `destroy()` | `() => void` | Idempotently ends pointer capture and removes listeners, subscription, root and installed stylesheet. | **Accessibility.** The root is a `role="group"` named by `label`. Four visually clipped native ranges are labelled Attack, Decay, Sustain and Release and expose formatted `aria-valuetext`; their native keys are the keyboard-equivalent editing path. SVG curve, handles and visual readout are `aria-hidden`, so they do not duplicate the ranges. Focus on a range highlights its related SVG handle. Disabled state reaches every native input, and forced-colors rules retain curve, handle and focus contrast.
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. The presenter installs `envelopeStyle` unless `stylesheet: false`. The SVG fills its container width and keeps the height set by `--wm-envelope-height`. The wrapping ADSR readout occupies a separate row below the plot and adds to the total height. Drag coordinates use the SVG's current bounds, so wrapping text and container resizing do not change the time/level mapping. Supply the exported stylesheet or your own CSS from the host when opting out. The graph is an inverse surface: `--wm-surface-inverse` and `--wm-foreground-on-inverse` keep its background and marks paired. Ordinary `--wm-surface` and `--wm-foreground` do not recolor that inner graph. Set `--wm-envelope-background` and the documented graph-ink tokens together when replacing its palette; the readout below uses ordinary foreground tokens. **Tokens** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-envelope-border` | `--wm-control-border` → `--wm-border` | `#d8d8d8` | Inner graph border. | | `--wm-control-border` | `--wm-border` | `#d8d8d8` | Shared non-primary control border fallback. | | `--wm-border` | literal | `#d8d8d8` | Shared border fallback. | | `--wm-envelope-radius` | `--wm-control-radius` | `0` | Inner graph corners. | | `--wm-control-radius` | literal | `0` | Shared radius fallback. | | `--wm-envelope-background` | `--wm-surface-inverse` | `#111` | Inner graph surface; the outer root uses the shared `--wm-component-*` surface. | | `--wm-surface-inverse` | literal | `#111` | Shared inverse-surface fallback. | | `--wm-envelope-foreground` | `--wm-foreground-on-inverse` | `#fff` | Root current color and curve/handle fallback. | | `--wm-foreground-on-inverse` | literal | `#fff` | Shared inverse foreground fallback. | | `--wm-envelope-height` | `--wm-surface-md` | `144px` | SVG display height, on the shared drawing-surface scale. | | `--wm-envelope-grid` | literal | `rgba(255,255,255,.12)` | Vertical guide lines. | | `--wm-envelope-fill` | literal | `rgba(255,255,255,.12)` | Area under the curve. | | `--wm-envelope-curve` | `currentColor` | Root foreground | ADSR line. | | `--wm-envelope-handle` | `currentColor` | Root foreground | Handle fill. | | `--wm-envelope-handle-border` | `--wm-control-border` → `--wm-border` | `#d8d8d8` | Handle outline. | | `--wm-focus` | `--wm-focus-ring` | `Highlight` | Focused-handle outline. | | `--wm-focus-ring` | literal | `Highlight` | Shared focus fallback. | | `--wm-font-mono` | literal | `ui-monospace, monospace` | ADSR readout font family. | | `--wm-envelope-text` | `--wm-foreground-muted` → `--wm-foreground` | `#666` | Visual ADSR readout. | **Classes** `classNames` adds compatibility classes without replacing canonical names. | Node | Canonical class | `classNames` key | |---|---|---| | Root | `.wui-envelope` | `root` | | SVG | `.wui-envelope__svg` | `svg` | | Each guide | `.wui-envelope__grid` | `grid` | | Filled polygon | `.wui-envelope__area` | `area` | | Curve polyline | `.wui-envelope__curve` | `curve` | | Each handle | `.wui-envelope__handle` | `handle` | | Visual readout | `.wui-envelope__readout` | `readout` | | Clipped input wrapper | `.wui-envelope__inputs` | `inputs` | | Each native range | `.wui-envelope__input` | `input` | Runtime modifiers are `.is-disabled` on the root and `.is-dragging` / `.is-focused` on handles. **Parts** The canonical part tokens are `root`, `svg`, `grid`, `area`, `curve`, `handle`, `readout`, `inputs` and `input`; `EnvelopeParts` adds tokens beside each matching name and de-duplicates them.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/score/element/play/score-synth-panel/) ## Related presenters - [`@webmusic/ui/parameter`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/parameter/) - [`@webmusic/ui/macro`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/macro/) - [`@webmusic/ui/lfo`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/lfo/) - [`@webmusic/ui/eq`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/eq/) --- # @webmusic/ui/eq > Renders an equalizer response curve with draggable band controls over caller-owned graph state. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/eq/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountEq()` renders a logarithmic-frequency response curve, fill, zero line, draggable band points and a per-band readout. The caller owns filters and response calculation; the presenter paints `EqState` and commits only frequency/gain changes through `setBand()`. Use [parameter]() for independent filter controls or [LFO]() for cyclic modulation. [``]() already composes this presenter with its audio graph. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/eq/) ## Import ```ts import {mountEq} from '@webmusic/ui/eq'; const handle = mountEq(host, { snapshot: () => graph.eqState, setBand: (index, band) => graph.setBand(index, band), subscribe: (notify) => graph.subscribe(notify), }); ``` ## Coordinate model and response ownership Horizontal position maps logarithmically from 30 Hz to 18 kHz. Vertical position maps linearly from +18 dB at the top to −18 dB at the bottom. Dragging paints the band optimistically and sends rounded frequency plus clamped gain; `q` and per-band disabled state remain caller-owned. `response` already contains normalized x positions and gain values. The presenter clamps x to `0…1`, converts gain to y, and connects the points; it never calculates a filter response. The Live Demo's caller does that calculation so dragging remains visible. Latest-command revisions are tracked independently per band. See the [shared presenter shape]() for common lifecycle rules.
    API `mountEq(host, binding, options?)` accepts an `HTMLElement` or `ShadowRoot` and returns a `MountedEqHandle`. **State** | Member | Kind | Description | |---|---|---| | `bands` | `EqBandState[]` | Required ordered band list; a length change rebuilds points and control nodes. | | `bands[].frequency` | `number` | Center frequency clamped to `30…18000` Hz; invalid values become `30`. | | `bands[].gain?` | `number` | Gain clamped to `−18…18` dB; absent or invalid values become `0`. | | `bands[].q?` | `number` | Missing or non-finite input becomes `1`; finite zero or negative values clamp to `Number.EPSILON`. | | `bands[].disabled?` | `boolean` | Strict `true` suppresses pointer and input commands for this band. | | `response?` | `EqResponsePoint[]` | Ordered caller-calculated points for curve and fill; absent paints no response. | | `response[].x` | `number` | Normalized logarithmic-frequency coordinate clamped to `0…1`. | | `response[].gain` | `number` | Response gain in dB; non-finite values normalize to `0` and visible y clamps to ±18 dB. | | `ready?` | `boolean` | Strict `true` hides the empty-state message below the graph. | | `disabled?` | `boolean` | Strict `true` disables every band and sets root `aria-disabled`. | **Binding** | Member | Kind | Description | |---|---|---| | `snapshot()` | `() => EqState` | Required pull port read on mount, notification, command settlement and `update()`. | | `setBand(index, band)` | `(number, {frequency: number; gain: number}) => void \| Promise` | Required command for one dragged/edited band; `q` is intentionally not sent. | | `subscribe?(notify)` | `(notify: () => void) => () => void` | Optional external-state subscription released on destruction. | **Options** | Member | Kind | Default | Description | |---|---|---|---| | `label` | `string` | `'Equalizer'` | Accessible name of the root group. | | `emptyLabel` | `string` | `'Connect an equalizer graph'` | Status text while `ready !== true`. | | `classNames` | `EqClassNames` | `{}` | Adds compatibility classes to all EQ node kinds. | | `parts` | `EqParts` | `{}` | Adds tokens beside every canonical EQ part; this type has the same keys as `EqClassNames`. | | `formatFrequency` | `(frequency: number) => string` | Compact Hz/kHz | Formats each frequency in the visible band readout. | | `onError` | `(error: unknown) => void` | — | Receives snapshot, formatter, command, subscription, stabilization and cleanup failures. | | `stylesheet` | `boolean` | `true` | Installs the presenter's stylesheet unless explicitly `false`, for a light-DOM host that cannot inject CSS into its page. | **Handle** | Member | Kind | Description | |---|---|---| | `element` | `HTMLElement` | Presenter-owned `.wui-eq` root. | | `svg` | `SVGSVGElement` | Stable response SVG for geometry-aware integrations. | | `emptyElement()` | `() => HTMLElement` | Stable presenter-owned empty-state message. | | `update()` | `() => void` | Re-reads the binding through a 32-pass bounded update loop and restores the last good state after a failed paint. | | `destroy()` | `() => void` | Idempotently removes pointer listeners, subscription, root and installed stylesheet. | **Accessibility.** The root is a named `role="group"` and reflects disabled state through `aria-disabled`. Every band has two visually hidden, focusable native ranges: frequency and gain. Tab selects a range and the native arrow keys edit it; `aria-valuetext` announces hertz or decibels rather than normalized coordinates. Focus outlines the corresponding band point and the visible readout names its band and axis. SVG graphics are decorative pointer targets. The empty message is a live status region.
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. The presenter installs `eqStyle` unless `stylesheet: false`. The SVG fills its container width and keeps the height set by `--wm-eq-height`. Readouts and empty text occupy separate, wrapping rows below it, so they add to the total height and do not cover the response. Pointer coordinates use the SVG's current bounds after resizing. Supply the exported stylesheet or your own CSS from the host when opting out. The graph is an inverse surface: `--wm-surface-inverse` and `--wm-foreground-on-inverse` keep its background and marks paired. Ordinary `--wm-surface` and `--wm-foreground` do not recolor that inner graph. Set `--wm-eq-background` and the documented graph-ink tokens together when replacing its palette; the readout below uses ordinary foreground tokens. **Tokens** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-eq-border` | `--wm-control-border` → `--wm-border` | `#d8d8d8` | Inner graph border. | | `--wm-control-border` | `--wm-border` | `#d8d8d8` | Shared non-primary control border fallback. | | `--wm-border` | literal | `#d8d8d8` | Shared border fallback. | | `--wm-eq-radius` | `--wm-control-radius` | `0` | Inner graph corners. | | `--wm-control-radius` | literal | `0` | Shared radius fallback. | | `--wm-eq-background` | `--wm-surface-inverse` | `#111` | Inner graph surface; the outer root uses the shared `--wm-component-*` surface. | | `--wm-surface-inverse` | literal | `#111` | Shared inverse-surface fallback. | | `--wm-eq-text` | `--wm-foreground-muted` → `--wm-foreground` | `#666` | Root current color, readout and empty state. | | `--wm-eq-height` | `--wm-surface-md` | `144px` | SVG display height, on the shared drawing-surface scale. | | `--wm-eq-grid` | literal | `rgba(255,255,255,.12)` | Grid lines. | | `--wm-eq-zero` | literal | `rgba(255,255,255,.3)` | Dashed zero-gain line. | | `--wm-eq-fill` | literal | `rgba(255,255,255,.1)` | Area under the response curve. | | `--wm-eq-curve` | `--wm-foreground-on-inverse` | `#fff` | Response curve. | | `--wm-foreground-on-inverse` | literal | `#fff` | Shared inverse foreground fallback. | | `--wm-eq-point` | `--wm-foreground-on-inverse` | `#fff` | Band point fill. | | `--wm-eq-point-border` | `--wm-control-border` → `--wm-border` | `#d8d8d8` | Band point outline. | | `--wm-font-mono` | literal | `ui-monospace, monospace` | Band readout font. | | `--wm-font-family` | `--wm-font` | `system-ui, sans-serif` | Empty-state font family; `--wm-font` remains a compatibility fallback. | | `--wm-focus` | `--wm-focus-ring` | `currentColor` (root), `Highlight` (band) | Root and focused band outlines. | | `--wm-focus-ring` | literal | `currentColor` (root), `Highlight` (band) | Shared focus fallback. | **Classes** `classNames` adds compatibility classes beside canonical names. | Node | Canonical class | `classNames` key | |---|---|---| | Root | `.wui-eq` | `root` | | SVG | `.wui-eq__svg` | `svg` | | Grid group | `.wui-eq__grid` | `grid` | | Zero line | `.wui-eq__zero` | `zero` | | Filled polygon | `.wui-eq__area` | `area` | | Response polyline | `.wui-eq__curve` | `curve` | | Point group | `.wui-eq__points` | `points` | | Each point | `.wui-eq__point` | `point` | | Band readout | `.wui-eq__readout` | `readout` | | Empty message | `.wui-eq__empty` | `empty` | | Each generated range | `.wui-eq__input` | `input` | Every empty state is a live region — `role="status"`, `aria-live="polite"` — so the message is announced when it appears, not only drawn. **Parts** The canonical part tokens are `root`, `svg`, `grid`, `zero`, `area`, `curve`, `points`, `point`, `readout`, `empty`, and `input`. `EqParts` adds tokens beside every matching canonical part.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/score/element/play/score-synth-panel/) ## Related presenters - [`@webmusic/ui/parameter`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/parameter/) - [`@webmusic/ui/macro`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/macro/) - [`@webmusic/ui/envelope`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/envelope/) - [`@webmusic/ui/lfo`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/lfo/) --- # @webmusic/ui/lfo > Renders run, shape, rate, depth and live-phase waveform controls over caller-owned LFO state. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/lfo/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountLfo()` renders a run toggle, four waveform buttons, rate and depth ranges, and a waveform with a phase head. The caller owns oscillator time, scheduling and modulation targets; the presenter paints `LfoState` and calls one structural command per control. Use [envelope]() for one-shot ADSR shaping or [parameter]() for independent values. The Live Demo advances `phase` in caller code only while `running`; [``]() already provides the production composition. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/lfo/) ## Import ```ts import {mountLfo} from '@webmusic/ui/lfo'; const handle = mountLfo(host, { snapshot: () => store.lfo, setRunning: (running) => clock.setRunning(running), setShape: (shape) => store.setShape(shape), setRate: (rate) => store.setRate(rate), setDepth: (depth) => store.setDepth(depth), subscribe: (notify) => store.subscribe(notify), }); ``` ## Commands and phase ownership The presenter normalizes each snapshot, then overlays the latest unsettled command of each kind for optimistic paint. Running, shape, rate and depth use independent revision counters, so an old rate promise cannot clear a newer rate command and never blocks a simultaneous shape command. After the latest command settles, the presenter re-reads authoritative state. `phase` has no presenter command: an external clock, audio graph or animation owner advances it and calls the binding subscription. `update()` is re-entrancy-safe and bounded to 32 passes. See the [shared presenter shape]() for the common lifecycle.
    API `mountLfo(host, binding, options?)` accepts an `HTMLElement` or `ShadowRoot` and returns a `MountedLfoHandle`. **State** | Member | Kind | Description | |---|---|---| | `shape` | `'sine' \| 'triangle' \| 'square' \| 'saw'` | Selects button state, chip glyph and generated waveform; unknown values normalize to `'sine'`. | | `rate` | `number` | Frequency in hertz, clamped to the native input's `0.05…12` range. | | `depth` | `number` | Normalized modulation depth clamped to `0…1`; scales waveform amplitude. | | `phase` | `number` | Normalized external cycle position clamped to `0…1`; sets the phase-head x coordinate. | | `running` | `boolean` | Strict `true` selects the stop glyph and accessible action. | | `disabled?` | `boolean` | Strict `true` disables every button and range input and dims the root. | **Binding** | Member | Kind | Description | |---|---|---| | `snapshot()` | `() => LfoState` | Required pull port read on mount, notification, command settlement and `update()`. | | `setRunning(running)` | `(boolean) => void \| Promise` | Required run/stop command. | | `setShape(shape)` | `(LfoShape) => void \| Promise` | Required waveform command. | | `setRate(rate)` | `(number) => void \| Promise` | Required frequency command receiving the native range value. | | `setDepth(depth)` | `(number) => void \| Promise` | Required normalized depth command. | | `subscribe?(notify)` | `(notify: () => void) => () => void` | Optional external-state subscription released by `destroy()`. | **Options** | Member | Kind | Default | Description | |---|---|---|---| | `label` | `string` | `'LFO'` | Accessible name of the root group. | | `runLabel` | `string` | `'Run'` | Run button accessible name while stopped. | | `stopLabel` | `string` | `'Stop'` | Run button accessible name while running. | | `classNames` | `LfoClassNames` | `{}` | Adds compatibility classes for root, controls, inputs, values and waveform nodes. | | `parts` | `LfoParts` | `{}` | Adds tokens beside all canonical LFO parts. | | `formatRate` | `(rate: number) => string` | `` `${rate.toFixed(2)}Hz` `` | Formats visible rate and `aria-valuetext`; thrown errors go to `onError`. | | `formatDepth` | `(depth: number) => string` | Integer percent | Formats visible depth and `aria-valuetext`; thrown errors go to `onError`. | | `onError` | `(error: unknown) => void` | — | Receives snapshot, formatter, command, subscription, stabilization and cleanup failures. | | `stylesheet` | `boolean` | `true` | Installs the presenter's stylesheet unless explicitly `false`, for a light-DOM host that cannot inject CSS into its page. | **Handle** | Member | Kind | Description | |---|---|---| | `element` | `HTMLElement` | Presenter-owned `.wui-lfo` root. | | `controls.run` | `HTMLButtonElement` | Run/stop toggle. | | `controls.shapes` | `HTMLElement` | Shape button group. | | `controls.rateInput` | `HTMLInputElement` | Native rate range. | | `controls.rateValue` | `HTMLElement` | Formatted, aria-hidden rate readout. | | `controls.depthInput` | `HTMLInputElement` | Native depth range. | | `controls.depthValue` | `HTMLElement` | Formatted, aria-hidden depth readout. | | `controls.wave` | `HTMLElement` | Waveform viewport. | | `controls.curve` | `SVGPolylineElement` | Generated waveform curve. | | `controls.head` | `SVGLineElement` | Current phase head. | | `update()` | `() => void` | Re-reads and repaints through the bounded update loop. | | `destroy()` | `() => void` | Idempotently invalidates pending commands and removes listeners, subscription, root and stylesheet. | **Accessibility.** The root and shape set are named groups. Run is a native button with a dynamic accessible name; every shape is a labelled toggle button with `aria-pressed`. Rate and depth are native labelled ranges whose custom formatted values are mirrored in `aria-valuetext`; visible readouts and all SVG glyphs are hidden from assistive technology. Disabled state reaches every interactive node, and `:focus-visible` outlines buttons and inputs.
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. The presenter installs `lfoStyle` unless `stylesheet: false`. Controls and waveform buttons wrap according to their container width. Range inputs can shrink below their preferred width; long formatted values wrap onto additional lines while the waveform keeps its configured height. Supply the exported stylesheet or your own CSS from the host when opting out. The waveform pairs `--wm-surface-inverse` with `--wm-foreground-on-inverse`. Ordinary surface and foreground tokens leave that inverse plot unchanged; set both inverse tokens or the LFO-specific background/curve tokens to replace it. **Tokens** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-lfo-height` | `--wm-control-size` → `--wui-row-h` | `2rem` | Height of the run toggle, the shape chips, and both range rails. | | `--wm-lfo-handle-size` | literal | `.7rem` | Range thumb width. Its height follows the control size. | | `--wm-lfo-thumb-radius` | `--wm-control-radius` | `0` | Range thumb corners. | | `--wm-lfo-padding` | literal | `.6rem` | Root-inset compatibility fallback. | | `--wm-lfo-border` | `--wm-border` | `#d8d8d8` | Root border. | | `--wm-control-border` | `--wm-border` | `#d8d8d8` | Shared non-primary control border fallback. | | `--wm-border` | literal | `#d8d8d8` | Shared border fallback. | | `--wm-lfo-radius` | `--wm-control-radius` | `0` | Root, run and shape-button corners. | | `--wm-control-radius` | literal | `0` | Shared radius fallback. | | `--wm-lfo-background` | `--wm-surface` | `#fff` | Root background. | | `--wm-surface` | literal | `#fff` | Shared surface and button fallback. | | `--wm-lfo-foreground` | `--wm-foreground` | `#111` | Root current color. | | `--wm-foreground` | literal | `#111` (`#333` chip text) | Shared foreground fallback. | | `--wm-lfo-font` | `.8rem var(--wm-font-family)` | `.8rem system-ui, sans-serif` | Root font shorthand. | | `--wm-font-family` | literal | `system-ui, sans-serif` | Font-family fallback. | | `--wm-lfo-gap` | literal | `.5rem` | Control-row gap. | | `--wm-lfo-button-background` | `--wm-accent` → `--wm-foreground` | `#111` | Run background and border. | | `--wm-lfo-button-foreground` | `--wm-accent-foreground` | `#fff` | Run glyph. | | `--wm-lfo-shape-gap` | literal | `.25rem` | Gap between waveform buttons. | | `--wm-lfo-chip-border` | `--wm-control-border` → `--wm-border` | `#d8d8d8` | Inactive shape-button border. | | `--wm-lfo-chip-background` | `--wm-surface` | `#fff` | Inactive shape background. | | `--wm-lfo-chip-foreground` | `--wm-foreground` | `#333` | Inactive shape glyph. | | `--wm-lfo-chip-active-background` | `--wm-accent` → `--wm-foreground` | `#111` | Selected shape background. | | `--wm-lfo-chip-active-foreground` | `--wm-accent-foreground` | `#fff` | Selected shape glyph. | | `--wm-lfo-label` | `--wm-foreground-muted` → `--wm-foreground` | `#666` | Rate/depth labels. | | `--wm-foreground-muted` | literal | `#666` | Shared muted-label fallback. | | `--wm-lfo-input-width` | literal | `5.5rem` | Preferred range width, bounded by the available container width. | | `--wm-lfo-track` | `--wm-control-track` | `#f3f3f3` | Range tracks. | | `--wm-control-track` | literal | `#f3f3f3` | Shared range-track fallback. | | `--wm-lfo-thumb` | `--wm-control-thumb` | `#111` | Range thumbs. | | `--wm-control-thumb` | literal | `#111` | Shared range-thumb fallback. | | `--wm-lfo-value` | `currentColor` | Label current color | Formatted readouts. | | `--wm-lfo-wave-height` | `--wm-surface-sm` | `72px` | Waveform viewport height, on the shared drawing-surface scale. | | `--wm-lfo-wave-background` | `--wm-surface-inverse` | `#111` | Waveform viewport. | | `--wm-surface-inverse` | literal | `#111` | Shared inverse-surface fallback. | | `--wm-lfo-wave` | `--wm-foreground-on-inverse` | `#fff` | Waveform curve. | | `--wm-foreground-on-inverse` | literal | `#fff` | Shared inverse-foreground fallback. | | `--wm-lfo-head` | `--wm-accent` | `#c0392b` | Phase head. | | `--wm-accent` | literal | `#c0392b` | Shared phase-accent fallback. | | `--wm-focus` | `--wm-focus-ring` | `currentColor` | Button/range focus outline. | | `--wm-focus-ring` | literal | `currentColor` | Shared focus fallback. | **Classes** `classNames` is additive. Rate and depth nodes carry both their shared and specific classes. | Node | Canonical class(es) | `classNames` key(s) | |---|---|---| | Root | `.wui-lfo` | `root` | | Control row | `.wui-lfo__row` | `row` | | Run button | `.wui-lfo__run` | `run` | | Shape group | `.wui-lfo__shapes` | `shapes` | | Shape button | `.wui-lfo__shape` | `shape` | | Rate label/control | `.wui-lfo__control .wui-lfo__rate-control` | `control`, `label`, `rateControl` | | Depth label/control | `.wui-lfo__control .wui-lfo__depth-control` | `control`, `label`, `depthControl` | | Rate input | `.wui-lfo__input .wui-lfo__rate` | `input`, `rateInput` | | Depth input | `.wui-lfo__input .wui-lfo__depth` | `input`, `depthInput` | | Rate value | `.wui-lfo__value .wui-lfo__rate-value` | `value`, `rateValue` | | Depth value | `.wui-lfo__value .wui-lfo__depth-value` | `value`, `depthValue` | | Wave viewport | `.wui-lfo__wave` | `wave` | | Wave curve | `.wui-lfo__curve` | `curve` | | Phase head | `.wui-lfo__head` | `head` | Runtime state also adds `.is-disabled` to the root and `.is-active.on` to the selected shape. **Parts** Canonical tokens are `root`, `row`, `run`, `shapes`, `shape`, `wave`, `curve`, and `head`. Each rate/depth label has `control label rate-control` or `control label depth-control`; each range has `input rate` or `input depth`; each readout has `value rate-value` or `value depth-value`. `LfoParts` adds its matching generic and specific tokens alongside all of them.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/score/element/play/score-synth-panel/) ## Related presenters - [`@webmusic/ui/parameter`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/parameter/) - [`@webmusic/ui/macro`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/macro/) - [`@webmusic/ui/envelope`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/envelope/) - [`@webmusic/ui/eq`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/eq/) --- # @webmusic/ui/macro > Renders one normalized macro control or a compound rack of macros over structural bindings. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/macro/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountMacro()` renders one normalized rotary control beside caller-owned destination readouts. Its `MacroBinding` owns the value, label, disabled state and every mapped target; the presenter only formats and paints them. The Live Demo mounts the compound rack so its first binding exposes this State while the strip also exercises every rack Option. Use [parameter]() for unrelated controls with independent bounds, or `mountMacroRack()` when several macro bindings should be stacked as one compound presenter. [``]() already composes the rack API. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/macro/) ## Import ```ts import {mountMacroRack, type MacroBinding} from '@webmusic/ui/macro'; const bindings: MacroBinding[] = stores.map((store) => ({ snapshot: () => store.macro, setValue: (value) => store.setMacro(value), subscribe: (notify) => store.subscribe(notify), })); const handle = mountMacroRack(host, bindings); ``` ## Compound macro racks `mountMacroRack(host, bindings, options?)` creates and owns one item host per binding, then mounts `mountMacro()` into each. `handle.items` preserves binding order and `handle.item(index)` returns a child handle. Rack `update()` forwards to every child; rack `destroy()` destroys children before removing its own root and stylesheet. The rack is layout and lifecycle composition, not a shared state owner. Each child binding keeps its own snapshot, command and subscription. See the [shared presenter shape]() for the common ownership contract.
    API The entry exports both `mountMacro()` and `mountMacroRack()`. **State** | Member | Kind | Description | |---|---|---| | `label` | `string` | Visible knob label; nullish input normalizes to `'MACRO'`. | | `value` | `number` | Normalized macro value clamped to `0…1` and painted as an integer percentage. | | `targets` | `readonly MacroTargetState[]` | Ordered, read-only destination rows. An empty list paints `Assign .targets`. | | `disabled?` | `boolean` | Strict `true` disables the nested parameter range. | | `targets[].label` | `string` | Destination label. | | `targets[].value` | `number` | Finite display value; non-finite values normalize to `0`. | | `targets[].unit?` | `string` | Suffix appended directly to the formatted target value. | **Binding** | Member | Kind | Description | |---|---|---| | `snapshot()` | `() => MacroState` | Required pull port read on mount and every update. | | `setValue(value)` | `(number) => void \| Promise` | Required command delegated by the nested normalized parameter control. | | `subscribe?(notify)` | `(notify: () => void) => () => void` | Optional external-state subscription released on destruction. | **Options** | Member | Kind | Default | Description | |---|---|---|---| | `mountMacro: onError` | `(error: unknown) => void` | — | Receives macro read/update failures and errors forwarded by the nested parameter presenter. | | `mountMacroRack: onError` | `(error: unknown) => void` | — | Forwarded to every child macro. | | `mountMacroRack: classNames` | `MacroRackClassNames` | `{}` | Adds compatibility classes to rack `root` and each `item` host. | | `mountMacroRack: parts` | `MacroRackParts` | `{}` | Adds part tokens to rack `root` and each `item` host. | | `stylesheet` | `boolean` | `true` | Installs the presenter and nested macro/parameter stylesheets unless explicitly `false`. Applies to both mounts. | **Handle** | Member | Kind | Description | |---|---|---| | `element` | `HTMLElement` | `.wui-macro` for one macro or `.wui-macro-rack` for a compound rack. | | `update()` | `() => void` | Re-reads one binding; the rack variant forwards to all child handles. | | `destroy()` | `() => void` | Idempotently removes subscriptions, nested controls, roots and installed styles. | | `rack.items` | `readonly MacroHandle[]` | Stable child handle array in binding order. | | `rack.item(index)` | `(number) => MacroHandle \| undefined` | Returns the child at an index without exposing private wrappers. | **Accessibility.** The macro value is the accessible native range created by the nested parameter presenter and named by `MacroState.label`; its value text is an integer percentage. Destination rows are ordinary readable text, not controls. `disabled` reaches the native range. A macro rack adds no extra landmark or label, so callers should provide surrounding context when the list needs an accessible group name.
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. Both mounts install their exported styles by default. `mountMacro()` also installs the nested [parameter presenter stylesheet]() for its knob. Set `stylesheet: false` on either mount to omit every owned style node, including nested macro and parameter styles. The host must then supply `macroStyle`, `parameterRackStyle`, and, for a rack, `macroRackStyle`, or its own CSS for their classes and parts. The destination list moves below the knob when the container is narrow. Destination labels and formatted values wrap without truncation; the knob keeps its configured geometry. **Tokens** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-macro-padding` | literal | `.6rem` | Single-macro outer inset compatibility fallback. | | `--wm-macro-border` | `--wm-border` | `#d8d8d8` | Single macro border. | | `--wm-border` | literal | `#d8d8d8` | Shared border fallback. | | `--wm-macro-radius` | `--wm-control-radius` | `0` | Single macro corners. | | `--wm-control-radius` | literal | `0` | Shared radius fallback. | | `--wm-macro-background` | `--wm-surface` | `#fff` | Single macro background. | | `--wm-surface` | literal | `#fff` | Shared surface fallback. | | `--wm-font-family` | `--wm-font` | `system-ui, sans-serif` | Single macro font family; `--wm-font` remains a compatibility fallback. | | `--wm-macro-label` | `--wm-foreground-muted` → `--wm-foreground` | `#666` | Destination labels. | | `--wm-foreground-muted` | literal | `#666` (`#999` empty) | Shared label and empty-state fallback. | | `--wm-macro-foreground` | `--wm-foreground` | `#444` | Destination values. | | `--wm-foreground` | literal | `#444` | Shared value fallback. | | `--wm-macro-rack-gap` | literal | `.55rem` | Vertical gap between macro item hosts. | The inner knob additionally reads the parameter presenter's size, track, fill, pointer, focus, label and value tokens. Macro CSS removes that nested rack's border, padding and background. **Classes** | Node | Canonical class | Configurable key / compatibility class | |---|---|---| | Single root | `.wui-macro` | None on `mountMacro()` | | Knob host | `.wui-macro__control` | None | | Target list | `.wui-macro__targets` | None | | Target row | `.wui-macro__target` | Always also `.dest` | | Target label | `.wui-macro__target-label` | Always also `.dn` | | Target value | `.wui-macro__target-value` | Always also `.dv` | | Empty target state | `.wui-macro__empty` | Always also `.empty` | | Rack root | `.wui-macro-rack` | `classNames.root` | | Rack item host | `.wui-macro-rack__item` | `classNames.item` | Every empty state is a live region — `role="status"`, `aria-live="polite"` — so the message is announced when it appears, not only drawn. The nested parameter control also receives the compatibility classes `knobwrap`, `name`, `knob`, `track`, `arc`, `ptr`, and `pct` on its item, label, control, track, fill, pointer and value nodes. **Parts** `mountMacro()` exposes canonical `root`, `control`, and `targets` parts. Target and empty rows do not receive parts. The nested parameter presenter retains its canonical parts. `mountMacroRack()` exposes `root` and `item`; `MacroRackParts` adds tokens beside those two canonical names.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/score/element/play/score-synth-panel/) ## Related presenters - [`@webmusic/ui/parameter`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/parameter/) - [`@webmusic/ui/envelope`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/envelope/) - [`@webmusic/ui/lfo`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/lfo/) - [`@webmusic/ui/eq`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/eq/) --- # @webmusic/ui/parameter > Renders a flat or grouped rotary parameter rack over caller-owned parameter state. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/parameter/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountParameterRack()` renders one accessible rotary range for every `ParameterRackItem` returned by the binding. The caller owns item identity, values, bounds, grouping and disabled state; the presenter owns the knob SVG, native inputs, formatted readouts and listener cleanup. Use [macro]() when one normalized control drives a list of read-only destinations, or [panel]() when multiple specialized presenters need a shared section skeleton. [``]() already composes parameter racks; mount this entry directly for a rack inside your own layout. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/parameter/) ## Import ```ts import {mountParameterRack} from '@webmusic/ui/parameter'; const handle = mountParameterRack(host, { snapshot: () => store.parameters, setValue: (id, value) => store.setParameter(id, value), subscribe: (notify) => store.subscribe(notify), }); ``` ## Structure and updates `layout: 'flat'` borders each item independently. `layout: 'grouped'` collects items by their `group` string, preserves first-seen group order, and labels non-empty groups. A missing or empty group remains an unlabelled group. Duplicate normalized `id` values are rejected and reported. The presenter rebuilds its item DOM only when layout or structural item fields change: `id`, `label`, bounds, step, unit, group, or choices. Value and disabled changes repaint existing nodes. `setValue()` paints optimistically, then re-reads the authoritative snapshot after the command settles. See the [shared presenter shape]() for host ownership and lifecycle rules.
    API `mountParameterRack(host, binding, options?)` accepts an `HTMLElement` or `ShadowRoot` and returns a `MountedParameterRackHandle`. **State** Finite numeric fields are normalized before painting. Bounds are swapped when `max < min`; values are clamped to the normalized range. A non-empty `options` list replaces numeric bounds with indices `0…options.length - 1` and forces `step: 1`. | Member | Kind | Description | |---|---|---| | `parameters` | `readonly ParameterRackItem[]` | Required ordered item list. A structural change rebuilds the presenter-owned item nodes. | | `disabled?` | `boolean` | Strict `true` disables every item input in addition to per-item disabled state. | | `parameters[].id` | `string` | Required stable identity used by `setValue()`, `itemElement()` and `inputElement()`; duplicates are invalid. | | `parameters[].label` | `string` | Visible label, tooltip and native range accessible name. | | `parameters[].value` | `number` | Current caller-owned value, clamped to the normalized bounds. | | `parameters[].min` | `number` | Lower numeric bound; defaults to `0` when non-finite. | | `parameters[].max` | `number` | Upper numeric bound; defaults to `1` when non-finite. | | `parameters[].step?` | `number` | Positive range step; absent, non-finite or non-positive values become `0.01`. | | `parameters[].unit?` | `string` | Appended by the built-in numeric formatter after a separating space. | | `parameters[].options?` | `readonly string[]` | Non-empty choice labels indexed by the control value. | | `parameters[].group?` | `string` | Group key and visible heading in grouped layout. | | `parameters[].disabled?` | `boolean` | Strict `true` disables this item without disabling its siblings. | **Binding** | Member | Kind | Description | |---|---|---| | `snapshot()` | `() => ParameterRackState` | Required pull port read on mount, notification, command settlement and `update()`; failures go to `onError`. | | `setValue(id, value)` | `(string, number) => void \| Promise` | Required command receiving the stable id and normalized range value. | | `subscribe?(notify)` | `(notify: () => void) => () => void` | Optional external-state subscription; its disposer is released by `destroy()`. | **Options** | Member | Kind | Default | Description | |---|---|---|---| | `layout` | `'flat' \| 'grouped'` | `'flat'` | Chooses per-item borders or named group containers. | | `emptyLabel` | `string` | `'No parameters'` | Placeholder text when `parameters` is empty. | | `formatValue` | `(parameter, value) => string` | Built-in choice/numeric formatter | Formats the visible readout and `aria-valuetext`; thrown errors are reported before the built-in fallback is used. | | `classNames` | `ParameterRackClassNames` | `{}` | Adds compatibility classes for all thirteen presenter node kinds. | | `parts` | `ParameterRackParts` | `{}` | Adds part tokens for all thirteen presenter node kinds. | | `onError` | `(error: unknown) => void` | — | Receives snapshot, duplicate-id, formatter, command, subscription, stabilization and cleanup failures. | | `stylesheet` | `boolean` | `true` | Installs the presenter's stylesheet unless explicitly `false`, for a light-DOM host that cannot inject CSS into its page. | **Handle** | Member | Kind | Description | |---|---|---| | `element` | `HTMLElement` | Presenter-owned `.wui-parameter-rack` root. | | `itemElement(id)` | `(string) => HTMLElement \| undefined` | Returns the current item container for an id. | | `inputElement(id)` | `(string) => HTMLInputElement \| undefined` | Returns the current native range for an id. | | `emptyElement()` | `() => HTMLElement \| undefined` | Returns the placeholder only while the list is empty. | | `update()` | `() => void` | Re-reads and repaints the binding; re-entrant updates are bounded to 32 passes. | | `destroy()` | `() => void` | Idempotently removes item listeners, subscription, root and installed stylesheet. | **Accessibility.** Each knob contains a native `input[type="range"]` named by the item label and given formatted `aria-valuetext`. Native range keys provide the keyboard interaction. Named groups use `role="group"` and `aria-labelledby`; empty group names do not manufacture a label. Disabled state reaches the native inputs, and forced-colors rules preserve track, fill, pointer, focus and border contrast.
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. The presenter installs `parameterRackStyle` unless `stylesheet: false`. Override its tokens, add compatibility classes/parts, or supply higher-priority CSS from the host. Groups and knobs wrap within the available container width. Group labels, parameter labels and formatted values can wrap without truncation; knob geometry keeps its configured size. Supply the exported stylesheet or your own CSS from the host when opting out. **Tokens** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-parameter-gap` | literal | `.6rem` | Gap between flat items or groups. | | `--wm-parameter-padding` | literal | `.6rem` | Root-inset compatibility fallback. | | `--wm-parameter-border` | `--wm-border` | `#d8d8d8` | Root and fallback item/group border color. | | `--wm-control-border` | `--wm-border` | `#d8d8d8` | Shared non-primary control border fallback. | | `--wm-border` | literal | `#d8d8d8` | Shared border fallback. | | `--wm-parameter-radius` | `--wm-control-radius` | `0` | Root radius and fallback item/group radius. | | `--wm-control-radius` | literal | `0` | Shared radius fallback. | | `--wm-parameter-background` | `--wm-surface` | `#fff` | Root and fallback item/group background. | | `--wm-surface` | literal | `#fff` | Shared surface fallback. | | `--wm-parameter-foreground` | `--wm-foreground` | `#444` | Root, heading and value fallback color. | | `--wm-foreground` | literal | `#444` root / `#111` pointer | Shared foreground fallback. | | `--wm-font-family` | literal | `system-ui, sans-serif` | Root UI font. | | `--wm-parameter-group-gap` | literal | `.4rem` | Heading-to-items gap. | | `--wm-parameter-item-border` | `--wm-parameter-border` → `--wm-control-border` → `--wm-border` | `#d8d8d8` | Flat item and group border. | | `--wm-parameter-item-radius` | `--wm-parameter-radius` → `--wm-control-radius` | `0` | Flat item and group radius. | | `--wm-parameter-item-background` | `--wm-parameter-background` → `--wm-surface` | `#fff` | Flat item and group surface. | | `--wm-parameter-heading` | `--wm-parameter-foreground` → `--wm-foreground` | `#444` | Group heading text. | | `--wm-parameter-item-gap` | literal | `.5rem` | Gap between items inside a group. | | `--wm-parameter-label-width` | literal | `5rem` | Maximum wrapping width of labels and values, bounded by the container. | | `--wm-parameter-label` | `--wm-foreground-muted` → `--wm-foreground` | `#666` (`#777` empty) | Item label and empty-state text. | | `--wm-foreground-muted` | literal | `#666` / `#777` | Shared muted-text fallback. | | `--wm-parameter-size` | literal | `46px` | Knob control width and height. | | `--wm-parameter-track` | literal | `#dcdcdc` | Knob background arc. | | `--wm-parameter-stroke` | literal | `5` | Track and fill SVG stroke width. | | `--wm-parameter-fill` | `--wm-accent` | `#999` | Filled value arc. | | `--wm-accent` | literal | `#999` | Shared fill fallback. | | `--wm-parameter-pointer` | `--wm-foreground` | `#111` | Radial pointer. | | `--wm-focus` | `--wm-focus-ring` | `currentColor` | Focus-within outline. | | `--wm-focus-ring` | literal | `currentColor` | Shared focus fallback. | | `--wm-parameter-value` | `--wm-parameter-foreground` → `--wm-foreground` | `#444` | Formatted value text. | **Classes** `classNames` adds compatibility classes alongside canonical names. | Node | Canonical class | `classNames` key | |---|---|---| | Root | `.wui-parameter-rack` | `root` | | Group | `.wui-parameter-rack__group` | `group` | | Group heading | `.wui-parameter-rack__group-label` | `groupLabel` | | Group item list | `.wui-parameter-rack__items` | `items` | | Item | `.wui-parameter-rack__item` | `item` | | Item label | `.wui-parameter-rack__label` | `label` | | Knob wrapper | `.wui-parameter-rack__control` | `control` | | Track path | `.wui-parameter-rack__track` | `track` | | Fill path | `.wui-parameter-rack__fill` | `fill` | | Pointer line | `.wui-parameter-rack__pointer` | `pointer` | | Native range | `.wui-parameter-rack__input` | `input` | | Value readout | `.wui-parameter-rack__value` | `value` | | Empty placeholder | `.wui-parameter-rack__empty` | `empty` | Every empty state is a live region — `role="status"`, `aria-live="polite"` — so the message is announced when it appears, not only drawn. **Parts** `parts` adds tokens beside the matching canonical names: `root`, `group`, `group-label`, `items`, `item`, `label`, `control`, `track`, `fill`, `pointer`, `input`, `value`, and `empty`. The camel-case keys are `root`, `group`, `groupLabel`, `items`, `item`, `label`, `control`, `track`, `fill`, `pointer`, `input`, `value`, and `empty`.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/score/element/play/score-synth-panel/) ## Related presenters - [`@webmusic/ui/macro`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/macro/) - [`@webmusic/ui/envelope`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/envelope/) - [`@webmusic/ui/lfo`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/lfo/) - [`@webmusic/ui/eq`](https://koperative-lab.github.io/WebMusic/uikit/parameters-gestures/eq/) --- # @webmusic/ui/minimap > A DPR-aware overview canvas with an accessible draggable and keyboard-operable range brush. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/transport-time/minimap/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountMinimap()` renders a device-pixel-ratio-aware overview canvas beneath a draggable range brush. It paints the caller's `MinimapState`, asks the binding to draw the overview, and routes range and seek intent back without owning a waveform, score, viewport, or playhead model. Use [timeline]() when labels and editable regions matter, [transport]() for playback controls, or [playlist]() for an ordered queue. Mount this presenter directly when your own renderer and viewport store should remain authoritative. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/transport-time/minimap/) ## Import ```ts import {mountMinimap} from '@webmusic/ui/minimap'; const handle = mountMinimap(host, { snapshot: () => store.viewport, draw: ({context, width, height}) => drawOverview(context, width, height), }); ``` ## Canvas and range geometry `minimum` and `maximum` are normalized into ascending domain bounds. The brush appears only when both `start` and `end` are finite and the domain has positive length; its edges are sorted and clamped before painting. That makes the brush a projection of binding state, not a second viewport model. Before every draw, the presenter sizes the canvas backing store from the root's CSS pixels and the current device pixel ratio, then resets the 2D transform so `draw()` receives CSS-pixel `width` and `height`. A `ResizeObserver`, binding notification, or explicit `update()` redraws both canvas and brush. ## Range and seek commands Dragging near either brush edge resizes that edge; dragging its center moves the complete range. Clicking the canvas centers the current-width brush at that value when `setRange()` exists, then also calls `seek()` when provided. See the [shared presenter shape]() for common ownership and cleanup rules. `setRange()` may settle asynchronously. The presenter paints an optimistic brush immediately, keeps only the newest command revision, then returns to authoritative snapshot state on fulfillment or rejection. `seek()` may also return a promise, but it has no presenter-owned optimistic playhead; the binding must notify when its visible draw state changes.
    API `mountMinimap(host, binding, options?)` accepts an `HTMLElement` or `ShadowRoot` and returns a `MinimapHandle`. **State** | Member | Kind | Description | |---|---|---| | `minimum` | `number` | One domain bound. Non-finite values normalize to `0`; it is sorted with `maximum`. | | `maximum` | `number` | The other domain bound. Non-finite values fall back to normalized `minimum`; it is sorted with `minimum`. | | `start?` | `number` | One brush edge. Both edges must be finite; they are sorted and clamped to the domain. | | `end?` | `number` | The other brush edge. Omission of either edge hides the brush. | | `disabled?` | `boolean` | Strict `true` blocks range and seek pointer commands, removes brush keyboard access, and dims the brush. | **Binding** | Member | Kind | Description | |---|---|---| | `snapshot()` | `() => MinimapState` | Required pull port, read on mount and every update. A thrown error reaches `onError` and preserves the previous normalized state. | | `draw(frame)` | `(CanvasStageFrame) => void` | Required overview renderer. Receives the 2D context, canvas, CSS-pixel size, pixel ratio, and current time. | | `setRange?(start, end)` | `(number, number) => void \| Promise` | Receives sorted, clamped range bounds from pointer or keyboard movement. Its presence makes the brush interactive. | | `seek?(value)` | `(number) => void \| Promise` | Receives a domain-clamped value when the overview canvas is pressed. | | `subscribe?(notify)` | `(notify: () => void) => () => void` | Optional change subscription; its disposer is called by `destroy()` and on same-host remount. | **Options** | Member | Kind | Default | Description | |---|---|---|---| | `label` | `string` | `'Visible range'` | Accessible name of the brush slider. | | `fallbackWidth` | `number` | `320` | CSS-pixel canvas width used only when neither root nor canvas reports a positive width. | | `fallbackHeight` | `number` | `40` | CSS-pixel canvas height used only when neither root nor canvas reports a positive height. | | `edgeSize` | `number` | `8` | CSS-pixel distance from a brush edge that selects resize rather than move; non-positive values use the default. | | `keyboardStep` | `number` | `0.25` | Fraction of current brush width moved by Arrow keys, clamped to `0.001…1`. | | `classNames` | `MinimapClassNames` | `{}` | Adds compatibility classes for `root`, `canvas`, and `brush`. | | `parts` | `MinimapParts` | `{}` | Adds part tokens for `root`, `canvas`, and `brush`. | | `onError` | `(error: unknown) => void` | — | Receives draw, snapshot, command, subscription, observer, and cleanup failures. | | `stylesheet` | `boolean` | `true` | Installs the presenter's stylesheet unless explicitly `false`, for a light-DOM host that cannot inject CSS into its page. | **Handle** | Member | Kind | Description | |---|---|---| | `element` | `HTMLElement` | Presenter-owned `.wui-minimap` root. | | `canvas` | `HTMLCanvasElement` | DPR-sized, display-only overview canvas. | | `brush` | `HTMLDivElement` | Range brush with slider semantics; it can be hidden or removed from the tab order by state/binding capability. | | `update()` | `() => void` | Re-reads state, redraws the canvas, and paints the brush; becomes a no-op after destruction. | | `destroy()` | `() => void` | Idempotently invalidates pending commands, disconnects observation, disposes the subscription, and removes DOM and stylesheet. | **Accessibility.** The canvas is `aria-hidden`; the brush is the accessible `role="slider"` with dynamic `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, and a two-edge `aria-valuetext`. ArrowLeft and ArrowRight move it by `keyboardStep × current range width`; Shift+Arrow moves one complete range width, and Home/End align it to the domain bounds. The brush leaves the tab order when hidden, disabled, or missing `setRange()`, and exposes `aria-disabled` otherwise. Canvas click seeking is pointer-only—the focusable brush remains the keyboard range control—and no caller node is rewritten.
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. The presenter root uses the shared [`--wm-component-*` surface](), or `--wm-minimap-surface-*` for a minimap-only override. The full-bleed inner viewport retains the `--wm-minimap-*` geometry tokens below. The canvas drawing itself is caller-owned. **Tokens** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-minimap-background` | `--wm-surface` | `transparent` | Inner viewport background. | | `--wm-minimap-foreground` | `--wm-waveform` → `--wm-foreground` | `#444` | Foreground inherited by caller drawing code. | | `--wm-minimap-border` | Literal | `0` | Inner viewport border shorthand. | | `--wm-minimap-radius` | `--wm-control-radius` | `0` | Inner viewport clipping and corners. | | `--wm-minimap-brush-border` | `--wm-selection` → `--wm-foreground` | `#111` | Brush border. | | `--wm-minimap-brush` | `--wm-selection-fill` | `rgba(127, 127, 127, .18)` | Brush fill. | | `--wm-focus` | `currentColor` | Current text color | Brush keyboard focus outline. | **Classes** `classNames` adds compatibility classes alongside these canonical names; it never replaces them. | Node | Canonical class | `classNames` key | |---|---|---| | Root | `.wui-minimap` | `root` | | Viewport | `.wui-minimap__viewport` | — | | Canvas | `.wui-minimap__canvas` | `canvas` | | Brush | `.wui-minimap__brush` | `brush` | **Parts** `parts` adds tokens alongside the canonical tokens and removes duplicates. | Node | Canonical part | `parts` key | |---|---|---| | Root | `root` | `root` | | Canvas | `canvas` | `canvas` | | Brush | `brush` | `brush` | The mount installs `minimapStyle` by default. Set `stylesheet: false` to omit it and supply the exported stylesheet or your own CSS from the host. Style these canonical classes and parts after the installed style when you need caller-owned overrides.
    ## Used by Mount this presenter with application-owned state and bindings. ## Related presenters - [`@webmusic/ui/transport`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/transport/) - [`@webmusic/ui/timeline`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/timeline/) - [`@webmusic/ui/playlist`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/playlist/) - [`@webmusic/ui/track-list`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/track-list/) --- # @webmusic/ui/playlist > Playlist transport, track selection, seeking, and per-entry state in one presenter. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/transport-time/playlist/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountPlaylist()` renders previous, play/pause, next, and seek controls over an ordered list of entries. It paints caller-owned `PlaylistState` and routes every transport or selection gesture through a required structural `PlaylistBinding`; it never owns a player, queue, decoder, or file. Use [transport]() for one current item without a queue, [timeline]() for labelled time regions, or [minimap]() for overview-range navigation. Mount this presenter directly when entry identity, loading policy, and player commands live in your store. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/transport-time/playlist/) ## Import ```ts import {mountPlaylist} from '@webmusic/ui/playlist'; const handle = mountPlaylist(host, { snapshot: () => store.playlist, toggle: () => player.toggle(), previous: () => player.previous(), next: () => player.next(), seek: (fraction) => player.seek(fraction), select: (id) => player.select(id), }); ``` ## Entry identity and patching `items` is the caller's ordered queue. Each item supplies a stable `id`, a label, optional preformatted duration, optional active state, and optional `loading` or `error` status. The presenter deliberately does not format duration or decide whether an errored entry may be selected; those are domain policies implemented by the binding. The row list is rebuilt only when item ids, labels, or order change. Active state, status, and duration are patched onto existing rows so frequent progress notifications do not steal focus or cancel an in-flight row click. Keep ids unique and stable if code uses `controls.item(id)`. Empty or whitespace-only ids reject the snapshot through `onError`; the prior DOM remains. A rebuilt row list restores focus to the same id when that entry remains present. ## Binding and command settlement All five user commands are required and may return promises. The presenter calls the requested command, then re-reads the authoritative snapshot after it fulfills; synchronous throws and current-command rejections reach `onError`. Older settlement may refresh accepted state but cannot report over a newer command. Settlement after replacement/destruction is ignored, and error-callback faults are contained. Subscription notifications can repaint independently. See the [shared presenter shape]() for common ownership and cleanup rules. The presenter does not apply an optimistic queue transition. `toggle()`, `previous()`, `next()`, `seek()`, and `select()` must update caller state—then notify, fulfill, or both—for the visible state to change.
    API `mountPlaylist(host, binding, options?)` accepts an `HTMLElement` or `ShadowRoot` and returns a `PlaylistHandle`. **State** | Member | Kind | Description | |---|---|---| | `playing` | `boolean` | Strict `true` chooses the pause glyph/label; otherwise uses play. | | `progress` | `number` | Current-entry position. Finite values clamp to `0…1`; non-finite values paint as `0`. | | `disabled?` | `boolean` | Strict `true` disables transport controls and blocks every command, including row selection; the root exposes `aria-disabled`. | | `items` | `readonly PlaylistItem[]` | Ordered entries. Absence at runtime is treated as an empty list. | `PlaylistItem` contains required `id` and `label`, optional preformatted `duration`, optional `active`, and optional `status: 'loading' | 'error'`. Strict `active: true` sets `aria-current`; status adds a styling data attribute. The presenter does not synthesize status text or mutate the item objects. **Binding** | Member | Kind | Description | |---|---|---| | `snapshot()` | `() => PlaylistState` | Required pull port, read on mount, notification, command fulfillment, and explicit update. A throw reaches `onError` and leaves the current DOM unchanged. | | `toggle()` | `() => void \| Promise` | Required play/pause intent from the central transport button. | | `previous()` | `() => void \| Promise` | Required previous-entry intent. | | `next()` | `() => void \| Promise` | Required next-entry intent. | | `seek(value)` | `(number) => void \| Promise` | Required current-entry seek intent; receives a normalized `0…1` fraction. | | `select(id)` | `(string) => void \| Promise` | Required entry-selection intent from click, Enter, or Space. | | `subscribe?(notify)` | `(notify: () => void) => () => void` | Optional change subscription; its disposer is called by `destroy()` and on same-host remount. | **Options** | Member | Kind | Default | Description | |---|---|---|---| | `label` | `string` | `'Playlist'` | Accessible name of the ordered entry list. | | `stylesheet` | `boolean` | `true` | Installs `playlistStyle` unless explicitly `false`. | | `transport` | `boolean` | `true` | False hides the transport bar for selection-only companion use; row selection remains available. | | `previousLabel` / `nextLabel` | `string` | `Previous` / `Next` | Accessible navigation names. | | `playLabel` / `pauseLabel` | `string` | `Play` / `Pause` | Accessible toggle names selected by playback state. | | `seekLabel` | `string` | `Seek` | Accessible range name. | | `classNames` | `PlaylistClassNames` | `{}` | Adds classes for `root`, `bar`, `button`, `previous`, `play`, `next`, `seek`, `list`, `item`, `number`, `label`, and `duration`. | | `parts` | `PlaylistParts` | `{}` | Adds part tokens for the same node roles, preserving canonical tokens. | | `onError` | `(error: unknown) => void` | — | Receives snapshot, command, subscription, and cleanup failures. | **Handle** | Member | Kind | Description | |---|---|---| | `element` | `HTMLElement` | Presenter-owned `.wui-playlist` root. | | `controls.previous` | `HTMLButtonElement` | Always-present previous button. | | `controls.toggle` | `HTMLButtonElement` | Always-present play/pause button. | | `controls.next` | `HTMLButtonElement` | Always-present next button. | | `controls.seek` | `HTMLInputElement` | Always-present range with DOM values `0…1000`. | | `controls.list` | `HTMLOListElement` | Always-present ordered list. | | `controls.item(id)` | `(string) => HTMLLIElement \| undefined` | Returns the currently rendered row mapped to an entry id. | | `update()` | `() => void` | Re-reads and patches state; becomes a no-op after destruction. | | `destroy()` | `() => void` | Idempotently disposes the subscription and removes presenter DOM and any installed stylesheet. | **Accessibility.** Previous, toggle, and next are native buttons with accessible names; the toggle name follows `playing`. Seek is a native range labelled `Seek`, and all four transport controls reflect `disabled` through the native attribute. The ordered list receives `label` as its accessible name. Each row is a focusable `role="button"` with click, Enter, and Space activation plus `aria-current` for the active item. Root `disabled` blocks row selection as well as transport commands. Loading/error remain visual data states, so the binding owns status messaging and per-item selection policy. Caller nodes outside the presenter root are never rewritten.
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. The installed stylesheet resolves presenter-specific playlist tokens through shared `--wm-*` values and then literals. **Tokens** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-playlist-height` | `--wm-control-size` → `--wui-row-h` | `2rem` | Height of the transport buttons and of the seek rail. | | `--wm-playlist-track` | `--wm-surface-muted` | `#f3f3f3` | Unfilled seek rail. | | `--wm-playlist-thumb` | `--wm-foreground` | `#111` | Seek handle fill. | | `--wm-playlist-thumb-radius` | `--wm-control-radius` | `0` | Seek handle corners. | | `--wm-playlist-handle-size` | literal | `.9rem` | Seek handle width. Its height follows the control size. | | `--wm-playlist-border` | `--wm-border` | `#d8d8d8` | Root border. | | `--wm-playlist-radius` | `--wm-control-radius` | `0` | Root, button, and item corners. | | `--wm-playlist-background` | `--wm-surface` | `#fff` | Root background. | | `--wm-playlist-text` | `--wm-foreground` | `#444` | Root text color. | | `--wm-font-family` | `--wm-font` | `system-ui, sans-serif` | Playlist UI font. | | `--wm-playlist-button-border` | `--wm-control-border` → `--wm-border` | `#d8d8d8` | Previous and next button borders. | | `--wm-control-border` | `--wm-border` | `#d8d8d8` | Shared non-primary control border fallback. | | `--wm-playlist-button` | `--wm-surface` | `#fff` | Previous and next button backgrounds. | | `--wm-playlist-button-text` | `--wm-foreground` | `#444` | Previous and next button glyphs. | | `--wm-playlist-primary-border` | `--wm-playlist-button-border` → `--wm-accent` | `#111` | Primary play/pause button border. | | `--wm-playlist-primary-background` | `--wm-playlist-button` → `--wm-accent` | `#111` | Primary play/pause button background. | | `--wm-playlist-primary-foreground` | `--wm-playlist-button-text` → `--wm-accent-foreground` | `#fff` | Primary play/pause button glyph. | | `--wm-playlist-accent` | `--wm-accent` | `#111` | Native seek accent. | | `--wm-playlist-active` | Literal | `rgba(17, 17, 17, .08)` | Active-row background. | | `--wm-playlist-error` | `--wm-error` | `#c0392b` | Error-row text. | | `--wm-focus` | `currentColor` | Current text color | 2px keyboard-focus outline on rows, transport buttons and seek control. | | `--wm-playlist-muted` | `--wm-foreground-muted` | `#666` | Duration text. | **Classes** `classNames` adds compatibility classes alongside the canonical classes for its six public node roles. Labels and durations expose canonical classes but have no `classNames` key. | Node | Canonical class | `classNames` key | |---|---|---| | Root | `.wui-playlist` | `root` | | Transport bar | `.wui-playlist__bar` | `bar` | | Each transport button | `.wui-playlist__button` | `button` | | Seek range | `.wui-playlist__seek` | `seek` | | Ordered list | `.wui-playlist__items` | `list` | | Entry row | `.wui-playlist__item` | `item` | | Entry number | `.wui-playlist__number` | `number` | | Entry label | `.wui-playlist__label` | `label` | | Entry duration | `.wui-playlist__duration` | `duration` | **Parts** `parts` adds tokens alongside canonical tokens and removes duplicates. Each transport button also carries its action modifier. | Node | Canonical part | `parts` key | |---|---|---| | Root | `root` | `root` | | Transport bar | `bar` | `bar` | | Previous button | `button prev` | `button`, `previous` | | Toggle button | `button play` | `button`, `play` | | Next button | `button next` | `button`, `next` | | Seek range | `seek` | `seek` | | Ordered list | `list` | `list` | | Entry row | `item` | `item` | | Entry number | `number` | `number` | | Entry label | `label` | `label` | | Entry duration | `duration` | `duration` | With `stylesheet: false`, the presenter installs no style node; the caller must style these classes and parts itself.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/audio/element/play/audio-playlist/) ## Related presenters - [`@webmusic/ui/transport`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/transport/) - [`@webmusic/ui/timeline`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/timeline/) - [`@webmusic/ui/minimap`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/minimap/) - [`@webmusic/ui/track-list`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/track-list/) --- # @webmusic/ui/timeline > A domain-neutral ruler with ticks, regions, markers, loops, selection, playhead, and seeking. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/transport-time/timeline/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountTimeline()` renders a ruler over a lane of ranged regions and point markers, with optional loop, selection, playhead, and seeking layers. It paints caller-owned `TimelineState`; the presenter owns only its DOM and forwards seek and region-selection intent through a structural `TimelineBinding`. Use [transport]() for playback controls, [minimap]() for moving a viewport over a dense overview, or [playlist]() for ordered entries. The [``]() element composes this presenter; mount it directly when the time domain and region model belong to your own store. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/transport-time/timeline/) ## Import ```ts import {mountTimeline} from '@webmusic/ui/timeline'; const handle = mountTimeline(host, { snapshot: () => store.timeline, }); ``` ## Domain and geometry `duration` defines the complete non-negative domain. `viewport` narrows the visible interval; an absent, reversed, or empty viewport falls back to the full duration. Regions are clipped to that interval, while a point marker is rendered only when its `start` lies inside it. Duplicate or empty region and tick ids are ignored. Supplying `ticks` gives the ruler domain-authored positions and labels. Omitting it generates a numeric ruler from `majorStep`, or from a readable interval derived from the viewport. `loop`, `selection`, and `playhead` are display state rather than a second editable model: the binding must publish any changes it wants the presenter to retain. Ruler label density follows the available width and measured text size. Major endpoint labels have priority, then other major labels, then minor labels that fit. Visible labels remain inside the ruler with space between them; a label wider than the available ruler is hidden. All tick lines, original label text and tick titles remain present. A resize or font-size change restores labels when room becomes available. Labels share the top ruler line; minor tick rules occupy the lower half. Ranged regions keep their exact clipped proportions, including very short intervals. Their visual borders do not enlarge the button's hit box or cover a neighbor. Point markers retain a 2px minimum width. Narrow regions keep complete accessible labels; the native playhead range remains available for precise keyboard navigation. ## Binding and command settlement The binding remains authoritative. A subscription notification schedules at most one repaint per animation frame; explicit `update()` repaints immediately. See the [shared presenter shape]() for common ownership and cleanup rules. Ruler size observation only repositions labels; it does not pull a new state, rebuild regions or change seek values. Browsers without `ResizeObserver` use a window resize listener; ordinary playhead paints do not measure labels. `seek()` and `selectRegion()` are optional and may return promises. When present, lane clicks and region buttons call them and repaint after settlement; a rejected command is routed to `onError`. Without `seek()`, the hidden range is removed from interaction. Without `selectRegion()`, every region button is disabled.
    API `mountTimeline(host, binding, options?)` accepts an `HTMLElement` or `ShadowRoot` and returns a `TimelineHandle`. **State** | Member | Kind | Description | |---|---|---| | `duration` | `number` | Complete domain. Non-finite values fall back to `1`; zero and negative values normalize to `Number.EPSILON`. | | `viewport?` | `TimelineRange` | Visible `{start, end}` interval, clamped to `0…duration`; it must have `end > start`, otherwise the full duration is used. | | `playhead?` | `number` | Position of the playhead and seek input. Non-finite values become `0`; the line is hidden outside the viewport. | | `ticks?` | `readonly TimelineTick[]` | Domain ticks. Empty/duplicate ids, non-finite positions, and ticks outside the viewport are skipped. Omission generates major ticks. | | `selection?` | `TimelineRange` | Display-only selected interval, clamped to the viewport and hidden unless `end > start`. | | `loop?` | `TimelineRange` | Display-only loop interval, clamped to the viewport and hidden unless `end > start`. | | `regions` | `readonly TimelineRegion[]` | Required ranged regions and point markers. An omitted `end` makes a marker; ranges are clipped to the viewport. | | `disabled?` | `boolean` | Strict `true` disables seeking and every region button without removing painted state. | `TimelineTick` contains required `id` and `position`, optional `label`, and optional `level: 'major' | 'minor'`. `TimelineRegion` contains required `id` and `start`, plus optional `end`, `label`, `color`, `selected`, and `disabled` fields. A region's strict `selected: true` sets its pressed state; strict `disabled: true` disables only that region. **Binding** | Member | Kind | Description | |---|---|---| | `snapshot()` | `() => TimelineState` | Required pull port, read on mount and every update. A thrown error reaches `onError` and leaves the current DOM unchanged. | | `seek?(position)` | `(number) => void \| Promise` | Receives an absolute, viewport-clamped position from lane pointer input or the hidden range. | | `selectRegion?(id, options)` | `(string, TimelineSelectOptions) => void \| Promise` | Receives the region id and `{additive}`, which is true for Ctrl/Command-click. | | `subscribe?(notify)` | `(notify: () => void) => () => void` | Optional change subscription. Its disposer is called by `destroy()` and on same-host remount. | **Options** | Member | Kind | Default | Description | |---|---|---|---| | `label` | `string` | `'Timeline'` | Accessible name of the root group and prefix for the hidden playhead range. | | `majorStep` | `number` | Derived readable interval | Fixed generated-tick interval. Non-positive or non-finite values use the derived interval. | | `keyboardStep` | `number` | `majorStep / 4` | Step of the hidden range. Non-positive or non-finite values use the default. | | `formatPosition` | `(position: number) => string` | Adaptive numeric formatter | Formats generated tick labels and seek `aria-valuetext`; supplied tick labels take precedence. | | `classNames` | `TimelineClassNames` | `{}` | Adds compatibility classes for `root`, `ruler`, `tick`, `tickLabel`, `lane`, `region`, `marker`, `label`, `selection`, `loop`, `playhead`, and `seek`. | | `parts` | `TimelineParts` | `{}` | Adds part tokens for the same twelve node roles. | | `onError` | `(error: unknown) => void` | — | Receives snapshot, command, subscription, and cleanup failures. | | `stylesheet` | `boolean` | `true` | Installs the presenter's stylesheet unless explicitly `false`, for a light-DOM host that cannot inject CSS into its page. | **Handle** | Member | Kind | Description | |---|---|---| | `element` | `HTMLElement` | Presenter-owned `.wui-timeline` root. | | `ruler` | `HTMLElement` | Always-present ruler node. | | `lane` | `HTMLElement` | Always-present region and overlay lane. | | `seek` | `HTMLInputElement` | Always-created visually hidden range; disabled and interaction-hidden when `seek` is absent. | | `regionElement(id)` | `(string) => HTMLButtonElement \| undefined` | Returns a currently rendered region or marker button by id. | | `update()` | `() => void` | Re-reads state immediately and repaints; becomes a no-op after destruction. | | `destroy()` | `() => void` | Idempotently cancels a pending frame, disposes the subscription and resize observation/listener, and removes presenter DOM and stylesheet. | **Accessibility.** The root is a labelled `role="group"`; the ruler and all display-only overlays are hidden from the accessibility tree. Regions are native buttons with an accessible label and `aria-pressed`, and disabled state uses the native `disabled` attribute. The playhead control is a visually hidden labelled range, so native Arrow, Page, Home, and End behavior uses `keyboardStep`; its formatted position is exposed through `aria-valuetext`. Lane pointer seeking has no separate keyboard gesture—the range is its keyboard equivalent. Focus outlines are painted on the lane or region without rewriting unrelated caller nodes.
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. The installed stylesheet resolves presenter-specific `--wm-timeline-*` tokens through shared `--wm-*` values and then literals. A region's `color` is written to the internal `--wui-timeline-region-color`; it takes precedence over the theme's region fill and border source. Region borders use an inset shadow so they do not alter geometry. Forced-colors mode uses an inward outline, including a distinct selected/focus outline, because shadows may be suppressed. `--wm-timeline-marker-width` is read from each marker's inline width declaration. **Tokens** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-timeline-ruler-height` | Literal | `1.65rem` | Ruler grid-row height. | | `--wm-timeline-lane-height` | `--wm-surface-sm` | `72px` | Minimum lane grid-row height, on the shared drawing-surface scale. | | `--wm-timeline-foreground` | `--wm-foreground` | `#222` | Root text color. | | `--wm-font-family` | `--wm-font` | `system-ui, sans-serif` | Timeline UI font. | | `--wm-timeline-border` | `--wm-control-border` → `--wm-border` | `#d8d8d8` | Ruler and lane borders. | | `--wm-control-border` | `--wm-border` | `#d8d8d8` | Shared non-primary control border fallback. | | `--wm-timeline-radius` | `--wm-control-radius` | `0` | Outer ruler and lane corners. | | `--wm-timeline-ruler-background` | `--wm-surface-muted` | `#f3f3f3` | Ruler background. | | `--wm-timeline-muted` | `--wm-foreground-muted` | `#666` | Ruler label color. | | `--wm-grid-major` | Literal | `#c8c8c8` | Major tick rule. | | `--wm-grid-minor` | Literal | `#dedede` tick / translucent black lane grid | Minor tick and lane grid rules. | | `--wm-font-mono` | Literal | `ui-monospace, monospace` | Tick label font. | | `--wm-timeline-background` | `--wm-surface` | `#fff` | Lane background. | | `--wm-timeline-grid-size` | Literal | `2rem` | Horizontal repeat size of the lane grid. | | `--wm-focus` | `currentColor` | Current text color | Keyboard focus outlines. | | `--wm-timeline-region-border` | Region color → `--wm-accent` mixed 70% with black | Derived | Region and marker border. | | `--wm-timeline-region-radius` | `--wm-control-radius` | `0` | Region and marker corners. | | `--wm-timeline-region` | `--wm-accent` | `#111` | Region fill when the state supplies no color. | | `--wm-timeline-region-foreground` | `--wm-accent-foreground` | `#fff` | Region label text. | | `--wm-selection` | `--wm-foreground` for pressed regions | `#4869d8` overlay / `#111` pressed ring | Selection boundaries and selected-region inset ring. | | `--wm-selection-fill` | Literal | `rgba(127, 127, 127, .18)` | Selection overlay fill. | | `--wm-loop` | Literal | `#2f8f5b` | Loop boundary and derived translucent fill. | | `--wm-timeline-playhead-width` | Literal | `2px` | Playhead line width. | | `--wm-playhead` | Literal | `#c0392b` | Playhead color. | | `--wm-timeline-marker-width` | Literal | `2px` | Point-marker width. | **Classes** `classNames` adds compatibility classes alongside these canonical names; marker hooks are added in addition to the region hook. | Node | Canonical class | `classNames` key | |---|---|---| | Root | `.wui-timeline` | `root` | | Ruler | `.wui-timeline__ruler` | `ruler` | | Tick | `.wui-timeline__tick` | `tick` | | Tick label | `.wui-timeline__tick-label` | `tickLabel` | | Lane | `.wui-timeline__lane` | `lane` | | Region button | `.wui-timeline__region` | `region` | | Point marker modifier | `.wui-timeline__marker` | `marker` | | Region label | `.wui-timeline__region-label` | `label` | | Selection overlay | `.wui-timeline__selection` | `selection` | | Loop overlay | `.wui-timeline__loop` | `loop` | | Playhead | `.wui-timeline__playhead` | `playhead` | | Hidden range | `.wui-timeline__seek` | `seek` | **Parts** `parts` adds tokens alongside canonical tokens and removes duplicates. A point marker carries both `region` and `marker`. | Node | Canonical part | `parts` key | |---|---|---| | Root | `root` | `root` | | Ruler | `ruler` | `ruler` | | Tick | `tick` | `tick` | | Tick label | `tick-label` | `tickLabel` | | Lane | `lane` | `lane` | | Region button | `region` | `region` | | Point marker | `region marker` | `region` and `marker` | | Region label | `label` | `label` | | Selection overlay | `selection` | `selection` | | Loop overlay | `loop` | `loop` | | Playhead | `playhead` | `playhead` | | Hidden range | `seek` | `seek` | Every mount installs `timelineStyle` by default. With `stylesheet: false`, install the exported stylesheet yourself or supply equivalent structural styling. Style these canonical classes and parts after the installed style when you need caller-owned overrides. Ticks publish `data-label-visible="true|false"`; hiding a visual ruler label never shortens region labels or seek `aria-valuetext`.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/score/element/view/score-view/) ## Related presenters - [`@webmusic/ui/transport`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/transport/) - [`@webmusic/ui/minimap`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/minimap/) - [`@webmusic/ui/playlist`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/playlist/) - [`@webmusic/ui/track-list`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/track-list/) --- # @webmusic/ui/track-list > An accessible selectable index with roving focus and one current row. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/transport-time/track-list/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountTrackList()` renders an accessible index of caller-owned named rows. It paints labels, preformatted detail text, optional swatches, active and disabled state, then routes selection back through the required `TrackListBinding.select()` command. Use it as the semantic counterpart to a graphical lane or canvas: the list owns list and button semantics, roving focus, and keyboard movement while the caller continues to own track identity and selection. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/transport-time/track-list/) ## Import ```ts import {mountTrackList} from '@webmusic/ui/track-list'; const handle = mountTrackList(host, { snapshot: () => store.trackList, select: (id) => store.selectTrack(id), subscribe: (notify) => store.subscribe(notify), }); ``` ## Selection and focus `select(id)` may be synchronous or return a promise. The binding remains authoritative: a command does not make a row active unless the next `snapshot()` does. Rows are patched in place while the list identity stays stable, preserving focus during frequent playback or selection notifications. Only one enabled row is in the page Tab order. Arrow keys move among enabled rows, Home and End jump to the bounds, and Enter or Space selects the focused row. See the [shared presenter shape]() for host and cleanup rules.
    API **State** | Member | Kind | Description | |---|---|---| | `items` | `readonly TrackListItem[]` | Ordered row data painted by the presenter. | | `disabled?` | `boolean` | Strict `true` disables every row without removing it. | Each `TrackListItem` has required string `id` and `label`, plus optional preformatted `detail`, CSS `color`, `active`, and `disabled`. Duplicate ids cannot produce two stable controls and should not be supplied. **Binding** | Member | Kind | Description | |---|---|---| | `snapshot()` | `() => TrackListState` | Required authoritative list and selection state. | | `select(id)` | `(string) => void \| Promise` | Required command invoked for pointer, Enter, or Space selection. | | `subscribe?(notify)` | `(notify: () => void) => () => void` | Publishes State changes; its disposer runs during destruction. | **Options** | Member | Kind | Default | Description | |---|---|---|---| | `label` | `string` | `'Tracks'` | Accessible name applied to the semantic list. | | `ordered` | `boolean` | `false` | Renders an `
      ` instead of `
        ` when order is meaningful. | | `emptyLabel` | `string` | `'No items'` | Visible text replacing the rows when `items` is empty. | | `stylesheet` | `boolean` | `true` | Installs the exported track-list stylesheet unless explicitly `false`. | | `classNames` | `TrackListClassNames` | `{}` | Adds compatibility classes for every named node. | | `parts` | `TrackListParts` | `{}` | Adds part tokens for every named node. | | `onError` | `(error: unknown) => void` | — | Receives command, snapshot, subscription, and cleanup failures. | `TrackListClassNames` and `TrackListParts` both expose `root`, `list`, `item`, `row`, `swatch`, `label`, `detail`, and `empty` keys. **Handle** | Member | Kind | Description | |---|---|---| | `element` | `HTMLElement` | Presenter-owned `.wui-track-list` root. | | `controls.list` | `HTMLElement` | Current `
          ` or `
            `. | | `controls.item(id)` | `(string) => HTMLLIElement \| undefined` | Looks up the rendered list item for an id. | | `controls.row(id)` | `(string) => HTMLButtonElement \| undefined` | Looks up its activatable button. | | `controls.focus(id)` | `(string) => boolean` | Moves DOM focus to an enabled rendered row and reports success. | | `update()` | `() => void` | Re-reads State and patches list structure, labels, current state, disabled state, and focus. | | `destroy()` | `() => void` | Releases listeners and subscription, then removes owned DOM and installed style. | **Accessibility.** The named `
              ` or `
                ` contains real list items and buttons. Active rows use `aria-current="true"`; disabled item and global state use native button disabling. Roving `tabindex` keeps one enabled row in the Tab order and skips disabled rows during Arrow, Home, and End movement. An empty list retains its accessible name and displays explicit fallback text.
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. **Tokens** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-track-list-padding` | literal | `.6rem` | Root-inset compatibility fallback. | | `--wm-track-list-border` | `--wm-border` → literal | `#d8d8d8` | Root border color. | | `--wm-track-list-radius` | `--wm-control-radius` → literal | `0` | Root and row corners. | | `--wm-track-list-background` | `--wm-surface` → literal | `#fff` | Root background. | | `--wm-track-list-text` | `--wm-foreground` → literal | `#444` | Root and row text. | | `--wm-track-list-active` | literal | `rgba(17,17,17,.08)` | Current-row background. | | `--wm-track-list-muted` | `--wm-foreground-muted` → literal | `#666` | Detail and empty text. | | `--wm-track-list-swatch` | literal | `currentColor` | Styles an externally unhidden swatch; normal rows hide the node without `item.color` and use inline color when present. | | `--wm-track-list-swatch-radius` | literal | `50%` | Swatch shape. | | `--wm-font-family` | `--wm-font` → literal | `system-ui, sans-serif` | List typeface. | | `--wm-focus` | literal | `currentColor` | Keyboard focus outline. | **Classes** | Node | Canonical class | `classNames` key | |---|---|---| | Root | `.wui-track-list` | `root` | | `
      ` / `
        ` | `.wui-track-list__items` | `list` | | List item | `.wui-track-list__item` | `item` | | Row button | `.wui-track-list__row` | `row` | | Active item compatibility state | `.on` on the list item | — | | Swatch | `.wui-track-list__swatch` | `swatch` | | Label | `.wui-track-list__label` | `label` | | Detail | `.wui-track-list__detail` | `detail` | | Empty message | `.wui-track-list__empty` | `empty` | Every empty state is a live region — `role="status"`, `aria-live="polite"` — so the message is announced when it appears, not only drawn. **Parts** | Node | Canonical part | `parts` key | |---|---|---| | Root | `root` | `root` | | `
          ` / `
            ` | `list` | `list` | | List item | `item` | `item` | | Row button | `row` | `row` | | Swatch | `swatch` | `swatch` | | Label | `label` | `label` | | Detail | `detail` | `detail` | | Empty message | `empty` | `empty` | Compatibility classes and part tokens are added alongside canonical hooks. With `stylesheet: false`, no style node is installed and the caller must style these hooks.
    ## Used by Mount this presenter with application-owned state and bindings. ## Related presenters - [`@webmusic/ui/transport`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/transport/) - [`@webmusic/ui/timeline`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/timeline/) - [`@webmusic/ui/minimap`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/minimap/) - [`@webmusic/ui/playlist`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/playlist/) --- # @webmusic/ui/transport > Renders play, pause, seek, and elapsed-time UI over a structural transport binding. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/transport-time/transport/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountTransport()` renders a compact play/pause button, progress track, optional seek control, and optional elapsed/total clock and volume slider. It paints `TransportState` supplied by the caller and routes playback commands back through a required structural `TransportBinding`. Controls wrap within their container when space is limited. The seek track keeps a usable width, while optional time and volume controls move to another line. This layout is shared by the stylesheet and inline rendering paths; no viewport breakpoint or extra player is needed. Use [timeline]() for labelled regions and a ruler, [minimap]() for overview-range navigation, or [playlist]() for an ordered queue. The [``]() and [``]() elements compose this presenter; mount it directly when the transport must live inside your own layout or use your own state owner. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/transport-time/transport/) ## Import ```ts import {mountTransport} from '@webmusic/ui/transport'; const handle = mountTransport(host, { snapshot: () => store.transport, play: () => player.play(), pause: () => player.pause(), seekFraction: (fraction) => player.seek(fraction * player.duration), subscribe: (notify) => store.subscribe(notify), }); ``` ## Seeking modes The `surface` layout's pointer, keyboard and ARIA behaviour is the kit's own [`mountSurfaceSlider`](), mounted over a `0…100` domain so `aria-valuemax` stays `100` and `aria-valuenow` stays a rounded percentage. It commits on release, so a scrub is one seek rather than one per pointer move, and `Shift` selects a coarser Arrow step. `seekControl` selects one of three presenter-owned layouts: - `'native'` (the default) places a real `input[type="range"]` inside the track. Its DOM range is `0…1000`; the binding continues to receive a normalized `0…1` fraction. - `'surface'` turns the track into an ARIA slider, adds a presenter-owned fill, and supports pointer capture plus keyboard seeking. - `false` leaves the track empty for caller-owned seeking UI. The presenter still paints its progress properties on the track. The older `seek: false` option selects the third mode only when `seekControl` is not supplied. An explicit `seekControl` always wins. ## Binding and command settlement The binding remains authoritative. `subscribe()` tells the presenter when to re-read `snapshot()`; it does not push a second copy of state into the UI. See the [shared presenter shape]() for the common ownership and cleanup contract. `play()` and `seekFraction()` may return promises. The presenter repaints immediately and again after a command fulfills; `pause()` is synchronous. While a native thumb or surface is being dragged, subscription paints do not pull it away from the pointer. Transport does not assign command revisions, so an older promise that fulfills late still triggers a fresh authoritative paint.
    API `mountTransport(host, binding, options?)` accepts an `HTMLElement` or `ShadowRoot` and returns a `MountedTransportHandle`. **State** | Member | Kind | Description | |---|---|---| | `playing` | `boolean` | Strict `true` selects the pause action and icon; every other value selects play. | | `progress` | `number` | Normalized position. Paints the native range, surface fill, and progress custom properties after clamping to `0…1`. | | `seconds?` | `number` | Elapsed seconds. When absent or non-finite, falls back to `progress × duration`; negative values paint as `0`. | | `duration?` | `number` | Total seconds used by the clock and seek value text; absent, non-finite, or negative values paint as `0`. | | `disabled?` | `boolean` | Strict `true` disables play and seek. Surface mode also sets `aria-disabled` and removes the track from the tab order. | | `volume?` | `number` | Linear level painted on the volume slider, clamped to `0…1`; absent or non-finite paints as `1`. A slider being dragged keeps its own position until the drag ends. | **Binding** | Member | Kind | Description | |---|---|---| | `snapshot()` | `() => TransportState` | Required pull port read on mount, notification, command interaction, and explicit `update()`. If it throws, `onError` receives the error and that paint uses a stopped, zero-progress, disabled fallback state. | | `play()` | `() => void \| Promise` | Required command called while stopped. | | `pause()` | `() => void` | Required synchronous command called while playing. | | `seekFraction(value)` | `(number) => void \| Promise` | Required seek command. Receives a clamped `0…1` fraction from either seek mode. | | `setVolume(value)` | `(number) => void \| Promise` | Optional volume command, receiving a clamped `0…1` level. A binding without it renders no volume slider, whatever `showVolume` says. | | `subscribe(notify)` | `(notify: () => void) => () => void` | Required subscription. Its disposer is called by `destroy()` and when the same presenter is remounted into the host. | **Options** | Member | Kind | Default | Description | |---|---|---|---| | `label` | `string` | `'Transport'` | Prefix for the native or surface seek control's accessible name: `
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. The installed stylesheet resolves compatibility values first, then presenter-specific semantic tokens, shared `--wm-*` tokens, internal `--wui-*` fallbacks, and finally literals. The compatibility mappings are `--cp-height`, `--cp-button-bg`, `--cp-button-color`, `--cp-button-border`, `--cp-track`, `--cp-fill`, `--cp-thumb`, `--cp-text`, `--cp-radius`, `--cp-thumb-radius`, and `--cp-seek-handle` before their matching `--wm-transport-*` properties. The presenter owns the runtime `--wui-progress` and `--cp-fill-pct` values; they are progress output, not theme inputs. **Tokens** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-transport-height` | `--wm-control-size` → `--wui-row-h` | `2rem` | Play button, track, and native thumb height. | | `--wm-control-size` | `--wui-row-h` | `2rem` | Shared control-size fallback for transport height. | | `--wm-transport-button-background` | `--wm-accent` → `--wui-ink` | `#111` | Play/pause button background. | | `--wm-accent` | Contextual `--wui-ink` or `--wui-fill` | `#111` button / `#999` fill | Shared fallback for button background and progress fill. | | `--wm-transport-button-foreground` | `--wm-accent-foreground` → `--wui-on-ink` | `#fff` | Play/pause glyph color. | | `--wm-accent-foreground` | `--wui-on-ink` | `#fff` | Shared button-foreground fallback. | | `--wm-transport-border` | `--wm-border` → `--wui-line-control` | `#111` | Primary play-button border. The progress and volume tracks have no border by default; their `--cp-*` overrides remain independent. | | `--wm-border` | `--wui-line-control` | `#111` | Shared border fallback. | | `--wm-transport-track` | `--wm-surface-muted` → `--wui-inset` | `#f3f3f3` | Track background. | | `--wm-surface-muted` | `--wui-inset` | `#f3f3f3` | Shared track-background fallback. | | `--wm-transport-fill` | `--wm-accent` → `--wui-fill` | `#999` | Native gradient and surface fill. | | `--wm-transport-thumb` | `--wm-foreground` → `--wui-pointer` | `#111` | Native range thumb. | | `--wm-foreground` | `--wui-pointer` | `#111` | Shared thumb-color fallback. | | `--wm-transport-foreground` | `--wm-foreground-muted` → `--wui-ink-soft` | `#444` | Root and clock text. | | `--wm-foreground-muted` | `--wui-ink-soft` | `#444` | Shared transport-text fallback. | | `--wm-transport-radius` | `--wm-control-radius` | `0` | Button and track corners. | | `--wm-control-radius` | literal fallback | `0` | Shared corner fallback for track, button, and thumb. | | `--wm-transport-thumb-radius` | `--wm-control-radius` | `0` | Native range thumb corners. | | `--wm-transport-handle-size` | literal fallback | `0.9rem` | Native range thumb width. | | `--wm-control-gap` | `--wui-gap` | `0.6rem` | Gap between button, track, and clock. | | `--wm-font-family` | literal fallback | `system-ui, sans-serif` | Root UI font. | | `--wm-focus` | `--wui-focus` → `--wui-ink` | `#111` | Visible keyboard focus outline. | | `--wm-font-mono` | `--wui-font-mono` | `ui-monospace, monospace` | Clock font. | **Classes** `classNames` adds compatibility classes alongside these canonical names; it never replaces them. In surface mode, the `seek` compatibility class is added to the track because the track is the seek control. | Node | Canonical class | Present when | `classNames` key | |---|---|---|---| | Root | `.wui-transport` | Always | `root` | | Toggle | `.wui-transport__play` | Always | `play` | | Track | `.wui-transport__track` | Always | `track` | | Surface modifier on track | `.wui-transport__surface` | `seekControl: 'surface'` | `seek` is added to the same node | | Surface fill | `.wui-transport__fill` | `seekControl: 'surface'` | `fill` | | Native range | `.wui-transport__seek` | `seekControl: 'native'` | `seek` | | Clock | `.wui-transport__time` | `showTime !== false` | `time` | | Volume fader | `.wui-fader .wui-fader--horizontal .wui-transport__volume` | `showVolume` and `binding.setVolume` | `volume` | | Volume fill / handle | `.wui-fader__fill`, `.wui-fader__thumb` | `showVolume` and `binding.setVolume` | — owned by the shared fader | **Parts** `parts` adds tokens alongside the canonical part names and removes duplicates. | Node | Canonical part | Present when | `parts` key | |---|---|---|---| | Root | `root` | Always | `root` | | Toggle | `play` | Always | `play` | | Track | `track` | Always | `track` | | Surface track | `track seek` | `seekControl: 'surface'` | Both `track` and `seek` are added | | Native range | `seek` | `seekControl: 'native'` | `seek` | | Surface fill | `fill` | `seekControl: 'surface'` | `fill` | | Clock | `time` | `showTime !== false` | `time` | | Volume fader | `fader volume` | `showVolume` and `binding.setVolume` | `volume` | | Volume fill / handle | `fader-fill`, `fader-thumb` | `showVolume` and `binding.setVolume` | — owned by the shared fader | With `stylesheet: false`, the presenter installs no style node. Geometry and theme tokens are painted inline; the caller supplies focus and disabled styling, for example by placing `transportStateStyle` in a caller-owned style node and removing it with the surrounding UI.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/score/element/play/score-player/) - [``](https://koperative-lab.github.io/WebMusic/audio/element/play/audio-player/) ## Related presenters - [`@webmusic/ui/timeline`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/timeline/) - [`@webmusic/ui/minimap`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/minimap/) - [`@webmusic/ui/playlist`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/playlist/) - [`@webmusic/ui/track-list`](https://koperative-lab.github.io/WebMusic/uikit/transport-time/track-list/) --- # @webmusic/ui/analysis > Analysis cards, summaries, distributions, timelines, and playhead-follow rendering helpers. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/views-analysis/analysis/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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/ui/analysis` is a set of one-shot render helpers for analytical cards, distributions, timelines, motifs, rhythm patterns, and audio results. `renderHistogram()` can retain selectable bins and active-state updates. `createAnalysisPlayhead()` follows other rendered spans: it follows quarter-note positions across span-bearing nodes produced by those helpers. Use [stage]() when the visualization needs a caller-drawn canvas, or [track-list]() when the output is a selectable collection alongside the analytical evidence. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/analysis/) ## Import ```ts import { createAnalysisPlayhead, createAnalysisRoot, renderChordTimeline, } from '@webmusic/ui/analysis'; const root = createAnalysisRoot(document); host.append(root); renderChordTimeline(result.chords, root); const playhead = createAnalysisPlayhead(root); playhead.update(player.positionQuarters); ``` ## Reports without Element wrappers The [Score report workflow]() combines `createScoreReport` and the stateless distribution/rhythm APIs with these presenters. Summary cards, histograms and rhythm lists are caller-owned reports; they do not require dedicated Score Analyze Elements. ## One-shot rendering and playhead follow Unlike the shared mount shape, render helpers append output immediately and return either nothing, the root, or a small painter. The caller owns replacement and removal of that output. Build a fresh root or clear one before rendering a new result; only `renderAudioAnalysisCard()` replaces its root contents itself. Timeline, Roman, motif, voice-leading, and rhythm helpers stamp their quarter ranges on rendered nodes. The playhead discovers those nodes, highlights every range containing the supplied position, sets `aria-current`, and restores each helper's idle inline style when it leaves.
    API There is no module-level `State` or `Binding`: helper arguments are immutable render input and the playhead is updated directly. **Root and render helpers** | Export | Kind | Description | |---|---|---| | `createAnalysisRoot(document?)` | `(Document) => HTMLElement` | Creates the canonical `.wui-analysis.webscore-analyze` root without mounting it. | | `renderKeyView(result, root, caption?)` | one-shot renderer | Appends detected key, confidence, and the five strongest candidate bars. | | `renderChordTimeline(chords, root)` | one-shot renderer | Appends an ordered chord timeline whose rows carry quarter spans. | | `renderRomanStrip(key, roman, root)` | one-shot renderer | Appends Roman-numeral chips in the supplied key, each with a quarter span. | | `renderMotifList(motifs, root)` | one-shot renderer | Appends up to eight motifs; each row carries all occurrence spans. | | `renderVoiceLeadingList(issues, root)` | one-shot renderer | Appends up to twelve severity-coded issues with one quarter span each. | | `renderRhythmPatternList(patterns, root, options?)` | one-shot renderer | Appends recurring patterns and their occurrence spans; `options.limit` defaults to `8`. | | `renderHistogram(bins, root, options?)` | `AnalysisHistogramHandle` | Appends normalized bars; optional `onSelect` makes nonempty bins native buttons. Retain the handle for current-bin feedback and cleanup. | | `renderSummaryCard(card, root)` | `AnalysisSummaryCardHandle` | Appends a title, optional subtitle, and definition list of facts; retain the handle to update row values without rebuilding the report. | | `renderAudioAnalysisCard(type, result, root?, options?)` | `HTMLElement` | Replaces and returns a root for key, tempo, loudness, onset, or pitch output. | | `appendSummaryRow(list, label, value)` | utility | Appends one `dt` / `dd` pair to a definition list. | | `renderLiveChordPanel(root, options?)` | `(HTMLElement, LiveChordPanelOptions?) => LiveChordPanel` | Sounding chord, its notes and a history strip. `formatPitch` spells one MIDI note; it defaults to the number, because how a pitch is spelled — sharps or flats, which enharmonic a key implies — is the domain's answer, not the kit's. | **Render data** | Type | Members | |---|---| | `AnalysisKeyResult` | `tonic`, `mode`, `confidence`, and scored key candidates. | | `AnalysisChordSegment` | `chord`, `startQuarters`, `endQuarters`. | | `AnalysisRomanSegment` | Chord-segment fields plus `roman`. | | `AnalysisLiveChordState` | `chord`, sounding `midis`, and chord `history`. | | `AnalysisMotif` | `intervals`, `rhythm`, and occurrence `startQuarters`. | | `AnalysisVoiceIssue` | `type`, `severity`, `voices`, `startQuarters`, `endQuarters`. | | `AnalysisRhythmPattern` | duration `pattern`, occurrence `count`, and onset quarters. | | `AnalysisHistogramBin` | `label` and numeric `value`. | | `AnalysisSummaryCard` | `title`, optional `subtitle`, and label/value `rows`. | | `AudioAnalysisCardResult` | optional key, tempo, onsets and pitch track plus required loudness metrics. | | `AnalysisSpan` | `startQuarters` and `endQuarters` for one playhead-follow range. | `AudioAnalysisCardType` is `'key' | 'tempo' | 'loudness' | 'onsets' | 'pitch'`. Pitch-track `times` are frame timestamps in seconds. **Helper options** | Helper | Member | Kind | Default | Description | |---|---|---|---|---| | `renderRhythmPatternList` | `limit` | `number` | `8` | Maximum number of sorted pattern rows appended. | | `renderRhythmPatternList` | `formatDuration` | `(quarters: number) => string` | the number | Names one duration. Notating it — which glyph a value takes, that a dot adds half again — is a convention of the domain, so the caller answers; `@webmusic/score` passes its own. | | `renderHistogram` | `emptyLabel` | `string` | `'Nothing to show.'` | Text appended when the bins array is empty. | | `renderHistogram` | `format` | `(value: number) => string` | rounded number | Formats each bar's trailing value. | | `renderHistogram` | `onSelect` | `(index: number, bin: AnalysisHistogramBin) => Promise \| void` | — | Makes positive-value bins operable buttons; called on pointer or native keyboard activation. | | `renderHistogram` | `onError` | `(error: unknown) => void` | — | Reports synchronous or asynchronous selection failures. | | `renderAudioAnalysisCard` | `durationSeconds` | `number` | last event / timestamp | Supplies the onset or pitch time span when positive. | | `renderAudioAnalysisCard` | `error` | `string` | — | Replaces the card body with a status failure message. | **Summary handle** `renderSummaryCard()` returns an `AnalysisSummaryCardHandle` with `setValue(label: string, value: string): void`. It updates all existing rows whose label matches exactly, writes only changed text, and preserves their DOM. Unknown labels are ignored. The caller owns removal of the rendered report; ignoring the return value remains supported. **Histogram handle** `AnalysisHistogramOptions` is the exported option bag above. The returned `AnalysisHistogramHandle` provides: | Member | Description | |---|---| | `setActive(indexes: readonly number[])` | Set current bins by their input indexes; updates styling and `aria-current` without rebuilding buttons or moving focus. | | `destroy()` | Idempotently release selection listeners; the caller owns removal of the rendered output. | Without `onSelect`, histogram output remains passive. With it, nonempty bins are native buttons and empty bins remain descriptive. Callback rejection is contained and reported through `onError`. **Playhead options** | Member | Kind | Default | Description | |---|---|---|---| | `scroll` | `boolean` | `true` | Scrolls the first newly active node into the nearest visible position unless `false`. | | `activeClassName` | `string` | `'is-playing'` | One or more compatibility classes added while a node is active. | | `onError` | `(error: unknown) => void` | — | Receives span discovery, style restoration, and scrolling failures. | **Playhead handle and span utilities** | Member / export | Kind | Description | |---|---|---| | `element` | `HTMLElement` | Analysis root controlled by the playhead. | | `update(quarters)` | `(number) => void` | Highlights every span containing the finite position; non-finite input becomes `0`. | | `clear()` | `() => void` | Restores every active node to its idle style and removes current state. | | `destroy()` | `() => void` | Idempotently clears highlighting and releases the root from the controller registry. | | `ANALYSIS_SPAN_SELECTOR` | `string` | Selector matching nodes with single or multiple quarter spans. | | `readAnalysisSpans(element)` | `(HTMLElement) => AnalysisSpan[]` | Parses `startQuarters` / `endQuarters` and multi-span data into ranges. | | `readAnalysisIdleStyle(element)` | `(HTMLElement) => string \| undefined` | Reads the helper-stamped inline style restored after playback. | **Accessibility.** Timelines and repeated-pattern output use real ordered lists; summaries use definition lists. Active span nodes receive `aria-current="true"`. The playhead never moves focus. Audio-card failures use `role="status"`; canvas-like pitch output remains supplemental and should be given surrounding context by the consuming Element.
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. The root uses the shared [`--wm-component-*` surface](), with `--wm-analysis-surface-*` available for a presenter-only override. The fallback retains the harmony theme, density and scheme token layer, followed by the older `--wm-analysis-*` and `--webscore-analyze-*` names. Inline roots and `analysisStyle` use the same declarations. The helpers do not install a style node; most structural styles are stamped on their output, and the exported `analysisStyle` supplies the base root, muted, and timeline rules when a consumer wants stylesheet-owned defaults. **Tokens** | Property | Falls back to | Default | Controls | |---|---|---|---| | `--wm-analysis-bar-height` | literal | `.5rem` | Height of each distribution bar. | | `--wm-analysis-strip-height` | literal | `1.4rem` | Height of the onset mark strip. | | `--wm-analysis-foreground` | `--webscore-analyze-color` → `--wm-foreground` → literal | `inherit` | Root text; theme and scheme can supply the resolved harmony ink. | | `--wm-analysis-background` | `--webscore-analyze-bg` → `--wm-surface` | `transparent` | Root background. Selected text independently uses `--wm-harmony-on-accent` → `--wm-accent-foreground` → `#fff`. | | `--wm-analysis-border` | `--webscore-analyze-border` → harmony rule | `1px solid #d8d8d8` | Root border. | | `--wm-analysis-padding` | `--webscore-analyze-padding` → literal | `.75rem` | Root padding compatibility fallback. | | `--wm-analysis-muted` | `--webscore-analyze-muted` → literal | `#777` | Captions, labels, and secondary values. | | `--wm-analysis-track` | `--webscore-analyze-track` → literal | `#eee` | Histogram tracks and active-span background. | | `--wm-analysis-accent` | `--webscore-analyze-accent` → literal | `#111` | Bars, outlines, active chips, onset marks, and pitch line. | | `--wm-analysis-rule` | `--webscore-analyze-rule` → `--wm-control-border` → `--wm-border` | `#d8d8d8` | Timeline separators and inactive chip borders. | | `--wm-control-border` | `--wm-border` | `#d8d8d8` | Shared non-primary control border fallback. | | `--wm-analysis-error` | `--wm-error` → literal | `#c0392b` | Audio-card failure text. | **Classes** | Node | Canonical class | Compatibility class | |---|---|---| | Root | `.wui-analysis` | `.webscore-analyze` | | Muted base-rule hook | `.wui-analysis__muted` | — | | Chord timeline | `.wui-analysis__timeline` | `.webscore-analyze__timeline` | | Chord label | — | `.webscore-analyze__chord` | | Beat label | — | `.webscore-analyze__beat` | | Rhythm list | `.wui-analysis__rhythms` | — | | Histogram | `.wui-analysis__histogram` | — | | Summary title | `.wui-analysis__title` | `.webscore-analyze__title` | | Summary list | `.wui-analysis__list` | `.webscore-analyze__list` | | Audio root modifier | `.wui-analysis--audio` | `.waa-card` | The helpers expose no `parts` option or canonical part tokens. `activeClassName` is appended only while a playhead range is active and does not replace these structural classes.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/score/element/analyze/score-chord-analysis/) - [``](https://koperative-lab.github.io/WebMusic/score/element/analyze/score-live-chord-analysis/) ## Related presenters - [`@webmusic/ui/level-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/level-analyzer/) - [`@webmusic/ui/spectrum-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/spectrum-analyzer/) - [`@webmusic/ui/oscilloscope`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/oscilloscope/) - [`@webmusic/ui/transient-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/transient-analyzer/) - [`@webmusic/ui/pitch`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/pitch/) - [`@webmusic/ui/harmony`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/harmony/) --- # @webmusic/ui/harmony > A conveyor, a nameplate, a ranked strip and a wheel: read-outs that show a harmonic analysis happening rather than listing what it concluded. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/views-analysis/harmony/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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/ui/harmony` is four read-outs for material that is still arriving. `mountFlowLane` pins a now line a third of the way across a viewport and scrolls the **material** under it, so two thirds of what a reader sees is what has not happened yet. `mountNameplate` says the one word the reader is actually reading and pops when it changes. `mountChipStrip` ranks. `mountWheel` points, and admits how sure it is by how wide it points. The conveyor is the reason the set exists, and it is the opposite of a map. A map answers *"where am I in the piece"*; a conveyor answers *"what is happening, and what is about to"*. Both are correct and they are different instruments — [timeline]() is the map and stays one. Several tracks share **one** axis, **one** now line and **one** transform, not by convention but because they are children of a single reel: there is no arrangement of the code in which two rows drift apart. None of the four knows any music theory. A band is *"from 4.0 to 8.0, tone slot 7, labelled `Am7`, role `third`"*, and the axis is a number line whose unit the kit has never been told. Reach for [pitch]() when the answer is a set of sounding notes rather than a name, and [analysis]() for the static cards and tables a playhead walks. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/harmony/) ## Import ```ts import {mountChipStrip, mountFlowLane, mountNameplate, mountWheel} from '@webmusic/ui/harmony'; const lane = mountFlowLane( laneHost, { snapshot: () => ({ bands: [ // `start`/`end` place the band; `stampStart`/`stampEnd` are the numbers // a playhead is fed. Here the lane draws seconds and stamps quarters. {id: 'b1', start: 0, end: 2, stampStart: 0, stampEnd: 4, primary: 'Dm7', tone: 2}, {id: 'b2', start: 2, end: 4, stampStart: 4, stampEnd: 8, primary: 'G7', tone: 7}, ], tracks: [{id: 'chords', label: 'Chords'}], span: {start: 0, end: 4}, now: transport.position(), }), // Pulled ONCE per frame. Everything else is pulled on a domain event. position: (tick) => transport.positionAt(tick.at), seek: (position, phase) => transport.seek(position, phase), subscribe: (notify) => store.subscribe(notify), }, {anchor: 0.33, visibleSpan: 8}, ); const nameplate = mountNameplate(nameplateHost, { snapshot: () => ({ // `key` is opaque identity: the same symbol on a later bar is a new chord // and pops, and the kit is never told why the string did not change. primary: {symbol: 'Dm7', full: 'D minor seventh', key: 'bar-3'}, alternates: [{symbol: 'F6/D', note: 'inversion'}], voicing: [{label: 'D3', role: 'root'}, {label: 'F3', role: 'third'}], next: {symbol: 'G7', key: 'bar-4'}, }), approach: (tick) => transport.nearnessOfNext(tick.at), subscribe: (notify) => store.subscribe(notify), }); const strip = mountChipStrip(stripHost, { snapshot: () => ({ items: [{id: 'Cmaj7', start: 0, end: 8, primary: 'Cmaj7', roman: 'Imaj7', meter: 0.34}], layout: 'flow', }), }); const wheel = mountWheel(wheelHost, { snapshot: () => ({ outer: keyNames.map((label) => ({id: label, label})), // A wedge, not a hair: an answer held at sixty per cent has to look unsure. needle: {at: 'G', spread: 0.8}, centre: {primary: 'C major', secondary: '86% sure'}, }), }); lane.band('b2')?.dataset.zone; // 'ahead', 'now' or 'wake' nameplate.pop('stress'); strip.item('Cmaj7')?.dataset.chip; wheel.destroy(); ```
    API All four mount options accept `surface?: 'default' | 'none'` (default `'default'`). Use `'none'` when a containing presenter owns the outer frame; it retains musical data, palette, interaction and lifecycle while omitting the child's outer paint. Four mounts, one shared vocabulary. Every one follows the kit's shape: `mountX(host, binding, options?)`, a binding that answers `snapshot()` and may offer `subscribe()`, and a handle whose `destroy()` is idempotent. Mounting twice on one host replaces the first read-out; the four kinds are different read-outs, not alternative renderings of one, so they do not displace each other. **Mounts and handles** | Export | Kind | Description | |---|---|---| | `mountFlowLane(host, binding, options?)` | `FlowLaneHandle` | The conveyor. `{element, viewport, reel, nowLine, index, track(id), band(id), tick(), update(), destroy()}`. | | `mountNameplate(host, binding, options?)` | `NameplateHandle` | The one line the reader is reading. `{element, symbol, pop(intent?), tick(), update(), destroy()}`. | | `mountChipStrip(host, binding, options?)` | `ChipStripHandle` | A ranked or atemporal read-out. `{element, list, item(id), update(), destroy()}`. | | `mountWheel(host, binding, options?)` | `WheelHandle` | A ring of segments and a needle. `{element, svg, index, segment(id), update(), destroy()}`. | | `harmonyPresenterStyle` | `string` | The stylesheet all four install, as side-effect-free text. | `HarmonyVoice` is `{label?, mark?, role?}` and is structurally a subset of the pitch surfaces' own mark, so one array can be handed to a keyboard and to a nameplate and the two cannot disagree. `ToneRole` and `Severity` are re-exported here, as are `MotionMode`, `FrameTick` and `FrameClock`. **Two axes, and why** A band carries `start`/`end` on the **lane** axis and `stampStart`/`stampEnd` on the **stamping** axis. They are different rulers on purpose: a conveyor is a portrait of real time, so a ritardando has to be visibly wider, while a playhead is fed the score's own metrical position — drawing the metrical one would render rubato as a metronome. The kit never converts between them, because it cannot: only the caller knows the map. Omit the stamping pair and it defaults to the lane pair. **The flow lane** | `FlowLaneState` | Kind | Default | Description | |---|---|---|---| | `bands` | `readonly FlowBand[]` | — | The material. | | `tracks` | `readonly FlowTrack[]` | — | `{id, label?, sublabel?, muted?}` rows, drawn in order, sharing one reel. | | `primaryTrack` | `number` | the lowest track containing bands | Preferred row for the pinned readout and current band. Use it when an earlier row is a full-length header; if the preferred row has no band at the current position, the lane falls back to another sounding band. | | `flags` | `readonly FlowFlag[]` | — | `{id, at, track?, severity?, label?}`. Without a `track` a flag crosses every row: a fact about the moment, not about a line. | | `brackets` | `readonly FlowBracket[]` | — | `{id, start, end, from, to, severity?, label?}` spanning rows `from`…`to`. | | `ruler` | `readonly {at, label, major?}[]` | — | Ticks, in the caller's own words. | | `span` | `{start, end}` | the material's bounds | The extent the reel and every scrub clamp to. | | `now` | `number` | — | The position. With `binding.position` it seeds only the first frame. | | `playing` | `boolean` | `false` | Published as `data-playing`. | | `future` | `boolean` | `true` | `false` draws the space right of the now line as an empty field. | | `pinned` | `{primary?, secondary?}` | the sounding band's own | Override the current readout above the plot. | | `focusGroup` | `string` | — | Every band whose `group` matches lights with the sounding one. | | `disabled` / `emptyLabel` | | — | | `FlowBand` is `{id, start, end, stampStart?, stampEnd?, spans?, track?, primary?, secondary?, trailing?, glyph?, points?, tone?, role?, severity?, weight?, group?, disabled?}`. `tone` is `0…11` and fills the block; `role` paints a stripe down the leading edge, so colour never carries a fact alone; `severity` paints a bead and never the whole block. `weight` controls the centred fill's share of the row height (`0.1…1`, default `1`); the band's text and hit area retain the full row height. `glyph` is a caller-owned path and `points` a normalised contour — the kit frames a shape and never invents one. Given `spans`, the band stamps only the multi-occurrence form: a node carrying both forms reads back as one span too many. `spans` is judged on what SURVIVES, not on what was passed: the natural caller shape is `spans: motif.occurrences.map(...)`, and a list that comes back empty — or whose every entry is still unplaced — falls back to the band's own `start`/`end` rather than un-stamping a band that has a perfectly good pair of numbers and dropping it out of the playhead's reach for good. | `FlowLaneBinding` | Description | |---|---| | `snapshot()` | Content. Pulled on domain events, **not** per frame. | | `position?(tick)` | Position. Pulled once per frame. Given it, the mount is live. It takes the frame rather than returning one so the caller can interpolate between sparse samples itself — whether to interpolate depends on the rate, on what is playing and on whether a seek just happened, all of which is domain knowledge. | | `seek?(position, phase)` | `'drag'` while the pointer is down, `'commit'` on release, so a caller can preview cheaply and commit once. Its presence is what makes the viewport a slider. | | `selectBand?(id, band, {additive})` | A band was clicked. | | `focusBand?(id)` | Keyboard focus moved to a band, or off one. | | `subscribe?(notify)` | | | `FlowLaneOptions` | Kind | Default | Description | |---|---|---|---| | `anchor` | `number` | `.33` | Where the now line is pinned, `0…1`. | | `scale` | `number` | `44` | Pixels per axis unit before zoom; used when `visibleSpan` is absent or invalid. | | `visibleSpan` | `number` | — | Axis units visible across the measured plot before zoom. A finite positive value takes precedence over `scale`; resizing keeps this window and the current position. | | `reservePinned` | `boolean` | `false` | Reserve one line above the plot when no band is sounding, so the readout can appear without shifting the lane. The empty slot has no visible text. | | `fallbackWidth` | `number` | `640` | What the lane assumes it is wide when nothing can measure it. | | `animate` | `boolean` | whether `position` exists | Run a frame loop. A `clock` forces it `false`. | | `clock` | `FrameClock` | — | Somebody else's clock. Given one, the mount opens no loop and becomes a passenger. | | `spans` | `boolean` | `true` | Write the playhead's span contract onto every band. | | `keyboardStep` / `keyboardPage` | `number` | `1` / `4` | Arrow and shift-arrow movement, in axis units. | | `formatPosition` | `(position) => string` | the bare number | `aria-valuetext`, in the caller's own words. | | `label`, `motion`, `classNames`, `parts`, `onError`, `stylesheet` | | | `classNames` and `parts` name `{root?, viewport?, reel?}`. | `[` and `]` walk the **harmony**: they step to the next band edge rather than to a point four seconds later that may be in the middle of a chord. Arrows step, shift-arrows page, `Home` and `End` go to the ends, and `Ctrl`/`Cmd` with the wheel zooms — all of it only where `seek` exists, because a focusable node with no behaviour is a trap rather than an affordance. With `visibleSpan`, pixels per unit are `viewportWidth / visibleSpan × zoom`. The measured width excludes the label gutter. A resize preserves the chosen zoom and current position, using the same scale for painting and pointer seeks. Without a usable measurement, `fallbackWidth` supplies the width. No layout measurements run in the frame callback. The current readout wraps above the clipped plot. The label gutter has its own column, and ruler labels have their own line above the tracks. The viewport grows to fit all rows even when the flow height token is smaller; a third or fourth track is never clipped by the default two-row height. Labels inside short bands use ellipsis; below 32px of band width, the inner label and note are hidden instead of showing clipped fragments. The complete reading remains in the band title and semantic twin, while a track label stays in the separate gutter. Ticks, flags and brackets do not intercept a band click or drag. A contour occupies the remaining row height below its labels, and its stroke keeps the same pixel width when the time axis stretches or zooms. **The nameplate** `ChordNameCandidate` is `{symbol, full?, note?, weight?, pressed?, key?}`. `key` is opaque identity: a change pops even when `symbol` is byte-identical, which is how *"C, G, C"* and *"the same chord struck again"* are told apart. The kit only ever compares it with `!==`. | `NameplateState` | Description | |---|---| | `primary` | The reading. `full` is announced instead of the symbol, and the symbol is then hidden from the reader so the pair is not read as an abbreviation and its expansion. | | `alternates` | Other or selectable readings. With `binding.selectAlternate` each is a real button; without it, plain text. Optional `pressed` supplies `aria-pressed` for a selectable reading. | | `voicing` | `readonly HarmonyVoice[]`, printed as one line. | | `caption`, `history`, `confidence`, `emphasis`, `emptyLabel` | `emphasis` is `'display'` or `'hero'`. `history` is `aria-hidden`: a history that announced itself would read every chord twice. | | `next` | The naming that is coming. Drawn as a ghost, never announced. | `binding.approach(tick)` answers how near `next` is, `0…1`, once per frame — nearness depends on the rate, the metre and on what the caller thinks *near* means, so it is a number the caller returns rather than one the kit infers from a lane it cannot see. `NameplateOptions` adds `popDuration` (default `180`, `0` in any reduced mode), `animate` and `clock` to the shared six. Set `stableLayout: true` for a live nameplate without `caption`, `next` or `history`: it reserves fixed symbol, voicing and alternate rows through note changes. When no primary reading exists, literal `voicing` labels occupy the main row; this is not a newly inferred chord. Long lines scroll horizontally instead of changing the surface height. The default is `false` and keeps the natural layout. **The chip strip** `ChipItem` is `{id, start, end, primary, secondary?, roman?, trailing?, meter?, meterGhost?, occurrences?, spans?, tone?, severity?}`. `meter` is the bar and `meterGhost` the whole-piece answer drawn as an outline behind it. `layout` is `'flow' | 'ribbon' | 'stack'` — all three are **order**, not time. A strip has no axis and no now line, which is exactly why it is not a lane: making it a degenerate mode of the conveyor would hide a frame loop and pointer capture inside a static list. **The wheel** `outer` is drawn clockwise **in the order given** — the order is the caller's answer, and twelve is not assumed. `needle.at` is a segment id or a fractional index, so *"between the third and the fourth"* is said without anyone writing a number of degrees; `spread` widens it into a wedge, because an answer held at forty per cent confidence should look unsure rather than point precisely at a guess. The needle's rotation is **accumulated**: segment 11 to segment 0 is plus one segment, not minus eleven. `trail` is recent positions, oldest first. **Motion** `MotionMode` is `'auto' | 'continuous' | 'stepped' | 'none'`. `'auto'` asks the nearest ancestor's `data-motion` **first**, because a shell answers that question once for a whole page and six surfaces must not disagree; with nothing declared above it, it asks `prefers-reduced-motion` itself, guarded. A reduced mode changes the **driver**, never the layout: a stepped lane holds the identical bands at identical offsets and re-anchors when the sounding band changes instead of on every frame. The screenshots are the same picture — which matters, because *"the next chord is approaching"* is a property of where things are, not of how they got there. The frame loop is one `requestAnimationFrame` per document, shared by every mount in it. With no subscribers there is no loop at all, and a lane nobody can see leaves it and rejoins in one frame, because the position is a pure function of the instant rather than an accumulation of the frames that were missed. **Accessibility.** Each root is a labelled `role="group"`. The lane publishes a visually hidden `
      ` twin, item for item with the bands, with `aria-current="true"` on the sounding one; the reel itself is `aria-hidden`, like every other visual part of the viewport, so the twin is the single reading and no label is announced twice. The wheel publishes the same twin for its segments, each with its share of the ring — and with no share at all where the ring carries no weights, because twelve segments announcing `0%` is worse than saying nothing about strength. `aria-current` there is keyed on the ring as well as the id, so an outer `C` and an inner `C` are two answers rather than one. The nameplate has exactly one live region and the primary symbol lives inside it for its whole life, because a replaced node inside `aria-live` is a change nobody hears; it is `aria-atomic` because its two children are one fact said twice. The wheel's centre is deliberately **not**, because its two are different facts and a confidence ticking a percent must not re-announce the key. `next` and `history` stay outside the region. The viewport becomes a `role="slider"` only where `binding.seek` exists, and `aria-valuenow` is on the **lane** axis, never the stamping one. **Above roughly two thousand items the correct answer is to merge in the headless projection**, not to virtualise here. A lane that redraws a thousand bands nobody can tell apart is the same mistake as a list, drawn faster.
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. All four install `harmonyPresenterStyle`, or paint the same declarations onto the nodes when `stylesheet: false`. Both paths are generated from one record, so the sheet and the inline form cannot describe two different read-outs. Every colour is a chain ending in a literal, never a bare `var()`: a bare custom property is invalid at computed-value time on a host carrying no tokens, which paints a band invisible — the one failure a colour system may not have. By default, nameplate symbols and alternate names wrap within the available width. Optional rows with no content collapse instead of reserving height, and revealing them restores their flex or block layout. With `stableLayout`, those rows remain mounted at fixed heights and long readings scroll horizontally. The live-region nodes remain mounted in either mode. Numeric geometry is deliberately **not** tokenised where JavaScript has to read it back. `scale` is an option with a documented default rather than a custom property, because reading a length out of one means `getComputedStyle` and custom properties do not inherit in every environment this runs in. The nameplate's pop is the same case for the same reason — the Web Animations API takes a number and cannot take a `var()` — so its length is the `popDuration` option. Heights and colours stay CSS. The outer frame uses `--wm-harmony-surface-background`, `-border`, `-padding` and `-radius` through the shared component quartet. It defaults to transparent with no border or padding. The flow viewport is transparent inside that frame, so a translucent background paints once. `--wm-harmony-flow-tone` overrides per-band palette fills; clearing it restores each band's tone. Use `--wm-harmony-foreground`, `--wm-harmony-muted`, `--wm-harmony-background`, `--wm-harmony-accent`, `--wm-harmony-on-accent`, `--wm-harmony-rule` and `--wm-harmony-track` for the palette. Their shared semantic fallbacks also work on standalone mounts without an ancestor's private token record. Typography also works on standalone mounts: `--wm-harmony-font` falls back to `--wm-font-family`, then inherits the host's font. `--wm-harmony-font-display` falls back to that prose family, while `--wm-harmony-font-mono` falls back to `--wm-font-mono`, then `ui-monospace, monospace`. These chains apply with and without an installed stylesheet. The shared light/dark neutral defaults are `#111` / `#eee` for paint and accent, `#777` / `#aaa` for muted text, `#d8d8d8` / `#333` for rules, and `#eee` / `#222` for tracks. Accent text uses `#fff` / `#111`. Musical degree and progression colors keep their separate palettes; only the unclassified `other` role uses neutral `#777` / `#aaa`. Interactive alternate-name buttons use `--wm-harmony-height` → `--wm-control-size` → `--wui-row-h` for a `2rem` minimum height. Their corners use `--wm-harmony-radius` → `--wm-control-radius`, defaulting to `0`. Passive analytical marks retain their own drawing geometry. **Compatibility and state tokens** The older `--wui-*` names below retain compatibility where previously supported. They also include presenter-owned outputs; use the public `--wm-*` palette above for new themes. | Property | Default | Controls | |---|---|---| | `--wui-harmony-flow-height` | `96px` / `72px` | The viewport's preferred height, expanded when needed for the ruler and all tracks; the second value is `data-density="compact"`. | | `--wui-harmony-lane-height` | `48px` / `36px` | One row inside it. | | `--wui-harmony-flow-now` | the accent | The now line. | | `--wm-harmony-flow-tone` | per-band tone, or accent at 18% alpha | Public fill override for every band, ahead of the per-band internal value. | | `--wui-harmony-approach` | — | Published by the nameplate, `0…1`, for the ghost of `next`. | | `--wui-harmony-motion-turn` | `320ms` | How fast the needle turns. | | `--wui-harmony-motion-tone` / `--wui-harmony-motion-chip` | `90ms` / `120ms` | Colour changes, and a chip's own transitions. | | `--wui-harmony-motion-ease` | `ease-out` | The easing all four share. | | `--wui-harmony-accent` / `--wui-harmony-on-accent` | ink / independent contrast ink | The needle, the meters and the now line. | | `--wui-harmony-surface` / `--wui-harmony-line` / `--wui-harmony-track` | transparent / rule / bed | The viewport, the rules and a meter's bed. | | `--wui-harmony-info` / `--wui-harmony-warning` / `--wui-harmony-danger` | | Severity beads, flags and brackets. | | `--wui-harmony-ink` / `--wui-harmony-ink-muted` | inherit / muted | Text, and the gutter and empty states. | | `--wui-harmony-font` / `--wui-harmony-font-display` / `--wui-harmony-font-mono` | Public typography chains above | Prose, the big word, and the fixed-width names. | | `--wui-harmony-size-display` / `-title` / `-body` / `-label` / `-micro` | `2rem` … `.7rem` | The type ladder. | | `--wui-harmony-pad` / `--wui-harmony-space-1` … `-5` | | Chips and spacing. | Tone colour comes from the shared degree palette — `--wui-degree-root` and its siblings sit in front of the `--wm-degree-*` names a theme sets — and a band's block colour from the twelve-slot `--wui-progression-tone-*` ring the caller indexes with `tone`. **Classes and state attributes** | Node | Class | Carries | |---|---|---| | Lane root / body / viewport / reel | `.wui-harmony-flow`, `.wui-harmony-flow__body`, `.wui-harmony-flow__viewport`, `.wui-harmony-flow__reel` | `data-motion`, `data-playing`, `data-disabled` on the root; `data-lanes` on the viewport. | | One row / one band | `.wui-harmony-flow__lane`, `.wui-harmony-flow__band` | `data-track`, `data-lane`, `data-muted`; and on a band `data-band`, `data-zone`, `data-role`, `data-group`, `data-focus`. | | A band's parts | `.wui-harmony-flow__fill`, `__role`, `__label`, `__note`, `__bead`, `__glyph` | The fill carries the tone and the drain. | | Current readout / now line / gutter | `.wui-harmony-flow__pinned`, `__now`, `__gutter`, `__gutter-row`, `__gutter-label` | None rides the reel; the readout sits above the plot and the gutter beside it. | | Decorations / semantic twin | `.wui-harmony-flow__decorations`, `__tick`, `__tick-label`, `__flag`, `__bracket`, `__index` | The twin is an `
      `, `aria-current` on the sounding entry. | | Nameplate | `.wui-harmony-nameplate`, `__live`, `__symbol`, `__full`, `__next`, `__voicing`, `__alternates`, `__history`, `__chip` | `data-emphasis`, `data-confidence`, opt-in `data-layout="stable"` and current `data-reading="chord|notes|rest"` on the root; `data-current` on a history chip. | | Chip strip | `.wui-harmony-chip`, `__list`, `__item`, `__primary`, `__secondary`, `__meter`, `__ghost`, `__fill`, `__trailing`, `__bead` | `data-layout` on the list; `data-chip`, `data-selectable` on an item. | | Wheel | `.wui-harmony-wheel`, `__svg`, `__sector`, `__needle`, `__trail`, `__mark`, `__centre`, `__index` | `data-segment`, `data-ring`, `data-current` on a sector. | Two rules that look like performance notes and are correctness rules. **A band's inline style is a pure function of its own fields, never of the position.** `createAnalysisPlayhead` replaces a lit node's whole `style.cssText` with the idle string stamped at render time, so a per-frame inline write on a band is erased at precisely the instant that band matters. Everything that moves lives on the reel's transform, or on a **child** of the band — a child's style is nobody else's business, which is where the drain that fills the sounding band only as far as the now line is written. `will-change: transform` is granted to the reel alone: three hundred promoted bands is a layer-tree explosion, and the bands never move relative to the reel. **An offscreen band is never removed.** The playhead discovers nodes with `querySelectorAll`, so a recycled band is a silently dead highlight — and the recycled ones would be exactly those near the now line. Offscreen cost is paid with `content-visibility`. A band leaves the DOM when the **data** no longer has it, and for no other reason. The viewport is clipped with `overflow: hidden; overflow: clip;`, in that order. An `overflow: hidden` box is a scroll container in the programmatic sense, and one `scrollIntoView` from a playhead inside a box whose reel is written every frame shoves the anchor permanently off. `clip` creates no scroll container, so **this box** never scrolls and the anchor is safe; the `hidden` before it is what an engine too old for `clip` keeps. It is not a no-op for the page, though. A clip box is *skipped* by `scrollIntoView`, which then goes on to the nearest real scrollport above it — and the node a playhead hands it is the first lit one in document order, which for a band that recurs need not be the occurrence under the line. So a root that holds a lane takes `createAnalysisPlayhead(root, {scroll: false})`. If the page starts scrolling sideways anyway, that is a diagnostic worth reading: it means the lane's axis and its `stampStart`/`stampEnd` are not the same ruler. The reel's width is **never transitioned**, and that is a correctness rule rather than a taste. A band's box is a percentage of the reel's width while the reel's offset is absolute pixels, so easing the width while writing the transform outright describes two different scales for the length of the ease — a zoom throws the material `(now − axis.start) × (newScale − oldScale)` px off the now line until the ease finishes, which is worse the further into the piece the listener has got. Both are written together, from one scale, in one placement. Nothing in a live state ever writes `outline`. That property belongs to the playhead, which is how six assertions elsewhere detect "is this node lit"; the sibling highlight paints with `background` instead. The `parts` option adds `part` tokens for a shadow-DOM host; `classNames` adds classes without replacing the structural ones. Neither is required.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/score/element/analyze/score-chord-analysis/) - [``](https://koperative-lab.github.io/WebMusic/score/element/analyze/score-live-chord-analysis/) ## Related presenters - [`@webmusic/ui/level-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/level-analyzer/) - [`@webmusic/ui/spectrum-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/spectrum-analyzer/) - [`@webmusic/ui/oscilloscope`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/oscilloscope/) - [`@webmusic/ui/transient-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/transient-analyzer/) - [`@webmusic/ui/analysis`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/analysis/) - [`@webmusic/ui/pitch`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/pitch/) --- # @webmusic/ui/level-analyzer > A sampled dBFS level inspector with RMS and peak history, threshold, freeze, and hold reset. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/views-analysis/level-analyzer/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountLevelAnalyzer()` is a human-readable dynamics inspector for a caller's sampled signal. It shows current RMS, held sample peak and crest factor in dB, plus a bounded recent history. The operator can freeze that history, move a sample-peak alert threshold, or clear the held peak. It neither samples an audio graph nor claims calibrated LUFS or oversampled true peak. Use [`meter`]() for a compact normalized level bar. Use this presenter when the decision is whether a sounding passage is too quiet, too peaky, or above an operator-selected dBFS threshold. The live demo supplies generated test samples, not device measurements. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/level-analyzer/) ## Import ```ts import {mountLevelAnalyzer, type LevelAnalyzerState} from '@webmusic/ui/level-analyzer'; const handle = mountLevelAnalyzer(host, { onThresholdChange: (dbfs) => setThreshold(dbfs), onFreezeChange: (frozen) => setFrozen(frozen), onResetHold: () => resetPeakHold(), }); handle.update(state satisfies LevelAnalyzerState); // Release the handle when its host is removed. ``` The application owns `host`, sampled `state`, the three command functions and the sampling schedule. Call `update()` for each new or changed state. Stopping updates is the caller's freeze behavior; the presenter does not retain an audio timer or source. ## Measurement boundary `rmsDbfs` and `peakDbfs` are already measured values. The presenter derives only the displayed crest difference (`peakDbfs - rmsDbfs`), plot geometry and threshold indication. The caller decides the sample window and history cadence, caps the `history` array, maintains `heldPeakDbfs`, and clears it after `onResetHold()`. A silence value may be `-Infinity`; it renders as an empty numeric readout. The demo generates a bounded 64-sample window to exercise the real presenter commands.
    API **State and sample** | Member | Kind | Default | Description | |---|---|---|---| | `sample` | `LevelAnalyzerSample?` | absent | Current `rmsDbfs` and `peakDbfs` numbers, each in dBFS. The presenter does not read PCM. | | `heldPeakDbfs` | `number?` | absent | Caller-held sample peak in dBFS; Reset hold requests that the caller clear it. | | `history` | `readonly LevelAnalyzerSample[]` | required | Recent samples, in time order. The most recent 64 are plotted; older values are ignored. | | `thresholdDbfs` | `number` | required | Alert and slider position. The plot visually spans `-60` to `0` dBFS. | | `frozen` | `boolean` | required | Freeze button's pressed state; the caller stops accepting new samples. | | `status` | `string` | required | Visible status beside the title. | **Actions and handle** | Member | Kind | Default | Description | |---|---|---|---| | `onThresholdChange(value)` | `(number) => void` | required | Called as the native threshold range moves; the caller commits it and calls `update()`. | | `onFreezeChange(frozen)` | `(boolean) => void` | required | Requests the opposite of the last state supplied to `update()`. | | `onResetHold()` | `() => void` | required | Requests a held-peak reset; the caller supplies the next state. | | `element` | `HTMLElement` | mounted root | The one presenter surface. | | `update(state)` | `(LevelAnalyzerState) => void` | — | Paints the supplied state without replacing the surface. | | `destroy()` | `() => void` | — | Removes owned nodes, stylesheet and listeners; repeated calls are safe. | There is no `Binding` object or mount-options bag for this presenter; actions are passed directly as the second argument. It owns no subscription. See the [shared presenter shape]() for the general host and cleanup model. **Accessibility.** Numeric readings use a definition list. The history plot has a descriptive image label updated from the current sample; it does not replace the numeric readouts. Freeze is a native button with `aria-pressed`, Reset hold is a native button, and the threshold is a native range input with a visible dBFS value. The status text uses `role="status"`.
    Styling The stylesheet is installed into the supplied host and removed by `destroy()`. This presenter does not currently accept `stylesheet`, `classNames` or `parts` options. | Token | Fallback | Controls | |---|---|---| | `--wm-level-analyzer-background` | `--wm-surface`, then white | Surface background. | | `--wm-level-analyzer-foreground` | `--wm-foreground`, then dark ink | Text color. | | `--wm-level-analyzer-border` | `--wm-border`, then light gray | Surface border. | | `--wm-level-analyzer-padding` | `.85rem` | Surface inset. | | `--wm-level-analyzer-rms` | teal | Recent RMS columns. | | `--wm-level-analyzer-peak` | dark blue | Sample-peak ticks. | | `--wm-level-analyzer-threshold` | orange | Threshold line and slider accent. | The canonical root is `.wui-level-analyzer`, with one `part="root surface"`. Internal classes follow `.wui-level-analyzer__*`; they are implementation selectors, not a class-extension API. Shared `--wm-border`, `--wm-muted`, `--wm-surface` and `--wm-accent` tokens provide fallback colors for the plot and controls.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-level-analyzer/) ## Related presenters - [`@webmusic/ui/spectrum-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/spectrum-analyzer/) - [`@webmusic/ui/oscilloscope`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/oscilloscope/) - [`@webmusic/ui/transient-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/transient-analyzer/) - [`@webmusic/ui/analysis`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/analysis/) - [`@webmusic/ui/pitch`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/pitch/) - [`@webmusic/ui/harmony`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/harmony/) --- # @webmusic/ui/oscilloscope > An inspectable time-domain waveform with caller-supplied trace, trigger state and time-amplitude probe commands. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/views-analysis/oscilloscope/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountOscilloscope()` draws one bounded waveform window and owns the controls for timebase, trigger, freeze and probing. The caller supplies sampled data and chooses whether those controls alter capture or only the displayed frame. The presenter does not open an audio graph or know a player's clip position. For a ready player-connected composition, use [``](). [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/oscilloscope/) ## Import ```ts import {mountOscilloscope, type OscilloscopeState} from '@webmusic/ui/oscilloscope'; const state: OscilloscopeState = { trace: {samples: new Float32Array([0, 0.4, 0, -0.4, 0]), timebaseMs: 10, triggered: true, silent: false}, availableTimeMs: 50, timebaseMs: 10, triggerLevel: 0, triggerEdge: 'rising', frozen: false, status: 'live', }; const handle = mountOscilloscope(host, { snapshot: () => state, setFrozen: (value) => { state.frozen = value; handle.update(); }, setTimebaseMs: (value) => { state.timebaseMs = value; handle.update(); }, setTriggerLevel: (value) => { state.triggerLevel = value; handle.update(); }, setTriggerEdge: (value) => { state.triggerEdge = value; handle.update(); }, probe: (timeMs, amplitude) => inspect(timeMs, amplitude), }); // Call handle.update() after replacing the trace; destroy on removal. ``` The caller owns `host`, `state`, the signal and `inspect()`. The commands above merely update demonstration state; a real caller projects a new trace from its own source and notifies the presenter. An optional `subscribe()` binding may call `update()` when that state changes. ## Window and trigger `trace.samples` are linear sample amplitudes along `trace.timebaseMs` milliseconds. The presenter plots exactly that caller-provided window. It does not detect a crossing or retune an analyser: `triggerEdge`, `triggerLevel`, `availableTimeMs` and `timebaseMs` are caller state and command inputs. The [shared presenter shape]() governs host replacement and cleanup. New source identity and obsolete trace rejection belong to the caller; `destroy()` removes this presenter's observer, subscription, listeners, nodes and installed style, not the audio source.
    API **State and trace** | Member | Kind | Default | Description | |---|---|---|---| | `trace` | `OscilloscopeTrace?` | absent | Current `{samples, timebaseMs, triggered}` window. With no trace, the plot has no waveform. | | `trace.samples` | `ArrayLike` | required in trace | Linear amplitudes painted from left to right; the caller owns the data. | | `trace.timebaseMs` | `number` | required in trace | Elapsed milliseconds across the displayed sample window. | | `trace.triggered` | `boolean` | required in trace | Whether the caller found an eligible edge; controls the visible live-status wording. | | `trace.silent` | `boolean` | required in trace | Whether the sampled window is silent; displays "No signal" rather than a matching-edge message. | | `availableTimeMs` | `number` | required | Maximum sampled window available for the timebase control; invalid or non-positive values fall back to `100`. | | `timebaseMs` | `number` | required | Requested visible milliseconds, clamped in the control to the available window. | | `triggerLevel` | `number` | required | Linear amplitude threshold shown by the trigger line and native range input (`-1..1`). | | `triggerEdge` | `'off' \| 'rising' \| 'falling'` | required | Selected native edge control. `off` is free-run display. | | `frozen` | `boolean` | required | Freeze button state; the caller decides whether to keep replacing `trace`. | | `status` | `'waiting' \| 'live' \| 'paused' \| 'unavailable' \| 'frozen'` | required | Visible status message. | **Binding, options and handle** | Member | Kind | Default | Description | |---|---|---|---| | `snapshot()` | `() => OscilloscopeState` | required | Reads the latest caller-owned trace and controls. | | `setFrozen(value)` | `(boolean) => void` | required | Called by the native Freeze button. | | `setTimebaseMs(value)` | `(number) => void` | required | Called by the timebase range control, in milliseconds. | | `setTriggerLevel(value)` | `(number) => void` | required | Called by the trigger range control, in linear sample amplitude. | | `setTriggerEdge(value)` | `(OscilloscopeTriggerEdge) => void` | required | Called by the rising/falling/off select. | | `probe(timeMs, amplitude)` | `(number, number \| undefined) => void` | optional | Reports a changed probe position and nearest sample amplitude, if present. Time is relative to the displayed window. | | `subscribe(notify)` | `(callback) => () => void` | optional | Caller notifications for repaint; the handle also offers `update()`. | | `label` | `string` | `'Oscilloscope'` | Accessible group name. | | `onError` | `(unknown) => void` | absent | Receives snapshot, command, drawing, resize or cleanup failures. | | `stylesheet` | `boolean` | `true` | Install this presenter's stylesheet for this mount. | | `element` | `HTMLElement` | mounted root | Group root with `part="root surface"`. | | `selectedTimeMs` | `number` | `0` | Read-only current probe position from the displayed window's left edge. | | `update()` | `() => void` | - | Read `snapshot()` and redraw the display and native controls. | | `destroy()` | `() => void` | - | Release observer, subscription, controls, nodes and owned style. | **Accessibility.** The canvas is a focusable slider named "Inspect waveform time", with the selected time and optional amplitude in `aria-valuetext` and a matching text readout outside the canvas. Pointer movement probes the window. Arrow keys move by one percent, Shift+Arrow by ten percent, and Home/End select the ends. Freeze is a native `aria-pressed` button; trigger and timebase use labelled native controls. The status is a live status region.
    Styling The public root class is `.wui-oscilloscope`. Public parts are `root surface`, `header`, `readout`, `plot`, `controls`, `freeze`, `timebase`, `trigger-level`, `trigger-edge` and `status`. There are no `classNames` or `parts` mount options; internal `.wui-oscilloscope__*` selectors describe implementation nodes. `stylesheet:false` skips this mount's stylesheet but does not remove caller-owned or previously installed CSS. | Token | Fallback | Controls | |---|---|---| | `--wm-oscilloscope-foreground` | `--wm-foreground`, then dark ink | Root text. | | `--wm-oscilloscope-muted` | gray | Readout and status text. | | `--wm-oscilloscope-height` | `11rem` | Plot height, minimum `7rem`. | | `--wm-oscilloscope-plot-background` | dark ink | Canvas CSS background. | | `--wm-oscilloscope-accent` | teal | Range-input accent. | | `--wm-oscilloscope-border` | `--wm-border`, then gray | Native select and button borders. | | `--wm-oscilloscope-control-background` | `--wm-surface`, then white | Select and button backgrounds. | | `--wm-oscilloscope-selected-background`, `--wm-oscilloscope-selected-border` | pale mint / teal | Pressed Freeze button. | | `--wm-focus` | browser Highlight | Focus outlines. | The canvas currently paints its trace, grid, trigger line and probe with fixed colors, so CSS tokens alone do not recolor those pixels. Its geometry follows the caller's trace and the rendered host width.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-oscilloscope/) ## Related presenters - [`@webmusic/ui/level-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/level-analyzer/) - [`@webmusic/ui/spectrum-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/spectrum-analyzer/) - [`@webmusic/ui/transient-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/transient-analyzer/) - [`@webmusic/ui/analysis`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/analysis/) - [`@webmusic/ui/pitch`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/pitch/) - [`@webmusic/ui/harmony`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/harmony/) --- # @webmusic/ui/pitch > Live keyboard, staff and fretboard read-outs that press, spell and slide with the pitches a caller hands them. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/views-analysis/pitch/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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/ui/pitch` draws one set of sounding pitches on three surfaces at once — a keyboard, a staff and a fretboard — and keeps all three honest about the same instant. Each is a passive read-out: it takes a snapshot, presses what is sounding, fades what has just stopped, and never asks who is playing. The three know no music theory, and that is the boundary. A mark is *"MIDI 64, diatonic step 30, role `third`, labelled `E4`"* — never *"the third of Cmaj7"*. Spelling has to be the caller's answer, because `F#4` and `Gb4` are one key on a keyboard and two lines on a staff, and only the caller knows which it meant. Clefs go one step further: the caller passes the glyph **string** and the kit only positions it, so a mount given no `clefs` produces no text node at all. Reach for [note]() instead when the surface has to be **played** — that one owns pointer and keyboard input and emits notes. These three own no gestures. Use [analysis]() for the span-carrying cards and timelines that a playhead walks. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/pitch/) ## Import ```ts import {fretPositionsFor, mountKeyboard, mountStaff, staffPlacement} from '@webmusic/ui/pitch'; const keyboard = mountKeyboard(keyboardHost, { snapshot: () => ({ marks: [ {midi: 60, label: 'C4', role: 'root', since: chordStartedAt}, {midi: 64, label: 'E4', role: 'third', since: chordStartedAt}, {midi: 67, label: 'G4', role: 'fifth', since: chordStartedAt}, ], ghostPitchClasses: [0, 2, 4, 5, 7, 9, 11], octaveLabels: new Map([[60, 'C4']]), now: positionQuarters, }), subscribe: (notify) => store.subscribe(notify), }); const staff = mountStaff( staffHost, { snapshot: () => ({ marks: [{midi: 66, diatonic: 31, accidental: 'sharp', label: 'F#4', column: 0}], clefs: {upper: trebleClefGlyph}, activeColumn: 0, follow: 'anchor', }), }, {anchor: 0.35}, ); // The two pure helpers, for the caller that has to decide before it can hand // anything over. Both are arithmetic; WHICH spelling and WHICH grip are yours. staffPlacement(28).ledgers; // [28] — middle C stands on one ledger line fretPositionsFor(60, [64, 59, 55, 50, 45, 40], {firstFret: 3, fretCount: 5}); keyboard.key(60)?.dataset.phase; // 'attack', then 'sustain', then 'release' staff.destroy(); ```
    API All three mount options accept `surface?: 'default' | 'none'` (default `'default'`). Use `'none'` inside an already framed presenter; only outer paint and insets are suppressed. Three mounts, one shared vocabulary, and two pure helpers. Every mount follows the kit's shape: `mountX(host, binding, options?)`, a binding that answers `snapshot()` and may offer `subscribe()`, and a handle whose `destroy()` is idempotent. Mounting twice on one host replaces the first surface; the three kinds are different surfaces, not alternative renderings of one, so they do not displace each other. **Mounts and handles** | Export | Kind | Description | |---|---|---| | `mountKeyboard(host, binding, options?)` | `KeyboardHandle` | A keyboard that presses and releases with its input. `key(midi)` returns that key's node, or nothing when the pitch is outside the drawn range. | | `mountStaff(host, binding, options?)` | `StaffHandle` | Columns of noteheads on a real staff, with ledgers and accidentals. `note(column, diatonic)` returns one notehead group. | | `mountFretboard(host, binding, options?)` | `FretboardHandle` | A neck whose window slides to follow the hand. `dot(stringIndex, fret)` returns one stopped position. | | `staffPlacement(diatonic, system?)` | `StaffPlacement` | Where a diatonic step sits and what it has to stand on: a half-space `y` plus the `ledgers` it needs, ordered from the staff outward. The step is clamped to `C-1 … E10` — everything MIDI can name, with every ledger it needs — because a malformed `` makes a *finite* step of `1e9`, and a ledger line per line between staff and note is half a million SVG nodes. A non-finite step answers middle C. | | `fretPositionsFor(midi, tuning, window?)` | `FretPosition[]` | Every place inside the window that sounds one pitch. `tuning[i]` is the open pitch of string `i`. | | `pitchStyle` | `string` | The stylesheet all three install, as side-effect-free text. | **The shared mark** `PitchMark` is what every surface is told about one sounding pitch. | Member | Kind | Default | Description | |---|---|---|---| | `midi` | `number` | — | The pitch. Rounded; a non-finite value is dropped. | | `label` | `string` | — | The caller's spelling. The kit never invents a name — a surface prints only what it was given. | | `mark` | `string` | the role's abbreviation | The figure printed beside the colour. Pass one wherever the real figure is known: a role alone cannot tell a 9 from a 13. | | `role` | `ToneRole` | `'other'` | `'root'`, `'third'`, `'fifth'`, `'seventh'`, `'extension'`, `'bass'`, `'other'` or `'ghost'`. Picks the colour and the default abbreviation. | | `weight` | `number` | `1` | `0…1` emphasis: velocity, weight or confidence. **Spent**, not merely accepted — it multiplies the mark's opacity on all three surfaces, through the same `--wui-pitch-weight` the age fade reads. | | `active` | `boolean` | `true` | `false` withdraws the mark without removing it from the array. | | `since` | `number` | — | When this pitch started sounding, on the same ruler as the surface's `now`. | `since` and `now` are the caller's own units — quarters, seconds, bars, anything monotonic. Given both, a surface publishes `0…1` on each mark for how long it has been held and dims it as it ages; given neither, a mark is simply on or off, which is a complete and supported way to use all three. `ageSpan` decides how many of those units the `0…1` covers, and it is the **same option with the same default on all three surfaces** — one pitch at one instant may not be half a beat old on the keyboard and a whole one on the staff. **Keyboard** | `KeyboardState` | Kind | Default | Description | |---|---|---|---| | `marks` | `readonly PitchMark[]` | — | What is sounding. | | `low` / `high` | `number` | `48` / `84` | The drawn range, in MIDI. | | `ghostPitchClasses` | `readonly number[]` | — | In the key but not sounding, drawn as a wash. A ghost key carries both `data-ghost="true"` and `data-active="false"`, so nothing about it depends on seeing the colour. | | `labels` | `'none' \| 'marked' \| 'white' \| 'all'` | `'marked'` | Which keys may print a name. | | `octaveLabels` | `ReadonlyMap` | — | The ruler's text. Whether middle C is called C4 is a convention, not a fact, so the caller supplies it. | | `now` | `number` | — | The caller's current position. | | `KeyboardOptions` | Kind | Default | Description | |---|---|---|---| | `label` | `string` | derived from the marks | Accessible name for the surface. | | `motion` | `MotionMode` | `'auto'` | See **Motion** below. | | `follow` | `'active' \| 'none'` | `'none'` | Scroll a wide keyboard to a newly active group that is outside its viewport. Repeated snapshots and releases preserve the user's scroll. | | `fitToWidth` | `boolean` | `false` | Fit the entire normalized range to the container, overriding explicit key widths and the CSS minimum width. Heights remain unchanged. | | `whiteKeyWidth` | `number` | responsive | Fixed natural-key width in CSS px; wider boards scroll locally. Ignored while `fitToWidth` is `true`. | | `blackKeyWidth` | `number` | `0.62 × white width` | Accidental-key width in CSS px, capped at white width. Alone derives white width as `value / 0.62`. Ignored while `fitToWidth` is `true`. | | `whiteKeyHeight` | `number` | theme height | Natural-key length in CSS px. | | `blackKeyHeight` | `number` | theme proportion | Accidental-key length in CSS px, capped at white-key height. | | `release` | `number` | `140` | How long a lifted key keeps fading, in ms. `0` in any reduced mode. | | `trail` | `boolean` | `false` | Leave the role colour on a key for the length of its release. | | `ageSpan` | `number` | `4` | How many of the caller's units the published age spans. | | `classNames` / `parts` | `{root?, board?}` | — | Extra classes and `part` tokens on the two structural nodes. | | `onError` | `(error: unknown) => void` | — | Receives a faulty snapshot or a failed repaint. | | `density` | `'comfortable' \| 'compact'` | `'comfortable'` | The compact tier: same information, less room. | | `scheme` | `'inherit' \| 'system' \| 'light' \| 'dark'` | `'inherit'` | Pin the colour scheme instead of following the page. | | `stylesheet` | `boolean` | `true` | `false` paints the identical declarations onto the nodes instead. | `KeyboardHandle` is `{element, board, key(midi), update(), destroy()}`. With `fitToWidth: false` and no explicit geometry, the board fills its container while preserving a minimum white-key width. A wide range scrolls inside the root instead of compressing adjacent labels. Long labels stay within their key; their full text remains in the title and accessible name. An omitted octave ruler occupies no space. Positive finite geometry options override the corresponding dimensions. Invalid, zero or negative values use defaults. Exact widths remain fixed when the container resizes; heights do not change pitch spacing. Recreate the presenter without an option to restore its responsive/theme default. Use the same widths, MIDI range and shared scroll container to align with a separately rendered waterfall; `follow: 'none'` keeps the keyboard from panning independently. `follow: 'active'` reveals new arrivals without moving playback or changing the drawn range. If the held group is wider than the viewport, it is centered and remains locally scrollable. Use `fitToWidth: true` to show every key in the normalized range without horizontal scrolling. Key widths follow the container, even when explicit `whiteKeyWidth`, `blackKeyWidth` or a CSS minimum width is supplied; black keys use `0.62 × white width`. Height options still apply, and fitting changes no marks or pitch range. Remount with `fitToWidth: false` or omit it to restore the supplied fixed widths or the normal responsive minimum width. All three surfaces declare the harmony **token layer on their own root**, so one dropped into a bare `
    ` looks the same as one in a workbench dock that already carries it. `density` and `scheme` are the two dials a caller can move; both are stamped as `data-*` and both are painted inline when `stylesheet: false`. **Staff** `StaffMark` extends `PitchMark` with `diatonic` (required — C0 = 0, middle C = 28; it follows the spelling, not the pitch), an optional `accidental` of `'sharp' | 'flat' | 'natural' | 'double-sharp' | 'double-flat'`, and a `column`, which is what makes marks sharing a column one chord. | `StaffState` | Kind | Default | Description | |---|---|---|---| | `marks` | `readonly StaffMark[]` | — | Every notehead, across every column. | | `system` | `'grand' \| 'treble' \| 'bass'` | `'grand'` | Which staves are drawn. | | `clefs` | `{upper?, lower?}` | — | Glyph **strings** the caller owns. Omit them and no text node is created. | | `keySignature` | `readonly {diatonic, accidental}[]` | — | Accidentals placed after the clef, in the given order. | | `columns` / `activeColumn` | `number` | widest column + 1 / `0` | How many columns exist, and which one is sounding. | | `follow` | `'anchor' \| 'none'` | `'none'` | `'anchor'` pins the active column at a fixed x and lets the columns flow past it. Both produce the identical noteheads with the identical attributes; only the reel's transform differs. | | `emptyLabel` | `string` | — | Announced when there is nothing to draw. | | `now` | `number` | — | The caller's current position. | `StaffOptions` adds `anchor` (`0…1`, default `.35`) and `ageSpan` (default `4`) to `label`, `motion`, `classNames`, `parts`, `onError` and `stylesheet`; `classNames` and `parts` name `{root?, svg?}`. `StaffHandle` is `{element, svg, note(column, diatonic), update(), destroy()}`. Marks sharing a column stack into one chord. Different accidentals on the same diatonic step retain separate noteheads, so simultaneous C and C♯ are both drawn. `note(column, diatonic)` returns the first head at that step; it does not select among accidentals. The staff fills the available inline width. Where `ResizeObserver` is available, staff lines extend to that width while noteheads, clefs and column spacing retain their existing proportions. The observer uses the rendered SVG dimensions, so the staff-space token still owns vertical sizing; no numeric theme tokens are read back. Dense accidental stacks reserve space after the clef and signature. Content wider than the available space scrolls at the staff-space size instead of shrinking noteheads. Without an observer, the content-sized viewBox remains the fallback. `destroy()` disconnects observation together with the subscription and pending effects. **Fretboard** `FretMark` is `{stringIndex, fret, midi?, role?, label?, mark?, finger?, active?, since?, weight?, id?}`. String `0` is the one drawn at the edge of the board and fret `0` is the open string; `finger` overrides the printed abbreviation, because which finger takes a stop is not derivable from it. `midi` is never read for geometry — a string and a fret are enough to draw a dot — and is stamped as `data-midi`, which is what lets a fretboard be checked against a keyboard and a staff without anything re-deriving a tuning. | `FretboardState` | Kind | Default | Description | |---|---|---|---| | `marks` | `readonly FretMark[]` | — | The stopped positions. | | `strings` | `number` | `6` | Up to `12`. | | `firstFret` | `number \| 'auto'` | `0` | The lowest fret in view. `'auto'` frames the marks and keeps a window that still holds every one of them, so a shape alternating between the two ends of a window does not drag the neck back and forth. | | `fretCount` | `number` | `5` | How wide the window is. It is closed at both ends — `firstFret … firstFret + fretCount` — so at the nut it is the open string plus five stopped frets, the five-fret box every chord chart draws. `fretPositionsFor` admits exactly the span the neck has room to draw. | | `muted` | `readonly number[]` | — | Strings marked as not sounding; a mark on the same string wins. The markers live in a fixed gutter, so a window at the fifth fret does not take them with it. | | `barre` | `readonly {fret, fromString, toString}[]` | — | Geometry, not theory: the caller says which strings a bar covers. | | `inlays` | `readonly number[]` | — | Which frets carry a position dot. | | `stringLabels` | `readonly string[]` | — | The tuning. **Drawn** beside the strings in the fixed gutter, and read into the accessible description. A diagram that only spoke them left a sighted reader no way to tell a drop-D box from a standard one. | | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Only the coordinates turn: the node set and every `data-*` are identical either way. A turned board is sized as a chord box — tall and narrow, with the strings spanning what they span across a horizontal neck. | | `emptyLabel` | `string` | — | Announced when there is nothing to draw. | | `now` | `number` | — | The caller's current position. | `FretboardOptions` is `label`, `motion`, `release`, `ageSpan`, `classNames`, `parts`, `onError`, `stylesheet`, plus the geometry options below. `classNames` and `parts` accept `{root?, svg?}`. `FretboardHandle` is `{element, svg, dot(stringIndex, fret), update(), destroy()}`. | Geometry option | Default | Description | | --- | --- | --- | | `fretWidth` | responsive | Fixed distance between adjacent fret wires in CSS px. | | `stringSpacing` | density geometry | Distance between adjacent strings in CSS px. | | `stringWidth` | density stroke | String line thickness in CSS px, independent of SVG viewport scaling. | All three accept positive finite numbers; missing/invalid values use defaults. Explicit fret spacing stays fixed as the host grows; a narrow host scrolls. String spacing changes the board height without changing the fret window or pitch locations. These are layout options, not values in the musical binding. With no explicit fret width, a horizontal board fills the available width. Where `ResizeObserver` is available, extra room expands fret spacing while circles, text and string spacing retain their size. The minimum geometry unit keeps markings readable, including at compact density; narrow boards scroll within their root. The height token is a minimum alongside this readable geometry floor. A vertical board retains chord-box proportions. Resizing changes no marks or musical window, and `destroy()` disconnects both sizing and overflow observation. Without an observer, the minimum-size geometry remains readable but wider boxes can retain slack around the drawing. The window follows the shape it was given, and only as far as it has to. A surface with no marks is at the nut, so a shape that fits there does not move the neck at all, and one that does not slides up by as little as it can and stops — hysteresis, so a shape alternating between the two ends of a window does not drag the neck back and forth once per chord. The neck draws **only the window**: a board at the third position has no nut in it, and an open string keeps its marker in the fixed gutter beside the muted ones, because an open string is a fact about a string rather than a place on the neck. An **open** string is drawn as a ring and a stopped one as a filled disc — the distinction every chord chart in print makes, and the first thing a player reads off one. The ring carries the role colour on its stroke, so the greyscale channel survives either way. A mark the window cannot hold is not drawn at all rather than drawn off the neck, and it does not enter the accessible sentence; the position marker prints `5fr` the way a chord book writes it, never a bare `5` that would read as a fingering beside two other kinds of number. **Motion** `MotionMode` is `'auto' | 'continuous' | 'stepped' | 'none'`. `'auto'` asks the nearest ancestor's `data-motion` **first**: these surfaces render into a caller's light DOM where a shell answers that question once for a whole page, and six mounts each reaching their own conclusion would be six answers and no answer. With nothing declared above it, a surface asks `prefers-reduced-motion` itself, guarded, and the stylesheet carries the same media query so a page whose JavaScript never asked is covered anyway — an explicit `'continuous'` does not out-rank the viewer's own setting. A reduced mode produces a **pixel-identical** surface with the transitions spent down to `0s` — turning the movement off is supported; turning the layout into a different layout is not. None of the three runs an animation frame loop. Every move is one attribute write plus a CSS transition, so a page holding six of these wakes up exactly as often as its data changes. Repeated `update()` calls diff by identity — a key, a notehead or a dot that was already sounding is not rebuilt — which is what lets a transition finish. **Accessibility.** Each root is `role="img"` with an accessible name naming the labels it was given, and the drawing itself is `aria-hidden` — forty-nine focusable keys would be a screen-reader disaster, so a surface has exactly one accessible name. Empty states are `role="status"`. Nothing here plays a note or emits an event. A root whose content overflows becomes one keyboard focus stop so native scrolling can reach its complete drawing; it leaves the tab order when it fits. A **change** to the sounding set is not announced: the name is rewritten, and a rewritten name on a static `role="img"` is not read out. Announcing it belongs to the shell that holds one reading per frame, not to four docks announcing the same chord four times.
    Styling Use the shared [surface and palette tokens]() for background (including `transparent`), foreground, accent, outer border and corner radius. Presenter-specific tokens below take precedence within their documented roles. All three install `pitchStyle`, or paint the same declarations onto the nodes when `stylesheet: false`. Both paths are generated from one record, so the sheet and the inline form cannot describe two different surfaces. Every colour is a chain ending in a literal, never a bare `var()`, so a surface mounted on a node carrying no tokens still paints. Exact keyboard dimensions, fret spacing, string gaps and string thickness use the typed geometry options above. CSS tokens retain ownership of colors, frames and default sizing; explicit numeric options override their corresponding default geometry without changing the bound musical state. `fitToWidth: true` takes precedence over keyboard width options and minimum-width tokens. The outer frame uses `--wm-pitch-surface-background`, `-border`, `-padding` and `-radius` through the shared component quartet, with transparent/zero-inset defaults. Key faces use `--wm-note-key` / `--wm-note-key-alt`; their fallbacks are `--wm-surface` / `--wm-surface-inverse`. Key outlines use `--wm-note-border` → `--wm-control-border` → `--wm-border`, then `#d8d8d8` in a light scheme (matching other neutral controls and frames) or `#333` in a dark scheme. Labels and corners use `--wm-note-label` and `--wm-note-radius` through the shared foreground and control-radius tokens. Staff paint uses `--wm-pitch-staff-line` → `--wm-foreground`. Fretboard paint uses `--wm-pitch-fret-neck` → `--wm-surface` and `--wm-pitch-fret-wire`, `-nut`, `-string`, `-inlay` → `--wm-foreground`. Unthemed surfaces use neutral light/dark defaults: white keys `#fff` / `#282828`, black keys `#222` / `#111`, staff lines `#666` / `#888`, and fretboard ground `#eee` / `#222`. Musical marks retain their caller-supplied role colors. **Compatibility and state tokens** Older `--wui-*` inputs below remain compatible, but new themes should use the public tokens above. Presenter-owned state values such as note weight are not theme configuration. | Property | Default | Controls | |---|---|---| | `--wui-pitch-key-min-width` | `2.5rem`; `2.25rem` compact | Minimum white-key width before local scrolling; ignored while `fitToWidth` is `true`. | | `--wui-pitch-fretboard-unit` | `2.2px`; `2px` compact | Minimum CSS length per fretboard geometry unit, preserving dots and labels. | | `--wui-pitch-key-white` / `--wui-pitch-key-black` | `#fff` / `#282828` and `#222` / `#111` | White and black key faces. | | `--wui-pitch-key-border` | `light-dark(#d8d8d8, #333)` after public border tokens | Key outline. | | `--wui-pitch-key-label` | `#666` / `#aaa` | The name and abbreviation printed on an unmarked key. | | `--wui-pitch-key-radius` | `0` | Key corner radius. | | `--wui-pitch-key-black-height` | `62%` | How far a black key reaches down the board. | | `--wui-pitch-staff-line` | `#666` / `#888` | Staff lines, ledgers, brace, clef and key signature. | | `--wui-pitch-fret-neck` / `--wui-pitch-fret-wire` / `--wui-pitch-fret-nut` | `#eee` / `#222`; `#999` / `#777`; `#444` / `#ccc` | Fretboard ground, fret wires and nut, as light/dark pairs. | | `--wui-pitch-fret-string` / `--wui-pitch-fret-inlay` | `#888` / `#aaa`; `#ccc` / `#444` | Strings and position dots, as light/dark pairs. | | `--wui-harmony-keyboard-height` | `92px` | Board height. | | `--wui-harmony-staff-space` | `9px` | One staff space; the drawn height is derived from it. | | `--wui-harmony-fretboard-height` | `104px` | Neck height. | | `--wui-harmony-motion-tone` | `90ms` | How fast a key or a notehead takes its colour. | | `--wui-harmony-motion-release` | `140ms` | How fast a lifted mark fades. | | `--wui-harmony-motion-column` | `200ms` | How fast an anchored reel slides. | | `--wui-harmony-motion-ease` | `ease-out` | The easing all three share. | | `--wui-harmony-ink` / `--wui-harmony-ink-muted` | inherit / muted | Text on the root, and the octave ruler and empty states. | | `--wui-harmony-font` / `--wui-harmony-font-mono` | inherit / `ui-monospace` | Prose and the fixed-width names. | | `--wui-harmony-size-micro` / `--wui-harmony-size-label` | `.6875rem` / `.75rem` | Key names and empty-state text. | Tone colour comes from the shared degree palette: a sounding node is painted from its `role`, and `--wui-degree-root` and its siblings sit in front of the `--wm-degree-*` names a theme sets. Each sounding node also publishes how long its pitch has been held, as `0…1` across `ageSpan` of the caller's own units, and **spends it**: a held pitch dims as it ages, by a third of its opacity across the span. It is `opacity` on a key and `fill-opacity` on a notehead and a dot, because `opacity` on those two already answers to the staff's `data-when` and the fretboard's `data-phase`. A theme that disagrees overrides one declaration: ```css .wui-pitch-keyboard__key[data-active='true'] { opacity: calc(1 - var(--wui-harmony-age, 0) * 0.5); } ``` **Classes and state attributes** | Node | Class | Carries | |---|---|---| | Keyboard root / board / ruler | `.wui-pitch-keyboard`, `.wui-pitch-keyboard__board`, `.wui-pitch-keyboard__ruler` | `data-motion` on the root. | | One key | `.wui-pitch-keyboard__key`, plus `.wui-pitch-keyboard__key--white` or `.wui-pitch-keyboard__key--black` | `data-midi`, `data-active`, `data-role`, `data-phase`, `data-ghost`. | | Staff root / drawing / reel | `.wui-pitch-staff`, `.wui-pitch-staff__svg`, `.wui-pitch-staff__reel` | The reel carries the follow transform. | | One notehead group | `.wui-pitch-staff__note`, holding `.wui-pitch-staff__head`, `.wui-pitch-staff__ledger` and `.wui-pitch-staff__accidental` | `data-column`, `data-diatonic`, `data-midi`, `data-role`, `data-active`, `data-phase`, `data-when`. | | Fretboard root / drawing / reel | `.wui-pitch-fretboard`, `.wui-pitch-fretboard__svg`, `.wui-pitch-fretboard__reel` | The reel carries the window transform; `.wui-pitch-fretboard__open` and `.wui-pitch-fretboard__gutter` do not move with it. | | One stopped position | `.wui-pitch-fretboard__dot`, holding `.wui-pitch-fretboard__disc` | `data-string`, `data-fret`, `data-midi`, `data-role`, `data-active`, `data-phase`. | `data-phase` is `attack` on the frame a pitch arrives, `sustain` once it has settled, and `release` while it fades — and it is written only when it **changes**, so a repaint with an unchanged set writes nothing and no transition restarts. `data-when` is the staff's distance from the sounding column: `past`, `now`, `next` or `far`. Both of those are written as **attributes**, not only as colours, so what a surface is saying can be read without seeing it — and the three surfaces always agree, mid-release included: a lifted pitch is `data-active="false"` with `data-phase="release"` on every one of them at once. Repaints do not rebuild. A key, a notehead and a dot are diffed by identity; a notehead's children are rebuilt only when its GEOMETRY moves, and a change of role is a guarded `fill` on the ellipse that is already there. A replaced element has no previous computed style and therefore cannot transition, so this is what makes the colour change a fade rather than a cut — and it is why a surface whose data did not change writes nothing at all. The `parts` option adds `part` tokens for a shadow-DOM host; `classNames` adds classes without replacing the structural ones. Neither is required.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/score/element/view/score-pitch-view/) ## Related presenters - [`@webmusic/ui/level-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/level-analyzer/) - [`@webmusic/ui/spectrum-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/spectrum-analyzer/) - [`@webmusic/ui/oscilloscope`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/oscilloscope/) - [`@webmusic/ui/transient-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/transient-analyzer/) - [`@webmusic/ui/analysis`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/analysis/) - [`@webmusic/ui/harmony`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/harmony/) --- # @webmusic/ui/spectrum-analyzer > An inspectable log-frequency spectrum with a probe, freeze, and peak hold over caller-supplied dB bins. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/views-analysis/spectrum-analyzer/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountSpectrumAnalyzer()` turns a live frequency frame into an inspectable spectrum. An operator can point to a frequency, move the probe by keyboard, freeze the trace, hold maxima, and reset those held peaks. The kit draws and owns these interactions; the application owns FFT sampling and source identity. Unlike [`meter mode="spectrum"`](), this is a log-frequency investigation surface with a frequency/dB readout, not a bank of normalized bars. The live demo generates harmonic dB bins to exercise the real controls; it does not sample the user's audio device. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/spectrum-analyzer/) ## Import ```ts import {mountSpectrumAnalyzer} from '@webmusic/ui/spectrum-analyzer'; const handle = mountSpectrumAnalyzer(host, { snapshot: () => state, setFrozen: (value) => setFrozen(value), setPeakHold: (value) => setPeakHold(value), probe: (hertz, db) => inspect(hertz, db), subscribe: (notify) => store.subscribe(notify), }); // Call handle.destroy() when this surface leaves the application. ``` The caller supplies `host`, `state`, a notification store and commands. The presenter subscribes to state changes and also exposes `handle.update()` for a caller-paced repaint. `setFrozen()` changes caller state; the caller decides whether to stop replacing `frame`. `peakHold` is presentation-local and can be reset without changing the FFT source. ## Frame and source identity `frame.bins` contains dB values for the `fftSize / 2` frequency bins. The presenter's frequency mapping uses `sampleRate / fftSize` Hz per bin; `minDb` and `maxDb` set the vertical axis. A new `sourceRevision` clears held maxima, so a trace from an old deck does not contaminate the next one. The caller increments that revision when the observed source changes and avoids publishing stale frames after replacement. No `AudioContext`, analyser or transport is created here.
    API **State and frame** | Member | Kind | Default | Description | |---|---|---|---| | `frame` | `SpectrumAnalyzerFrame?` | absent | Current FFT `bins`, `sampleRate`, `fftSize`, `minDb` and `maxDb`; absent leaves the plot empty. Bin values are dB, not normalized bytes. | | `sourceRevision` | `number` | required | Invalidate locally held peaks when the source changes. | | `frozen` | `boolean` | required | Freeze button state; the caller retains the selected frame. | | `peakHold` | `boolean` | required | Overlay maximum dB per bin while enabled. | | `minFrequency` / `maxFrequency` | `number` (Hz) | required | Visible log-frequency range, clamped to the frame's Nyquist frequency. | | `status` | `'waiting' \| 'live' \| 'paused' \| 'unavailable' \| 'frozen'` | required | Visible signal state. | **Binding, options and handle** | Member | Kind | Default | Description | |---|---|---|---| | `snapshot()` | `() => SpectrumAnalyzerState` | required | Reads the current frame and interaction state. | | `setFrozen(value)` / `setPeakHold(value)` | `(boolean) => void` | required | Commands from the corresponding native buttons; update state and notify. | | `probe(hertz, db)` | `(number, number \| undefined) => void` | optional | Fires only when the user selects a new frequency. The dB value comes from the nearest current bin. | | `subscribe(notify)` | `(callback) => () => void` | optional | Supplies caller-paced repaint notifications. | | `label` | `string` | `'Spectrum analyzer'` | Accessible group name. | | `onError` | `(unknown) => void` | absent | Receives snapshot, probe, resize, paint or cleanup failures. | | `stylesheet` | `boolean` | `true` | Install the exported stylesheet in this host. | | `element` | `HTMLElement` | mounted root | Root with `part="root surface"`. | | `selectedFrequency` | `number` (Hz) | `1000` before range clamp | Read-only selected probe frequency. | | `update()` | `() => void` | — | Read `snapshot()` and redraw. | | `resetPeaks()` | `() => void` | — | Drop presenter-owned maxima without changing source state. | | `destroy()` | `() => void` | — | Release listener, subscription, resize observer, owned nodes and stylesheet. | See the [shared presenter shape]() for host ownership. The callback never receives decoded PCM or a player. Replacing or disposing a borrowed audio graph belongs to the caller. **Accessibility.** The plot is a focusable frequency slider with `aria-valuemin`, `aria-valuemax`, `aria-valuenow` in Hz and a frequency/dB `aria-valuetext`. Arrow keys move a semitone, Shift+Arrow moves an octave, and Home/End select the range limits. Pointer down or movement probes the plot. Freeze and Peak hold use native `aria-pressed` buttons; Reset peaks is a native button. The current probe and signal status remain readable outside the canvas.
    Styling The root is `.wui-spectrum-analyzer`. Public parts are `root surface`, `toolbar`, `freeze`, `peak-hold`, `reset-peaks`, `readout`, `plot` and `status`. There is no `classNames` or `parts` option. `stylesheet:false` skips installing this mount's stylesheet; it does not erase caller CSS. | Token | Fallback | Controls | |---|---|---| | `--wm-spectrum-foreground` | `--wm-foreground`, then dark ink | Root text. | | `--wm-spectrum-muted` | `--wm-foreground-muted`, then gray | Probe readout and status. | | `--wm-spectrum-height` | `11rem` | Plot height, minimum `7rem`. | | `--wm-spectrum-plot-background` | dark ink | Canvas CSS background. | | `--wm-spectrum-border` | `--wm-border`, then gray | Button borders. | | `--wm-spectrum-control-background` | `--wm-surface`, then white | Buttons. | | `--wm-spectrum-selected-background` / `--wm-spectrum-selected-border` | mint / teal | Pressed buttons. | The canvas drawing currently uses fixed axis and trace colors; the tokens above style the surrounding DOM and canvas background, not those drawn pixels. Internal `.wui-spectrum-analyzer__*` classes are structural selectors. Focus uses the shared `--wm-focus` token.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-spectrum-analyzer/) ## Related presenters - [`@webmusic/ui/level-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/level-analyzer/) - [`@webmusic/ui/oscilloscope`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/oscilloscope/) - [`@webmusic/ui/transient-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/transient-analyzer/) - [`@webmusic/ui/analysis`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/analysis/) - [`@webmusic/ui/pitch`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/pitch/) - [`@webmusic/ui/harmony`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/harmony/) --- # @webmusic/ui/transient-analyzer > Present caller-supplied attack strength and hit history with sensitivity, freeze and clear actions. Canonical documentation: https://koperative-lab.github.io/WebMusic/uikit/views-analysis/transient-analyzer/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/ui@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. `mountTransientAnalyzer()` presents the current attack strength, threshold, recent hit pattern and hit count. Its native Sensitivity, Freeze and Clear controls call into the application's actions. The caller owns signal sampling, the detection algorithm and the history; this presenter does not infer onsets, BPM or a beat grid. For a player-connected implementation, use [``](). [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/transient-analyzer/) ## Import ```ts import {mountTransientAnalyzer, type TransientAnalyzerState} from '@webmusic/ui/transient-analyzer'; const state: TransientAnalyzerState = { sample: {strength: 0.62, hit: true}, history: [{strength: 0.62, hit: true}], sensitivity: 0.55, threshold: 0.206, frozen: false, hitCount: 1, status: 'Live', }; const handle = mountTransientAnalyzer(host, { onSensitivityChange: (value) => { state.sensitivity = value; state.threshold = 0.08 + (1 - value) * 0.28; handle.update(state); }, onFreezeChange: (value) => { state.frozen = value; handle.update(state); }, onClear: () => { state.sample = undefined; state.history = []; state.hitCount = 0; state.lastIntervalMs = undefined; handle.update(state); }, }); handle.update(state); // Call handle.destroy() when this surface leaves the application. ``` The caller supplies `host`, `state` and the action callbacks. It calls `update(state)` after each new observation or state change; this presenter has no `snapshot()` or `subscribe()` contract and does not sample an audio graph itself. The [shared presenter shape]() covers one mounted presenter per host and cleanup. ## Evidence and lifetime `sample.strength`, `history[*].strength`, `sensitivity` and `threshold` are normalized `0..1` values supplied by the caller. The most recent 64 history samples occupy fixed display columns, with `hit` coloring accepted cues. The presenter never calculates a threshold from sensitivity; the caller must keep those values consistent when Sensitivity changes. `lastIntervalMs` describes the gap between accepted cues when known, not a beat period. Replacing the underlying source requires the caller to discard stale observations before calling `update()`. `destroy()` releases native listeners, nodes and the stylesheet; it neither disposes a detector nor stops a player. Unlike presenters with an `onError` option, this entry does not catch errors thrown by caller actions; the caller should handle failures in its own action callbacks.
    API **State** | Member | Kind | Default | Description | |---|---|---|---| | `sample` | `TransientAnalyzerSample?` | absent | Latest `{strength, hit}` observation. Without one, the Strength readout is empty. | | `history` | `readonly TransientAnalyzerSample[]` | required | Ordered observations; the newest 64 are drawn. Each sample has normalized `strength` and boolean `hit`. | | `sensitivity` | `number` | required | Caller-provided `0..1` value shown by the labelled native range control. | | `threshold` | `number` | required | Caller-provided normalized threshold shown across the history plot. | | `frozen` | `boolean` | required | Freeze button state; the caller decides whether to keep supplying observations. | | `hitCount` | `number` | required | Number of accepted cues displayed since the caller's reset. | | `lastIntervalMs` | `number?` | absent | Most recent gap between accepted cues, in milliseconds. | | `status` | `string` | required | Text shown in the status region. | **Actions and handle** | Member | Kind | Default | Description | |---|---|---|---| | `onSensitivityChange(value)` | `(number) => void` | required | Called by the native range input with a `0..1` value. | | `onFreezeChange(value)` | `(boolean) => void` | required | Called by the Freeze button with its next state. | | `onClear()` | `() => void` | required | Called by the Clear button; the caller resets its evidence. | | `element` | `HTMLElement` | mounted root | Named section with `part="root surface"`. | | `update(state)` | `(TransientAnalyzerState) => void` | required for repaint | Replace readouts, threshold, bars and control state from caller-owned data. | | `destroy()` | `() => void` | - | Release listeners, nodes, installed style and host claim. | There are no mount options, subscription, automatic sampling or presenter events. The actions do not themselves alter the audio graph. **Accessibility.** Freeze and Clear are native buttons; Freeze reports its state with `aria-pressed`. Sensitivity is a labelled range input with a numeric output. The plot has an `img` role and a text alternative for current strength, threshold and hit count. Numeric readouts and a `status` role remain available without reading the plot's colors.
    Styling The root is `.wui-transient-analyzer`. Public parts are `root surface`, `plot`, `freeze`, `clear` and `sensitivity`; internal `.wui-transient-analyzer__*` classes are structural. This presenter has no `classNames`, `parts` or `stylesheet` option: mounting installs its stylesheet and `destroy()` removes the style node it owns. | Token | Fallback | Controls | |---|---|---| | `--wm-transient-analyzer-foreground` | `--wm-foreground`, then dark ink | Root text. | | `--wm-transient-analyzer-background` | `--wm-surface`, then white | Root background. | | `--wm-transient-analyzer-border` | `--wm-border`, then gray | Root border. | | `--wm-transient-analyzer-padding` | `.85rem` | Root inset. | | `--wm-transient-analyzer-strength` | teal | Strength bars and range accent. | | `--wm-transient-analyzer-hit` | orange | Accepted-hit bars. | | `--wm-transient-analyzer-threshold` | dark ink | Dashed threshold line. | | `--wm-muted`, `--wm-border`, `--wm-surface-muted`, `--wm-surface`, `--wm-accent`, `--wm-control-radius` | stylesheet fallbacks | Secondary text, plot/control surfaces, pressed button and corners. | Bar height and threshold position come from the supplied state, not CSS variables. Forced-colors mode uses system colors for bars and the threshold line.
    ## Used by - [``](https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-transient-analyzer/) ## Related presenters - [`@webmusic/ui/level-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/level-analyzer/) - [`@webmusic/ui/spectrum-analyzer`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/spectrum-analyzer/) - [`@webmusic/ui/oscilloscope`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/oscilloscope/) - [`@webmusic/ui/analysis`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/analysis/) - [`@webmusic/ui/pitch`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/pitch/) - [`@webmusic/ui/harmony`](https://koperative-lab.github.io/WebMusic/uikit/views-analysis/harmony/)