Skip to content

Search documentation

Enter a title, API, or topic.

    ↑ ↓ NavigateEnter Open resultEsc Close

    @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

    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.

    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.

    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.

    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 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.

    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 → output
    meter.output!.connect(context.destination);
    const {level, peak, peakHold} = meter.readLevel(); // call it from your own frame loop
    meter.configure({fftSize: 2048, smoothingTimeConstant: 0.5}); // ports stay connected

    configure(options?: AnalyserMeterConfiguration) accepts the tuning fields of AnalyserMeterOptions (fftSize, smoothingTimeConstant, levelScale, peakDecay, minimumBars), excluding source ownership. Omitted fields retain their current values. Owned FFT size must be a power of two from 32 to 32768, and smoothing must be finite in 0–1; a rejected candidate leaves previous settings and ports intact. Borrowed analyser settings are never assigned. The display scalars retain constructor normalization. Configuration after dispose() throws.

    For compatibility, live peakHold is max(level, previousHold - peakDecay): it holds scaled RMS with an absolute decrement per read. peak is a separate scaled sample peak. These byte-analyser display measurements are not calibrated loudness/true-peak measurements or decay measured in seconds.

    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.

    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 worker

    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.

    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.

    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.

    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.