@webmusic/kernel
Every entry is import-safe in Node, workers, and SSR. The root entry re-exports all of them; the subpaths exist so applications and bundlers can take exactly what they use.
| Entry | Contract |
|---|---|
/events |
Hardened typed EventEmitter |
/element |
WebMusicElement connected-lifetime cleanup plus SSR-safe attribute and registration helpers |
/worker |
WorkerLike + RequestTracker correlation plumbing |
/audio-context |
SSR-safe AudioContext construction |
/player |
PlayerLike playback control contract |
/effect |
Effect/EffectNodes over BaseAudioContext |
/meter |
Shared analyser meter contract and implementation |
/transport |
TransportClock affine anchor math + TimelineMapping |
/tick |
Throttling-resistant tick delivery for lookahead schedulers |
/sync |
Transport grouping, timeline mapping and drift coordination |
events — @webmusic/kernel/events
Section titled “events — @webmusic/kernel/events”EventEmitter<TEvents> is a typed emitter with two delivery modes: emit
propagates listener throws to the caller, while emitSafely snapshots the
listener list, keeps delivering after a faulty observer, and reports both
synchronous throws and async rejections through a failure callback.
import {EventEmitter} from '@webmusic/kernel/events';
const events = new EventEmitter<{tick: {beat: number}}>();const off = events.on('tick', ({beat}) => console.log(beat));events.emit('tick', {beat: 1});off();Both families re-export this implementation as their public EventEmitter.
element — @webmusic/kernel/element
Section titled “element — @webmusic/kernel/element”WebMusicElement is a minimal custom-element base owning one concern:
connected-lifetime cleanup. onMount() runs per connection and may return a
cleanup; own() registers additional cleanups that drain in LIFO order on
disconnect, with rollback if mounting fails partway. It owns no Shadow DOM,
no profile, no presenter, and no domain state.
import {WebMusicElement, defineOnce} from '@webmusic/kernel/element';
class MyBadge extends WebMusicElement { protected override onMount() { const timer = setInterval(() => this.refresh(), 1000); return () => clearInterval(timer); } refresh() {}}defineOnce('my-badge', MyBadge);Also here: upgradeProperty/upgradeProperties for the
set-before-upgrade pitfall, numAttr/boolAttr/cssSizeAttr attribute
readers, and defineOnce idempotent registration. Importing this entry
registers nothing and is SSR-safe; subclassing works in Node.
worker — @webmusic/kernel/worker
Section titled “worker — @webmusic/kernel/worker”WorkerLike is the minimal structural surface a worker must expose
(postMessage with transfer, terminate, message and error listeners), so
clients can accept a real Worker, a shim, or a test double.
createRequestTracker<T>() correlates request/response pairs across the
message channel, with timeout, abort, and dispose paths that always settle
pending requests.
audio-context — @webmusic/kernel/audio-context
Section titled “audio-context — @webmusic/kernel/audio-context”createWebAudioContext(missingMessage, options?) constructs an
AudioContext where available and throws the caller’s message where not
(SSR, Node), keeping AudioContext construction out of import-time paths.
getAudioContextConstructor() returns the constructor or undefined for
feature-testing.
player — @webmusic/kernel/player
Section titled “player — @webmusic/kernel/player”PlayerLike is the lowest-common-denominator playback control contract both
families’ players satisfy:
interface PlayerLike { play(): Promise<void> | void; pause(): void; stop(): void; seek(seconds: number): void; setRate?(rate: number): void; setVolume?(volume: number): void; readonly seconds?: number; readonly duration?: number; readonly playing?: boolean; on(event: string, listener: (data: unknown) => void): () => void; dispose?(): void;}The optional members are deliberate: the base contract is the lowest common denominator both families can always promise. The named capability tiers and their guards are the supported feature-testing protocol:
| Capability interface | Adds | Guard |
|---|---|---|
TimedPlayerLike |
seconds, duration |
isTimedPlayer() |
StatefulPlayerLike |
playing |
isStatefulPlayer() |
RateControlledPlayerLike |
setRate() |
isRateControlledPlayer() |
VolumeControlledPlayerLike |
setVolume() |
isVolumeControlledPlayer() |
DisposablePlayerLike |
dispose() |
isDisposablePlayer() |
A generic consumer narrows once with a guard and gets the non-optional
members; a failed guard means the capability is absent, never an error.
Implementers advertise a tier by making its members non-optional. Both
families pin their players against the tiers they advertise at the type
level (ScorePlayer/TonePlayer: timed, rate-controlled, disposable;
AudioClipPlayer: all five), so drift fails typecheck.
import {isTimedPlayer, type PlayerLike} from '@webmusic/kernel/player';
function renderProgress(player: PlayerLike): number | undefined { return isTimedPlayer(player) ? player.seconds / player.duration : undefined;}effect — @webmusic/kernel/effect
Section titled “effect — @webmusic/kernel/effect”Effect describes an audio post-processing unit over BaseAudioContext:
createAudioNodes(context) returns an EffectNodes value — an
input/output node pair, optionally live params (AudioParams for
real-time control) and a dispose. An optional label lets UIs caption
controls. Both families’ effect chains and the score synth panel speak this
contract.
meter — @webmusic/kernel/meter
Section titled “meter — @webmusic/kernel/meter”AnalyserMeter owns the transparent input → analyser → output tap both
families’ level meters read, plus the byte-domain arithmetic over it: RMS,
scaled level, instantaneous peak and a decaying peakHold. It decides
nothing about when a frame is read — a presenter, canvas renderer or
framework drives that — and it holds no score, clip or DOM.
aggregateSpectrumBars() is the same maths for the frequency domain, folding
FFT bins into a bar count. Both existed once per family and had begun to
drift; only the mechanism moved here, while genuinely editorial differences
(minimum bar count, the frame shape each family publishes) stayed with the
families.
import {createAnalyserMeter} from '@webmusic/kernel/meter';
const meter = createAnalyserMeter({context, levelScale: 1.4, peakDecay: 0.02});sourceNode.connect(meter.input!); // owned tap: input → analyser → outputmeter.output!.connect(context.destination);const {level, peak, peakHold} = meter.readLevel(); // call it from your own frame loopmeter.configure({fftSize: 2048, smoothingTimeConstant: 0.5}); // ports stay connectedconfigure(options?: AnalyserMeterConfiguration) accepts the tuning fields of
AnalyserMeterOptions (fftSize, smoothingTimeConstant, levelScale,
peakDecay, minimumBars), excluding source ownership. Omitted fields retain
their current values. Owned FFT size must be a power of two from 32 to 32768,
and smoothing must be finite in 0–1; a rejected candidate leaves previous
settings and ports intact. Borrowed analyser settings are never assigned.
The display scalars retain constructor normalization. Configuration after
dispose() throws.
For compatibility, live peakHold is max(level, previousHold - peakDecay): it
holds scaled RMS with an absolute decrement per read. peak is a separate
scaled sample peak. These byte-analyser display measurements are not calibrated
loudness/true-peak measurements or decay measured in seconds.
transport — @webmusic/kernel/transport
Section titled “transport — @webmusic/kernel/transport”TransportClock is pure affine anchor math over an injectable clock: start
/ startAt / pause / seekTo / setRate update the anchor, and
positionAt(t) / timeAt(position) convert between host time and timeline
position without accumulating drift. An armed-start hold covers pre-roll
(holding), and TransportClockReader is the read-only view engines expose
to consumers. Both families’ schedulers anchor on it, and the
bridge reads the two clocks for skew-free synchronization.
TimelineMapping (positionToSeconds/secondsToPosition) is the structural
contract for musical-position-to-seconds mappings; score’s TimeMap and
audio’s BeatGrid both satisfy it.
tick — @webmusic/kernel/tick
Section titled “tick — @webmusic/kernel/tick”createTickSource(options) delivers ticks on a cadence that survives
background-tab timer throttling: an inline-Blob interval worker where the
environment allows it (with the Blob URL revoked on both success and CSP
failure), falling back to a main-thread interval otherwise. Lookahead
schedulers in both families and the bridge run on it.
import {createTickSource} from '@webmusic/kernel/tick';
const ticks = createTickSource({intervalMs: 25});ticks.start(() => schedulerPass());// later: ticks.stop() to pause, ticks.dispose() to release the workersync — @webmusic/kernel/sync
Section titled “sync — @webmusic/kernel/sync”TransportGroup drives one master transport and any number of followers on a
shared lead-in, checking drift on a cadence and re-seeking a follower that has
slipped past tolerance. SyncMasterTransport and SyncFollowerTransport are
the structural ports it accepts, so anything exposing that surface can join —
which is how the bridge synchronizes a Score player against an
Audio clip without either package importing the other. Commands are
serialized, so a late async settlement cannot reorder state.
Session commands and observation
Section titled “Session commands and observation”The group is one command authority on the master’s stable position axis.
dispatch() uses the same participant engines as the existing controls; it
creates no second player or writable clock. Commands should go through the group
while those participants form a session. Direct engine changes cannot receive a
group revision until observed reconciliation detects them.
| Member | Contract |
|---|---|
dispatch(command: TransportCommand) |
Returns Promise<TransportCommit>. Play, seek and rate wait for participant preparation/re-entry; pause/stop take effect immediately and await late-start reconciliation. Invalid commands throw before changing revision. Current-operation failure rejects, publishes error, and attempts to pause participants after partial application. Cleanup continues if an adapter throws; a superseded failure cannot override a newer command. |
snapshot |
TransportSnapshot read at one reference instant: revision, referenceTime, stable position, rate, paused, pre-roll holding, loop, pending, disposed. This is a frozen value, not a ticking event source. |
subscribe(listener) |
Registers before synchronously delivering {type: 'snapshot', snapshot}; returns an idempotent unsubscribe function. An initial listener failure removes its subscription; subsequent listener failures are contained. Detaching leaves playback alive. |
TransportCommand |
{type: 'play'}, {type: 'pause'}, {type: 'stop'}, {type: 'seek', position}, {type: 'rate', rate}, or {type: 'loop', loop}. Positions/bounds use the master’s content axis; rate is finite and positive. loop is {startSeconds, endSeconds} or null; bounds must be finite, nonnegative and ordered. |
TransportCommit |
{revision, command, status, snapshot}. status is committed or superseded: a newer command, background reconciliation or disposal prevents old work claiming the current authority. Settlement describes the adapters’ work, not proof of audible output. |
TransportEvent |
Initial/disposal snapshot; invalidate before a command with its revision/command/snapshot; commit with its result/snapshot; error with revision/command/error/snapshot; background reconcile with operation, invalidate or settled phase and snapshot; operation-error with operation/error/snapshot. |
dispatch({type: 'rate', rate}) checks accepted rates where readers expose them;
transports without observable rate state cannot prove they honored a request.
A master that remains paused after a running seek is a rejected dispatch, even
when its adapter resolves instead of throwing. Older play, pause, stop,
seek, setRate and setLoop methods retain their signatures and compatibility
behavior, including best-effort optional rate matching. Their changes also pass
through the observation surface; use dispatch when command failure must be
awaited. Background errors are visible through both operation-error events and
the optional onOperationError construction callback.
Use snapshot.position or clock.positionAt(referenceTime) for moving display
positions. Subscription events describe state transitions rather than animation
frames. Invalidation precedes engine commands: engines cancel and re-arm through
their existing methods. It is not a callback enabling unrelated engines to share
a writable TransportClock. Disposal publishes a final snapshot, removes
observers and invokes registered optional participant disposers; its snapshot
retains the last readable position and is not a continuing playback observation.
Participant-local time and native loops
Section titled “Participant-local time and native loops”addFollower(transport, options?) accepts FollowerOptions:
| Option | Default | Meaning |
|---|---|---|
offsetSeconds |
0 |
Finite nonnegative local position at master zero. |
positionScale |
1 |
Finite positive scale; local position is offset + masterPosition × scale, local rate is masterRate × scale. A non-unit scale requires setRate. |
loop |
absent | {startSeconds, endSeconds} in the follower’s local seconds, for a participant already configured to loop natively. This option maps phase; it does not enable a loop on the transport. |
Before the local loop end, intro positions pass through. At/past the end, local
positions fold into [startSeconds, endSeconds). Scheduled joins and seeks use
this mapped phase; drift reads normalize wrapped or unwrapped clocks and compare
shortest circular phase error, converted back to master units for the tolerance.
Several followers may have different periods while the master remains continuous.
All mappings and loops are copied on registration; remove/re-add to change them.
removeFollower leaves that participant’s current playback/resources with its
caller, while group disposal invokes the disposers of remaining members.
These mappings support affine scaling and native-loop phase. They do not consume
arbitrary nonlinear TimelineMapping, implement audio warping, or make tick-watched
group loops sample accurate. A native-loop adapter must expose consistent
local seconds, rate and scheduling behavior; incorrect metadata can produce
incorrect alignment even when its shape is valid.
MirrorClockMaster adapts a transport that has no readable clock into the
master role: it dead-reckons a mirror TransportClock across the command
surface and reconciles it against the transport’s real position at drift checks
or periodic lifecycle samples when drift checks are disabled. Once forward
motion has established an observed position step, a long enough stationary
interval can report stalled; before any observed motion, repeated positions
alone are inconclusive. A moving transport that diverged re-anchors the mirror.
This is the one home of the sampled fallback
the bridge used to interleave through its clocked path.
Element target discovery
Section titled “Element target discovery”observeElementTarget(host, selector, listener) is exported from
@webmusic/kernel/element. It resolves one unique target within the host’s current
Document or ShadowRoot and calls listener(target, state) immediately. The exported
ElementTargetState is ready, missing, invalid or ambiguous; unsuccessful
resolution supplies undefined, never an arbitrary matching element.
Discovery follows insertion, removal, relevant attribute changes, replacement and
late custom-element definition while attached. A target changing a non-reflected
source property can dispatch a bubbling webmusic:sourcechange event to request a
fresh connection. The helper contains no musical models or playback policy.
The returned idempotent cleanup stops observation and ignores pending upgrade callbacks. Call it on host disconnection, selector changes or moving the host to another root; create a new observation after reconnecting. An initial listener exception releases the observer before being rethrown. This helper never disposes the selected target or grants authority to its commands.