# WebMusic component references > Web Component, Headless and UI presenter contracts, examples, lifecycle and styling. 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) # > Inspect sampled RMS, sample peak, crest and a peak threshold from a live audio graph. Canonical documentation: https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-level-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 level analyzer is a live dynamics tool beside an [``](). Its recent-sample display, peak hold and adjustable threshold help inspect the signal while it sounds. For offline loudness estimates over a decoded clip, use [`measureLoudness`](). Neither this tool nor the current API establishes inter-sample true peak. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/audio/element/analyze/audio-level-analyzer/) ## Import ```html ``` 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]() --- # 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]() --- # > A seekable chord lane with selected-passage analysis data. Canonical documentation: https://koperative-lab.github.io/WebMusic/score/element/analyze/score-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. The lane groups written notes into up to two beat-aligned cells per measure and names each cell's combined pitch material. The notes need not sound simultaneously: this is a readable harmonic summary of a passage, not a claim that every note in the cell forms one held chord. Select a cell to inspect its evidence and seek the connected player. `.analysis` provides its score-note evidence, candidate readings and key-relative degree for application-owned inspectors. Use a sibling [``]() for individual notes over time, or [``]() for the chord currently sounding in playback. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/score/element/analyze/score-chord-analysis/) ## Import ```html ``` 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. --- # 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 |
    --- # @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/)