# WebMusic composition and usage patterns > Curated task sections from the owning public references. Follow the source links for complete API tables. 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) # Build a browser player Source: [Quick Start](https://koperative-lab.github.io/WebMusic/quick-start/) · [Complete Markdown](https://koperative-lab.github.io/WebMusic/agent-context/quick-start.md) # Quick Start > Use WebMusic's npm packages to build a score player with synchronized notation and a three-octave piano keyboard. Canonical documentation: https://koperative-lab.github.io/WebMusic/quick-start/ Source snapshot 1377617e9c9a1f153606135c04f6d9fbfecee6cd8b42698c3e9042a51a83b2ba: @webmusic/kernel@0.1.0, @webmusic/ui@0.1.0, @webmusic/score@0.1.0, @webmusic/audio@0.1.0, @webmusic/bridge@0.1.0. Includes Audio/Bridge additions; package version labels do not establish published compatibility. Match this source checkout's exports and declarations. The manifest retains the exact comparison with the unchanged main release baseline. Build a player for *Arabesque No. 1*, supplied as a compressed MusicXML score. One player loads the file; a staff view follows its position and a three-octave piano keyboard highlights sounding notes within its C3–C6 range. Playback uses the built-in oscillator, so no SoundFont is needed. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/quick-start/) Press **Play** to enable sound. Try pausing and seeking with the player's slider: both views follow the same player. The keyboard is a visual readout; for playable keys, see [``](). ## Install the packages In your browser application, install WebMusic's Score package and its Kernel and UI dependencies: ```bash npm install @webmusic/score @webmusic/kernel @webmusic/ui ``` Score supplies the musical behavior and Web Components; Kernel supplies runtime primitives, and UI supplies their controls and views. The example below uses TypeScript and an ES-module bundler such as Vite to resolve the package imports. ## Add the player and its views Download [Arabesque No. 1 (MXL)]() and save it as `public/arabesque.mxl` in your application. The player reads compressed MusicXML directly; no manual extraction is needed. The example assumes that your application serves this file at `/arabesque.mxl`; set `src` to your file's public URL. Put this markup in `index.html`. The player owns the file load, musical state and sound; the two `player="#piece"` selectors attach views to that same instance. ```html
``` ## Register the components Add this to `src/main.ts`. Importing the `/element` entries gives you the registration functions; calling them makes the three tags available to the browser. ```ts import {defineScorePlayerElement} from '@webmusic/score/play/element'; import {definePitchViewElement, defineScoreViewElement} from '@webmusic/score/view/element'; defineScorePlayerElement(); defineScoreViewElement(); definePitchViewElement(); ``` ## How it works | Code | What it does | | --- | --- | | `` | Loads the MusicXML once and owns playback, sound and the current position. Its controls let you play, pause and seek. | | `time-control="full"` | Shows elapsed time and total duration beside the player controls. | | `` | Borrows the loaded score and playback position to draw notation with a following playhead and active-note highlights. | | `` | Borrows the same player's sounding notes and highlights their piano keys. | | `low="48" high="84"` | Sets the keyboard's MIDI endpoints to C3 and C6: a three-octave span with both end keys included, for 37 keys. | | `white-key-height="128" black-key-height="80"` | Sets white keys to 128 px tall and black keys to 80 px tall. | | `fit-to-width="true"` | Fits all three octaves to the container width without horizontal scrolling; key heights stay unchanged. | Keep `id="piece"` unique within the document. The `player="#piece"` selectors connect both views to that player, so pausing or seeking updates their display automatically. You do not need to load the score again or synchronize a second player. To use another piece, change the player's `src`; the views follow its replacement score and note state. The staff view provides lightweight notation. For detailed engraving, see [``]().
Add loading and error feedback The bare player publishes readiness and load errors through `.playback`; it does not draw those messages itself. To add the same feedback as the demo, place a status element directly after `` in `index.html`: ```html ``` Add this helper to `src/main.ts` after the registration code. It shows loading or failure messages and hides the status element once the score is ready. ```ts import type {ScorePlayerElement} from '@webmusic/score/play/element'; /** Show loading and failures; keep the status row hidden during normal playback. */ export function mountQuickStartStatus(player: ScorePlayerElement, status: HTMLElement): () => void { let lastState = ''; const write = (message: string): void => { status.hidden = message.length === 0; if (status.textContent !== message) status.textContent = message; }; const message = (error: unknown): string => error instanceof Error ? error.message : String(error); const unsubscribe = player.playback.subscribe((snapshot) => { const state = `${snapshot.sourceRevision}:${snapshot.readiness}:${snapshot.state}`; if (state === lastState) return; lastState = state; if (snapshot.readiness === 'error') { write(`Could not load the score: ${message(snapshot.error)}`); } else if (snapshot.readiness !== 'ready') { write('Loading score…'); } else { write(''); } }); const onError = (event: Event): void => { const {error} = (event as CustomEvent<{error: unknown}>).detail; write(`Playback failed: ${message(error)}`); }; player.addEventListener('webscore:error', onError); return () => { unsubscribe(); player.removeEventListener('webscore:error', onError); }; } ``` Then call it at the end of `src/main.ts`: ```ts const player = document.querySelector('#piece')!; const status = document.querySelector('#player-status')!; let detachStatus = mountQuickStartStatus(player, status); window.addEventListener('pagehide', () => { detachStatus(); player.stop(); }); window.addEventListener('pageshow', (event) => { if (event.persisted) detachStatus = mountQuickStartStatus(player, status); }); ``` The Play click supplies the browser's user activation for sound. A missing or invalid file now produces a visible message. Check the requested URL in the browser's Network panel; replace `src` or reload the page to retry after fixing the file. A failed playback command also appears in the status text. For a framework component, call `detachStatus()` in its unmount cleanup. Removing the player from the DOM releases its owned playback resources; removing either view releases only that view's bindings and rendering resources. Use the [Score setup]() for other formats, registration and integration options.
## Keep the behavior and change the interface - **Use your own buttons:** see the [ScorePlayer engine and its commands](). - **Reuse controls with custom bindings:** see the [Transport presenter](). - **Add analysis:** attach companions through the [shared playback source](). Full references: [Score player]() · [Score view]() · [Pitch view](). To build with a coding agent, see [Agent Toolkit]() for reference selection, task prompts and project instructions. ## Play recorded audio For recorded sound, install `@webmusic/audio` alongside Kernel and UI: ```bash npm install @webmusic/audio @webmusic/kernel @webmusic/ui ``` Download the [Arabesque No. 1 WAV recording](), save it as `public/arabesque.wav`, and add the player to your browser application: ```html ``` Click **Play** to enable audio. This example uses built-in WAV decoding and needs no optional decoder. The player owns its file and playback resources; removing it releases them. Check its error state and the URL if loading fails. A Score player and an Audio player run independently. To coordinate them, use the [Bridge composition](). For custom controls and API + UI composition, see [Audio setup](), [Audio playback](), and the [Audio player reference](). --- # Share one player with views and analysis Source: [](https://koperative-lab.github.io/WebMusic/score/element/play/score-player/) · [Complete Markdown](https://koperative-lab.github.io/WebMusic/agent-context/score/element/play/score-player.md) ## Four ways to drive it The element resolves exactly one source of music, in a fixed order of precedence. The first one present wins and the rest are ignored: | Precedence | Source | What the element owns | | --- | --- | --- | | 1 | `.controller` | Nothing. Chrome only — no score is loaded, and the borrowed [`PlayerController`]() outlives the element. | | 2 | `.rack`, else the first nested ``'s Rack | A transport binding over the borrowed [`Rack`](). Commands reach its members; the seek bar follows the longest built timeline member. An explicit `.rack` wins over the desk. | | 3 | `.score` | The transport and the audio graph, over a `Score` you already parsed. | | 4 | `src` | All of the above, plus the fetch and parse. | `src` is loaded through the sibling [`@webmusic/score/io`]() capability, imported on demand so callers who only ever assign `.score` never pull the parser stack into their bundle. The format is taken from `format`, else the file extension, else sniffed from the bytes. A failed load publishes an error through `.playback`. Native replacement releases the previous owner first, so loading and error snapshots carry no stale Score. Reassigning a source retries. Switching source at runtime is supported: assign a new one and the element tears the previous transport down before mounting the next. Only the latest render survives, so a fast sequence of changes cannot leave a stale player scheduling in the background. Every mode mounts into an element-owned inner host. Authored children, including a nested desk and its declarations, survive mode switches. Removing or replacing the selected desk releases the old composed Rack binding; events from another desk do not replace the selected one. In Rack mode, `currentTime` and `duration` read the same mounted transport that drives the controls. Duration is `0` until a member player is built, normally by `rack.preload()` or the first play. `seekFraction(f)` takes a fraction of the longest built member's duration and seeks each member to that many transport seconds, clamped to its own end. `seek(seconds)` uses that same axis. `seekNominal()` rejects because the Rack has no single nominal score timeline. Member command coordination does not replace their separate Headless clocks with one shared clock. ## Share data and state with companions `.playback` is the stable [ScorePlaybackSource]() for this Element's lifetime. Native mode publishes the score actually loaded by `src` or assigned through `.score`, an immediate playback snapshot and subsequent updates. Controller/Rack modes without a single native Score report `unavailable`. The `.score` property remains the explicit assignment; use the source snapshot when you need the data actually selected for playback. Companions can borrow that data without repeating `src`: ```html ``` Register each tag through its owning `/element` entry. A follower borrows the player and never disposes it. Native replacement releases the previous owner before loading the next. Loading and failure have explicit readiness and no committed Score; failure is not successful empty data. Source revisions change when the committed owner changes, including teardown; callbacks from old owners cannot restore them. The compatibility `.resolvedScore` and `getPlaybackSnapshot()` read the currently mounted native owner. New integrations can use `.playback` to observe explicit readiness and revision changes through the nonvisual contract. ## Ownership and teardown The element is careful about what it disposes, because most of what it plays through belongs to somebody else: - **Borrowed.** `.controller`, `.rack`, `.sound`, `.audioContext` and `.destination` remain caller-owned. A nested desk owns its automatically created Rack. Unmounting the player's Rack transport stops its playback but does not dispose the Rack; borrowed controller mode only releases the UI binding. - **Owned.** Native player engines, mounted controls and resources created by the element are released on teardown. A SoundFont synth/context pair is cached against its template while mounted, then disposed by its owner. Scores are immutable data, not disposable engines. - **Native settings survive a remount.** Stored `rate`, `volume`, `pan` and `loop` values are applied when a native player mounts. They do not reconfigure Rack members. Controller mounting preserves the owner's settings; an explicit `.rate` write delegates to `controller.setRate()`. Native and Rack `play()` await preparation and startup. Borrowed controller mode delegates its command and resolves immediately; the controller's error channel is forwarded as `webscore:error`. --- # Own a player from custom UI Source: [ScorePlayer](https://koperative-lab.github.io/WebMusic/score/headless/play/score-player/) · [Complete Markdown](https://koperative-lab.github.io/WebMusic/agent-context/score/headless/play/score-player.md) ## 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](). --- # Connect a view to playback Source: [](https://koperative-lab.github.io/WebMusic/score/element/view/score-view/) · [Complete Markdown](https://koperative-lab.github.io/WebMusic/agent-context/score/element/view/score-view.md) ## 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. --- # Follow analysis and navigate musical positions Source: [createAnalysisFollower](https://koperative-lab.github.io/WebMusic/score/headless/analyze/analysis-follower/) · [Complete Markdown](https://koperative-lab.github.io/WebMusic/agent-context/score/headless/analyze/analysis-follower.md) ## 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. --- # Compose React state and components Source: [@webmusic/score/react](https://koperative-lab.github.io/WebMusic/score/api/react/) · [Complete Markdown](https://koperative-lab.github.io/WebMusic/agent-context/score/api/react.md) ## Install `npm install @webmusic/score react` — use an available published version or the local workspace; [Install]() has the full setup. React is a peer dependency: ```txt react >= 18 ``` ## Customize With Provider Use `ScoreProvider` directly when you want one shared score/player context and full control over the layout. ```tsx import type { Score } from '@webmusic/score'; import { ScoreProvider } from '@webmusic/score/react'; export function App({ score }: { score: Score }) { return ( ); } ``` Provider output: ```txt ScoreProvider -> ScoreContext -> PlayerContext -> CursorContext ``` ## Customize With Hooks Use hooks inside `ScoreProvider` to build custom controls, readouts, analysis panels, or editor UI. ```tsx import { useCursor, usePlayer, useScore, useScoreAnalysis } from '@webmusic/score/react'; export function CursorReadout() { const score = useScore(); const player = usePlayer(); const cursor = useCursor(); const analysis = useScoreAnalysis({ windowQuarters: 4 }); return ( ); } ``` Hook output: ```txt useScore() -> Score usePlayer() -> Player useCursor() -> { tick, seconds, measure, beat } useScoreAnalysis() -> { key, chords, roman, motifs, issues } useScoreAnalysisAsync() -> { result, pending } ``` For large scores, `useScoreAnalysisAsync` runs the same analysis off the main thread in a Web Worker (see [Analysis in a Worker]()). Instead of blocking the render it returns `{ result, pending }`: `result` is the latest finished analysis (`null` until the first one lands), `pending` is true while the current score is being analyzed. Edits are coalesced latest-wins — rapid successive edits only analyze the newest score — and when `Worker` is unavailable (SSR, tests) the hook falls back to an in-process session. The worker is disposed on unmount. Pass the same options as `useScoreAnalysis`, plus an optional `worker` (instance or factory) for custom bundler setups: ```tsx const { result, pending } = useScoreAnalysisAsync({ windowQuarters: 4 }); if (result) console.log(result.key.tonic, pending ? '(updating…)' : ''); ``` Hooks must be called inside `ScoreProvider`: ```txt useScore must be used inside ScoreProvider usePlayer must be used inside ScoreProvider useCursor must be used inside ScoreProvider ``` ## Customize Individual Components Use the lower-level components when you want to compose your own workspace but still want packaged React wrappers around the view layer. ```tsx import type { Score } from '@webmusic/score'; import { AnalysisSummary, PianoRollView, PlayerControls, ScoreProvider, StaffView, WaterfallView, useCursor, useScore, } from '@webmusic/score/react'; function StatusBar() { const score = useScore(); const cursor = useCursor(); return ( {score.metadata.title} · measure {cursor.measure} · beat {cursor.beat.toFixed(2)} ); } export function CustomWorkspace({ score }: { score: Score }) { return ( ); } ``` Component tree: ```txt ScoreProvider PlayerControls StatusBar AnalysisSummary PianoRollView StaffView WaterfallView ``` --- # Bind an independent presenter Source: [@webmusic/ui](https://koperative-lab.github.io/WebMusic/uikit/api/) · [Complete Markdown](https://koperative-lab.github.io/WebMusic/agent-context/uikit/api.md) ## The shared presenter shape Most entries export three things: a `mount*()` function, the structural binding interface it consumes, and the same stylesheet text the mount installs, as a side-effect-free string constant. ```ts const handle = mountX(host, binding, options); handle.element; // the presenter's root node inside the host handle.update(); // re-read binding.snapshot() and repaint handle.destroy(); // remove nodes, listeners and the subscription ``` For ordinary snapshot-based mounts: - **Host.** `host` is an `HTMLElement` or an open `ShadowRoot`. The mount appends its presenter markup and normally installs its exported style text, then removes what it created on destroy. `stylesheet: false` skips stylesheet installation where supported. - **Structural binding.** The binding is a plain object the caller writes: `snapshot()` returns current state, commands such as `setValue()` or `play()` perform changes and may return `void` or a `Promise`, and `subscribe(notify)` returns an unsubscribe function. The presenter repaints from a fresh snapshot after each command settles. `subscribe` is optional on most bindings; `TransportBinding` requires it. - **One presenter per host, per entry.** Each module keeps a `WeakMap` from host to handle. Mounting the same kind of presenter on a host again destroys the previous handle first, so a host never accumulates duplicate racks or transports. - **Idempotent destroy.** `destroy()` is safe to call repeatedly; cleanup failures are collected and reported instead of thrown. - **`onError`.** Where supported, options accept `onError?: (error: unknown) => void`. Snapshot, command, subscription and cleanup failures handled by that presenter are routed there; entries without this option leave caller actions responsible for their own failures. Check the owning entry before relying on this callback. - **DOM-safe at import.** Every entry evaluates in Node/SSR: importing reads no browser global, and mounting uses `host.ownerDocument` — including `@webmusic/ui/analysis`, whose render helpers create nodes in the caller-supplied root's owner document (`createAnalysisRoot(document?)` is the one place a document can be passed directly). The following entry-specific shapes serve different lifetimes. They are not interchangeable bindings: | Entry | Difference | | --- | --- | | [meter]() | Pulls frame data through `readLevel` or `readSpectrum`; exposes `redraw()` and optional animation. | | [level-analyzer]() | Receives action callbacks and caller-paced `update(state)` rather than a snapshot/subscription binding. | | [note]() | Uses a note-surface snapshot and optional synchronous interaction ports for note holds and release. | | [oscilloscope]() | Uses a snapshot and commands, with optional subscription; the caller supplies each sampled trace and source identity. | | [transient-analyzer]() | Accepts an `HTMLElement` and action callbacks, then caller-paced `update(state)`; it has no mount options, subscription or `onError` callback. | | [analysis]() | Renders caller-owned report output through one-shot helpers; the caller replaces/removes it and updates any retained painter or playhead. | | [stage]() | Uses `render(surface)` for retained DOM or `draw(frame)` for canvas; its surface-slider helper augments an existing element. | | [panel]() | Receives section descriptors rather than a state binding; returns stable slots whose child mounts the caller owns. | Each owning page describes its complete state, cleanup and styling contract. --- # Style a custom music interface Source: [UI Kit](https://koperative-lab.github.io/WebMusic/uikit/) · [Complete Markdown](https://koperative-lab.github.io/WebMusic/agent-context/uikit/index.md) ## Use UI Kit directly Install the npm package in your application: ```bash npm install @webmusic/ui ``` Add a host to your HTML: ```html
``` In a TypeScript module processed by your application's bundler, mount a parameter control against a value your application owns: ```ts import {mountParameterRack} from '@webmusic/ui/parameter'; const host = document.querySelector('#level')!; let level = 50; const handle = mountParameterRack(host, { snapshot: () => ({ parameters: [ {id: 'level', label: 'Level', value: level, min: 0, max: 100, step: 1, unit: '%'}, ], }), setValue: (_id, value) => { level = value; }, }); // When your route or component unmounts: // handle.destroy(); ``` The control displays **Level 50 %** and supports pointer and keyboard input. `snapshot()` supplies the current value and bounds; `setValue()` updates the application state. The presenter reads the snapshot again after that command. The example changes a value only. To control sound, connect the command to an application-owned player, synth or other engine. When something else changes the value, call `handle.update()` or provide the binding's optional `subscribe(notify)` so the view can refresh. Call `handle.destroy()` when removing the view; it releases its DOM, listeners and subscription without disposing your state or engine. See [parameter]() for the complete contract. ## Theme with CSS custom properties Applications theme presenters through inherited `--wm-*` custom properties: ```css :root { --wm-component-background: #fff; --wm-component-border: 1px solid #d8d8d8; --wm-component-radius: 0; --wm-foreground: #171717; --wm-foreground-muted: #666; --wm-accent: #111; --wm-accent-foreground: #fff; --wm-font-family: system-ui, sans-serif; } ``` Changing a custom property does not remount a presenter, reset playback or rebuild an audio graph. No theme provider, registry or JavaScript runtime is required. Presenters expose a shared outer-surface contract. Most default to a neutral card; Harmony, Pitch and a bare Workbench default to an unframed transparent surface. These four inherited tokens customize either form without changing the binding or remounting: | Token | Typical fallback | Controls | | --- | --- | --- | | `--wm-component-background` | `--wm-surface`, then presenter default | Outer background; accepts `transparent`. | | `--wm-component-border` | `1px solid #d8d8d8`, or `0` for an unframed surface | Complete border shorthand, including `0` or `none`. | | `--wm-component-padding` | `.6rem`, or the presenter's existing inset | Space between the outer surface and its content. | | `--wm-component-radius` | `0` | Outer component corner radius. | Use the global names for a coherent application surface, or the corresponding `--wm--surface-background`, `-border`, `-padding`, and `-radius` names for one presenter (for example `--wm-transport-surface-background`). Older component variables such as `--wm-stage-background` and `--wm-meter-background` remain supported for their existing inner viewport or compatibility role. `--wm-surface`, `--wm-border`, and `--wm-control-radius` remain semantic palette fallbacks. `--wm-control-border` is the shared line color for non-primary inner controls; it falls back through `--wm-border` to `#d8d8d8`. Primary actions, active states, and focus rings keep their own stronger tokens instead of inheriting that soft line. The `--wm-component-*` quartet is the direct outer-surface API. Set `--wm-foreground` for text and drawing ink, `--wm-foreground-muted` for secondary text, `--wm-accent` for active fills and `--wm-accent-foreground` for their labels. Surface tokens never intentionally provide selected text colour. In particular, transparent Analyze backgrounds no longer make selected labels transparent; set the explicit accent foreground to customize them. Musical palettes (`--wm-degree-*`, `--wm-progression-tone-*`), inverse surfaces, note-key faces, and loading overlays retain their dedicated tokens so independent visual roles remain legible. Envelope, EQ and LFO graphs pair `--wm-surface-inverse` with `--wm-foreground-on-inverse`; generic surface and foreground values do not recolor their inverse plots. Set both inverse tokens when changing that palette. Use `--wm-font-family` for UI text and `--wm-font-mono` for fixed-width readouts; presenter-specific font overrides retain precedence. For one transparent outer frame, set `--wm-component-background: transparent`. To also clear ordinary inner surfaces, set `--wm-surface: transparent`; an explicitly configured inner or inverse surface still takes precedence. `--wm-border` is a **colour** for inner edges, while `--wm-component-border` is a complete outer-border declaration. When a Workbench or another presenter already owns the outer frame, pass `surface: 'none'` to nested Harmony or Pitch mounts. It suppresses only their outer background, border, padding and radius, on both stylesheet paths; their musical marks, controls and palette remain active. This prevents duplicate borders and translucent backgrounds being painted twice. Workbench's public `frame` part is the painted box; its root remains the unpainted query container. `stylesheet: false` always skips installation, including owned child presenters. Harmony, Pitch, Workbench and Transport retain inline declarations; other presenters expect the caller to supply the exported stylesheet or replacement CSS. Pseudo states, focus rules, native control pseudo elements and container queries require stylesheet rules even when inline geometry is supplied. Private `--wui-*` properties are implementation or data values; new themes should use `--wm-*`. ### Control geometry Shared control tokens establish the default size and radius for row controls such as transport buttons and seek controls. Presenter-specific overrides and the control's function can require different geometry. | Token | Default | Controls | | --- | --- | --- | | `--wm-control-size` | `2rem` | Shared row-control height where the presenter supports it. | | `--wm-control-radius` | `0` | Shared control corner radius; square is the default. | A vertical fader's length sets its travel, a rotary dial retains its circular shape, and note keys preserve their musical layout. Consult the presenter's Styling section for its size overrides and minimum dimensions. A drawing surface is not a control either. An envelope, an EQ curve, an LFO waveform, a piano board and a timeline lane need room, and how much room is an editorial choice, so they share one scale instead of each carrying its own number: | Token | Default | Used by | | --- | --- | --- | | `--wm-surface-sm` | `72px` | LFO waveform, timeline lane. | | `--wm-surface-md` | `144px` | Envelope, EQ curve, piano board. | | `--wm-surface-lg` | `216px` | Available for a taller drawing; no presenter defaults to it. | Set a tier to re-proportion every drawing at once, or give one presenter any length you like through its own `--wm--height` — the tier is only the default behind it. ## Customization depth Customization is progressive rather than all-or-nothing: | Need | Use | | --- | --- | | One coherent application theme | inherited semantic `--wm-*` custom properties | | Component-specific adjustment | `--wm-transport-*`, `--wm-parameter-*`, `--wm-envelope-*`, `--wm-lfo-*`, `--wm-meter-*`, `--wm-stage-*`, `--wm-status-*`, `--wm-minimap-*` properties | | Style internal Shadow DOM nodes | documented `::part(...)` hooks | | Preserve an existing product skin | legacy variables/classes remain compatibility aliases during migration | | Mount a presenter yourself | a `mount*()` function and a structural binding | | Change markup or interaction | compose different presenters, use Headless with your own UI, or customize an Element mount hook | | Change domain behavior/API | subclass the controller/binding hooks, or compose the Headless object directly | CSS custom properties cross open Shadow DOM boundaries. Stable `part` names expose selected internal nodes without treating all private markup as public. Arbitrary markup customization belongs in the subclass or Headless path instead of accumulating a large matrix of Element attributes. Each presenter's **Styling** section owns its tokens, parts and named handle accessors. Use those documented hooks when styling a specific control; private `wui-*__*` selectors and DOM order are implementation details. --- # Load, convert and parse music in a worker Source: [@webmusic/score/io](https://koperative-lab.github.io/WebMusic/score/api/io/) · [Complete Markdown](https://koperative-lab.github.io/WebMusic/agent-context/score/api/io.md) ## Install `npm install @webmusic/score` — use an available published version or the local workspace; [Install]() has the full setup. Every parser and serializer this entry exposes is bundled; nothing here needs an optional peer. See the [Score entry map]() for other capabilities. JSON model persistence belongs to `Score.toJSON()` / `scoreFromJSON()` in [`@webmusic/score`](); JSON is not a format accepted by these file loaders. ## Load and detect formats ```ts import {detectFormat, loadScore, loadScoreFromUrl} from '@webmusic/score/io'; const fromUrl = await loadScoreFromUrl('/scores/etude.mxl'); const fromBytes = await loadScore(buffer); // format is sniffed const fromXml = await loadScore(xml, {format: 'musicxml'}); detectFormat(buffer); // 'midi' | 'mxl' | 'musicxml' | 'abc' ``` URL and Worker operations accept `signal` and `timeoutMs` across download, bounded stream reading, and result activation: ```ts const controller = new AbortController(); const pending = loadScoreFromUrl('/scores/large.mxl', { signal: controller.signal, timeoutMs: 15_000, }); controller.abort(); // pending rejects with AbortError ``` A timeout rejects with `ScoreLoadTimeoutError`. Synchronous parser computation cannot always be pre-empted, but its cancelled result is suppressed. `loadScoreDetailed(input, options)` and `loadScoreFromUrlDetailed(url, options)` return `{score, diagnostics}`. Their score-only counterparts return `Score`. Input is a string, `ArrayBuffer`, or the exact range of a `Uint8Array`; text bytes are decoded as UTF-8. `format` overrides detection. URL loading next uses the extension (ignoring query and fragment), then sniffs bytes. Detection checks MIDI/ZIP magic or a leading XML `<` after whitespace/BOM; remaining text is treated as ABC. It is not validation of the detected document. ## Parse and serialize directly ```ts import { parseABC, parseMIDI, parseMusicXML, parseMXL, serializeABC, serializeMIDI, serializeMusicXML, serializeMXL, } from '@webmusic/score/io'; ``` ### Representation and information loss | Format | Preserved behavior | Limits of the representation | | --- | --- | --- | | MIDI | Format 0/1 with positive quarter-note PPQ; exact source tick positions and performed seconds; tempo/meter streams, track names, channels and programs. Export uses sounding pitches, merges contiguous ties and projects performed seconds at the requested PPQ. | SMPTE time division and format 2 independent sequences reject. MIDI has no authored notation grid; import reports ignored key signatures, controls and pitch bends. Export omits rests and grace notes whose steal time has not been realized. Written spelling, ties and transposition are realized as performance events, not preserved as notation. | | MusicXML | Score-partwise document order, backup/forward, voices, chords, note/rest durations, written pitches, ties, staff assignments and part transposition. Written bases/dots/tuplet ratios and endpoints (including hidden numbers), numbered beams, stems, hidden notation, numbered slurs, tie placement, common articulations and rest positions are retained. Per-part/staff clefs include changes inside bars; words, rehearsal, dynamics, numeric metronome, pedal and wedge directions retain exact offsets. Left/right bar styles use the master grid. Export moves the document cursor for overlapping onsets and splits notes crossing bars into tied fragments. | Score-timewise rejects. The first part supplies the measure/tempo/meter/barline grid; differing secondary measures produce diagnostics. Mid-measure key/meter declarations and changing part transposition cannot be fully represented. Grace notes retain zero timing and export with an eighth-note type. Page credits, full layout, lyrics, uncommon articulations, articulation placement, unsupported direction families and performed overlays are not lossless. | | MXL | Bounded ZIP extraction of the container manifest and selected MusicXML entry; shares the MusicXML semantics above. | Reads the first manifest rootfile, with a filename fallback when no path is declared. Additional scores, images and archive metadata are not retained. Export writes a single score and manifest. | | ABC | Compact single-stream pitch/rest tokens, octave marks, rational length suffixes, numeric meter and explicit accidentals. Accidentals propagate by pitch letter across octaves until a barline or another accidental; export writes explicit naturals. | Key/tempo/voice declarations, repeat execution, chords, tuplets, grace groups, overlays, lyrics and decorations are not fully interpreted. Detailed parsing reports supported families of loss. Export uses only the first part, inserts gaps as rests, and reports skipped overlaps/zero-duration notes. | MIDI export accepts `{ppq?: number}` (default `480`, integer `1…32767`). A non-quarter metronome marking is converted to quarter BPM before encoding the tempo event. Sounding pitches outside the MIDI data range `0…127` reject rather than wrap. Tick rounding and MIDI's integer microsecond tempo remain format precision limits. MusicXML export accepts `{ppq?: number}` (default `480`, positive safe integer) as its divisions per quarter. MXL additionally accepts `scorePath` (default `score.musicxml`); an empty path or `META-INF/container.xml` rejects because it would replace or hide the score manifest. Written ``, `` and `` values are checked against the exact rational ``. If they disagree, import keeps the encoded musical timing, derives a compatible duration base and reports `musicxml-notation-duration-mismatch`. Export uses the written base for `` and retains tuplet ratios and authored endpoints. Programmatic `tupletId` groups without endpoints receive start/stop marks on export. Tuplet IDs represent group membership, not stable MusicXML identifiers. Per-part clefs and supported directions use the [timed notation model](). The importer applies signed `` values on the current document cursor and retains source staff, placement and numeric engraving offsets. Unsupported direction families (for example octave shifts, dashes and metric modulations) report `musicxml-direction-unsupported`; arbitrary XML child structures and typography are not a lossless schema round-trip. A metronome direction alone is visual metadata; `` supplies playback tempo. Sound offsets use the active divisions and may be negative; a sound's own offset overrides its enclosing direction's offset. Export writes playback-only tempo events as measure-level `` elements, preserving their offsets without adding visual metronome markings or other directions. Export still rounds onsets and durations to the chosen PPQ: use a resolution divisible by the score's rhythmic denominators for an exact round-trip (`480` handles ordinary triplets; septuplets may require, for example, `3360`). When export splits a note across bars, its fragments receive new written durations and ties; the original beam/tuplet marks are not copied onto those different durations. Slur endpoints and articulations stay on the original onset fragment. ABC's omitted `L:` follows its meter: `1/8` for meters at least `3/4`, otherwise `1/16`. Earlier releases used `1/4` unconditionally; add `L:1/4` when that old timing was intended. The serializer always writes `L:1/4` explicitly. ABC key signatures and accidental-propagation directives remain outside the supported subset; the default propagation rule applies. Use `parseMIDIDetailed`, `parseMusicXMLDetailed`, `parseMXLDetailed`, or `parseABCDetailed` to retain recoverable diagnostics. The result is `{score, diagnostics}`; `serializeABCDetailed` returns `{data, diagnostics}`. Diagnostics contain `code`, `severity`, `format`, `message`, and an optional `location` (`partId`, `measureNumber`). An empty list does not certify a lossless conversion. Syntax, unsupported timing modes and resource-limit failures throw or reject; ordinary parser failures are not all instances of one error class. ### Resource limits The defaults are exported as frozen constants. Resolution helpers validate overrides without parsing. Integer limits must be positive safe integers; `maxDurationQuarters` is positive and finite. Direct format parsers accept their format's partial limits as a second argument. Load options pass them under `midi`, `musicxml`, `mxl`, or `abc`, with the top-level input cap still applying. | Options / default constant | Defaults | | --- | --- | | `LoadScoreLimits` / `DEFAULT_LOAD_SCORE_LIMITS` | `maxInputBytes: 32 MiB` | | `MIDIParseLimits` / `DEFAULT_MIDI_PARSE_LIMITS` | `maxInputBytes: 32 MiB`, `maxTracks: 512`, `maxEvents: 1,000,000` | | `MusicXMLParseOptions` | `maxInputBytes: 32 MiB`, `maxElements: 250,000` (opening elements) | | `MXLParseLimits` / `DEFAULT_MXL_PARSE_LIMITS` | `maxArchiveBytes: 32 MiB`, `maxEntryBytes: 64 MiB`, `maxTotalExtractedBytes: 64 MiB`, `maxEntries: 4,096`, `maxPathLength: 1,024` characters | | `ABCParseLimits` / `DEFAULT_ABC_PARSE_LIMITS` | `maxInputBytes: 1,000,000`, `maxTokens: 100,000`, `maxMeasures: 4,096`, `maxDurationQuarters: 256` | ZIP limits are inspected before the selected entries inflate; XML disables custom DTD entity expansion and restores only predefined/numeric references. These are bounded resource checks, not complete format validators or latency guarantees. A fetch implementation without a readable stream is checked after its `arrayBuffer()` allocation; native streamed responses are counted and cancelled at the byte limit. ## Parse in a Worker ```ts import { ParserWorkerRemoteError, createParserWorker, } from '@webmusic/score/io/worker-client'; const parser = createParserWorker(); const score = await parser.parse(buffer); parser.dispose(); ``` `parse()` and `parseDetailed()` keep binary ownership with the caller, so supplied memory is never mutated or detached. After checking `maxInputBytes`, the client snapshots an `ArrayBuffer` or the exact range of a `Uint8Array` once at call time. A real Worker receives that private snapshot by transfer; the fallback parses the same snapshot. URL loading can transfer its internally allocated response buffer directly. Views backed by `SharedArrayBuffer` are copied to a private transferable buffer rather than attempting to transfer shared memory. If you pass a Worker instance or return one from a factory, its lifecycle is transferred to this parser client; it is not borrowed and must not be shared. `parser.dispose()` and terminal protocol failures terminate the supplied Worker. Use a dedicated Worker for each parser client. The self-registering Worker entry is `@webmusic/score/io/worker`. In environments without Web Workers, the client transparently falls back to in-process parsing. Worker protocol v1 adds structured errors and a best-effort per-request cancel message; an explicit incompatible version retires the Worker rather than leaving a request pending. Both v1 headers and structured failure details are mandatory; only legacy v0 may omit both headers. Partial headers, malformed diagnostics, and unknown response ids are terminal instead of being ignored. Known input-limit, ABC/MIDI/MusicXML/MXL format-limit, abort, and timeout failures preserve the same exported error classes used by the in-process path. Other remote failures reject with `ParserWorkerRemoteError`, which exposes stable `code`, `operation`, and `retryable` fields. A malformed diagnostic location also retires the Worker and rejects pending requests. If a custom factory disposes the client before returning its Worker, that late Worker is terminated and receives no request. A cancelled request cannot activate a later result; synchronous parser work may still finish in its Worker. `parse` / `parseDetailed` / `loadFromUrl` / `loadFromUrlDetailed` accept the same limits, signal and timeout options. `dispose()` is idempotent and rejects active work. The stateless capability root does not expose Worker state. Use `@webmusic/score/io/worker-client` for the client, `/worker-protocol` for custom hosts, and `/worker` for the self-registering runtime. --- # Play a decoded audio clip from custom UI Source: [AudioClipPlayer](https://koperative-lab.github.io/WebMusic/audio/headless/play/audio-clip-player/) · [Complete Markdown](https://koperative-lab.github.io/WebMusic/agent-context/audio/headless/play/audio-clip-player.md) ## 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(); ``` ## 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. --- # Attach an audio waveform and choose its interaction Source: [](https://koperative-lab.github.io/WebMusic/audio/element/view/audio-view/) · [Complete Markdown](https://koperative-lab.github.io/WebMusic/agent-context/audio/element/view/audio-view.md) ## Three ways in ```html ``` `peaks-src` is the path that draws a long file with **no decode at all**: no `AudioContext`, no samples and no `@webmusic/audio/play` import. Its duration comes from the pyramid, since there is no clip to ask. A failed request surfaces as `webaudio:error`; while either loader is in flight the element carries `aria-busy`. ## Dragging and seeking `drag-mode` chooses how a pointer drag behaves on a waveform or spectrogram. The element default is `seek`; these demos explicitly select `scrub`. | `drag-mode` | Drag behavior | Requires | | --- | --- | --- | | `seek` (default) | Seek continuously to the absolute time under the pointer. | `interactive` | | `scrub` | Keep the playhead centered and move the track: left advances, right reverses. A supported player pauses normal playback and sounds the movement at gesture speed. Release coasts in position and sound, then restores its previous playing/paused state. | `interactive`; decoded PCM and a scratch-capable player for sound | | `pan` | Move the visible window without changing the playback position. Left reveals later audio; right reveals earlier audio. | `scrollable`; works without `interactive` or a player | | `none` | No drag action. Click and keyboard seeking remain available. | `interactive` for seeking | ```html ``` With `interactive`, `seek`, `scrub` and `none` retain click and keyboard seeking. In `pan`, pointer and keyboard input operate only the viewport: a click does not seek. `annotate` takes priority over every drag mode; a meter ignores `drag-mode`. An absent or unrecognized value falls back to `seek`. The matching JavaScript property is `view.dragMode`; its `AudioViewDragMode` union is exported from `@webmusic/audio/view/element`. With annotation off, `pan` suspends auto-follow for the entire mode so playback does not pull the window away from the place being inspected. `scrub` uses a fixed center playhead independently of `follow` and `scrollable`, even at zero or the clip's end. Blank space fills the area outside the clip. Hit-testing that padding clamps to zero/end; `visibleRange()` reports only the real clip portion. Native panning and `panTo()` are ignored while centered; choose `pan` to browse another window. ### Audible scratch support and fallback For a bound player exposing `beginScratch()`, pointer press borrows a scratch session. The native decoded player suspends normal playback and maintains continuous forward/reverse audio through bounded PCM windows on its existing volume, pan and effect route. Left means forward; right means reverse; speed and pitch follow movement. Holding still brakes scratch output and does not keep normal playback running. This is sample-speed scratching, not pitch-preserving time stretching. Normal release applies the final coordinate, then coasts for about 280 ms on the player's AudioContext clock. Position and sound transition together from the signed release speed toward the configured playback rate for a previously playing source, or toward zero for a paused source. Ordinary playback resumes only after that transition; a paused source settles to rest. A stationary click changes position silently and restores directly. Releasing after motion has stopped does not reuse an old fling velocity. The View retains the session through release inertia so another press can take over while preserving the original playing intent. Cancellation (pointer cancel, lost capture, blur, mode/zoom/source change or removal) stops it without resuming, including after pointer release. A later transport command or replacement source invalidates older session work so late readiness cannot restart it. Rendering follows the player's accepted position throughout; it has no separate momentum animation or scratch clock. The capability is optional. A legacy player without `beginScratch()`, or a player that returns no session because decoded PCM is unavailable, keeps position-only scrubbing. It does not promise scratch sound, release inertia or playback-state restoration; the unsupported request itself has no player side effects. `pan` never needs this capability. A failed supported session is reported through `webaudio:error`. See [AudioClipPlayer scratch sessions]() for the public nonvisual API; the existing `scrub(deltaSeconds)` method remains a relative seek helper. --- # Coordinate Score and Audio from one session Source: [Playback composition](https://koperative-lab.github.io/WebMusic/bridge/composition/) · [Complete Markdown](https://koperative-lab.github.io/WebMusic/agent-context/bridge/composition.md) ## Setup in your application Install the five packages used by this composition: ```bash npm install @webmusic/score @webmusic/audio @webmusic/bridge @webmusic/kernel @webmusic/ui ``` Use published versions that are available, or this repository's local workspaces. The implementation below is compiled by the documentation site against its workspace packages. Browser audio requires a user gesture and Web Audio support; this example also requires OfflineAudioContext. Unsupported environments and loading/command failures appear in the status output. The composition is ordinary application code: `ScoreBuilder` produces the score, `renderScoreToClip` produces its audio, `createSyncedPlayback` creates the paired players, and a UI transport dispatches group commands. The application closes its context when the session is disposed. Bridge releases the players it creates; removing a view releases only that view's borrowed connection and own resources. ## Three integration choices in one session | Choice | Used here | State and ownership | | -------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------- | | Web Components | `score-view` and `score-chord-analysis` | Borrow a public playback source; no repeated `src` and no new player. | | Headless | `ScorePlayer`, `AudioClipPlayer`, `ScoreAudioSync` | Own playback behavior and participate in the selected group authority. | | API + UI | Offline rendering, peaks, waveform renderer and `mountTransport` | Produce data or display state supplied by the application. | A player selector is a browser convenience around the same nonvisual source protocol. The owner host in this demo exposes a public `playback` value. Its snapshot and subscription delegate to the native Score player, while its seek command delegates to the **group**, so an interactive follower cannot move only one participant: ```ts import type { ScorePlaybackSource } from "@webmusic/score"; import { createSyncedPlayback } from "@webmusic/bridge"; export function groupPlaybackSource( pair: ReturnType, ): ScorePlaybackSource { return { snapshot: () => pair.scorePlayer.playback.snapshot(), subscribe: (listener) => pair.scorePlayer.playback.subscribe(listener), seekNominal: async (position) => { const result = await pair.sync.dispatch({ type: "seek", position }); if (result.status !== "committed") throw new Error("Seek superseded."); }, }; } ``` New companions receive their initial data and state through that protocol. Application drawing uses animation frames to read current positions; it does not advance musical time. Command completion comes from `dispatch`, not from an optimistic button label. The last six command results are bounded diagnostics, not an unbounded event history. ## Independent loop lengths A group loop repeats the master's timeline. A local native loop repeats one participant's content while the master keeps advancing. The following separate session uses a continuous application-owned master and two native Audio buffer loops: one second and one-and-a-half seconds. Both derive timing from the same AudioContext; each has its own local phase. [Interactive example on the documentation page](https://koperative-lab.github.io/WebMusic/bridge/composition/) Click **Play loops**, then **Pause** and **Seek master to 4.25 s**. The expected local positions are **0.25 s** for the one-second loop and **1.25 s** for the one-and-a-half-second loop. At **2×**, the content positions retain those meanings while the loops run faster. The progress bars read actual player positions; they do not schedule the sound. The implementation configures each `AudioClipPlayer` with its native `loop: {start, end}` option and registers the same local bounds as `TransportGroup.addFollower(..., {loop: {startSeconds, endSeconds}})` metadata. The group maps joins, seeks and drift checks into each local phase. Metadata alone does not turn a player into a native looping engine. This uses the Kernel group directly with structural adapters. The default `createSyncedPlayback` factory continues to reject clip-side loop overrides; its group-loop option has a different responsibility. The demo does not claim nonlinear tempo warping, time stretching that preserves pitch, or a shared writable clock instance.