Skip to content

Search documentation

Enter a title, API, or topic.

    ↑ ↓ NavigateEnter Open resultEsc Close

    @webmusic/score/play

    @webmusic/score/play is the playback capability of the @webmusic/score package. It owns the playback engines, audio graph, interaction drivers and playback UI. MIDI, MusicXML, MXL and ABC import/export live exclusively in @webmusic/score/io.

    This page is the reference for that surface — the push (ScorePlayer) and pull (InteractivePlayer) engines, the Sound timbre wrapper, the Effect chain, the Rack ensemble, DOM-free LfoController, offline WAV render, live recording back to a Score, and clock / Web MIDI input drivers — with linked object references and editable examples for offline rendering and file input. For the same objects introduced through live running demos, see Headless Components › Play; for the drop-in custom elements — <score-player>, <score-rack-control>, <score-note-input>, <score-recorder>, <score-synth-panel> — see Web Component › Play.

    It does not render notation, piano rolls, analysis, or framework views — those live in @webmusic/score/view, @webmusic/score/analyze, and @webmusic/score/react.

    npm install @webmusic/score — use an available published version or the local workspace; Install has the full setup.

    The root model, I/O, and playback capabilities ship in that one package. tone and spessasynth_lib are optional peers used by TonePlayer and SF2 playback.

    The @webmusic/score/play root is the stateless playback API. The stateful, DOM-free Headless Components — engines, Sound, Rack, synths and Effect — live on /headless; browser transport adapters live on /drivers; styled custom elements live on /element. The root therefore tree-shakes free of both engine and DOM code.

    @webmusic/score/play/headless is also the stable public boundary for audio playback. Internally, ScorePlayer is a thin API and audio-graph coordinator over a lazy immutable score timeline and a rolling clock scheduler. InteractivePlayer coordinates an immutable beat-source index, a lazy per-voice mixer, and an independent performance recorder. Sound is likewise a lifecycle facade over isolated oscillator, FM, wavetable, noise, Web MIDI, SFZ sampler, exact-MIDI sampler, Tone adapter, and optional SF2 modules. Consumers keep one unchanged import path while those responsibilities remain independently testable.


    A player is one instrument, composed of three pluggable layers:

    <player · score · sound · effect>
    │ │ └ Effect — post-processing (reverb, delay, filter…)
    │ └ Sound — timbre (oscillator, samples, .sfz, .sf2, custom)
    └ Score — the notes (from a file or built in code)

    For many instruments, give each its own player and group them in a Rack. Two engines sit at opposite ends, chosen by who owns time:

    You want Use
    Autonomous timeline playback (pure Web Audio) ScorePlayer (push)
    External-signal / beat-at-a-time interaction, live score swap InteractivePlayer (pull)
    Many instruments, each its own score/sound/effect, played together Rack
    A progress-bar / scrub / rate widget ScorePlayer’s transport surface + the bind* drivers

    The examples below run in an editable Sandpack environment. The preview is disabled by default and must be enabled per embed after its package dependencies resolve. @webmusic/score/play is an import subpath of the @webmusic/score package; publication alone does not toggle the preview.

    Every stateful object this capability publishes lives on @webmusic/score/play/headless and is documented with its primary workflow under Headless › Play: the push and pull engines, Sound, Effect, Rack, Metronome, the ready-made transports, PlayerController, the synth backends — LfoController, EqController and the meter controller among them — and the Tone and Web MIDI adapters.

    SynthPanelAudioGraph is the reusable effects/EQ graph coordinator behind <score-synth-panel>. It retains same-context routing ports across successful DSP replacements and keeps the old usable chain after a failed candidate build. Its Headless reference owns the options, dormant-port behavior and cleanup contract; the Element reference owns section controls and asynchronous sound-parameter feedback.

    This page owns stateless functions and browser transport adapters. Headless objects and their supporting helpers link to the workflow that owns their construction and lifecycle contracts.

    parseSfz(text, options?) flattens supported <global>, <group> and <region> opcodes into SfzRegion[]. parseSfzKey(value) accepts numeric keys and note names (c4 is MIDI 60); an unrecognized spelling returns undefined. This is a limited opcode parser, not a complete SFZ interpreter.

    resolveSfzZones(regions, sfzUrl, options?) resolves sample paths and produces SampleZone[]. Missing lokey, hikey and pitch_keycenter independently default to 0, 127 and 60. Setting a range boundary does not change the root pitch; the explicit key shorthand sets all three values. These defaults match the SFZ definitions for key ranges, the upper boundary and sample root.

    SfzParseOptions.maxInputCharacters defaults to 1,000,000 and maxRegions defaults to 512, also exposed by the frozen DEFAULT_SFZ_PARSE_LIMITS object. Limits must be positive safe integers. Parsing rejects a non-string source with TypeError; invalid limits or exceeded budgets throw RangeError. Zone resolution accepts only the maxRegions option and omits regions without a sample. None of these functions fetches or decodes audio.

    notesToScore(notes, options?) accepts RecordedNote values with midi, velocity, voice, onsetSec and durationSec. It creates one part per voice, preserves performed seconds and velocity, and derives notated quarters at bpm (default 120). ppq is a positive safe integer tick resolution (default 480); title defaults to Recording. A finite positive quantizeGrid snaps onsets and durations to that many quarters; zero, negative or omitted values disable snapping. Notated durations are at least one tick, including a captured zero-duration note. The generated measures use 4/4.

    Invalid tempo, PPQ, non-finite quantization, negative or non-finite captured timings, and derived timing beyond the supported measure range throw RangeError before allocating measures. Empty recordings still validate options and produce one empty measure. The conversion owns no recording device, player or clock.

    qwertyMidi(code, baseC) returns the mapped MIDI number or null for an unmapped physical-key code. Only the map’s own key entries are considered; inherited object names do not map to notes. gridKeyboardMidi and gridQwertyCellMidi share that lookup and their existing explicit-pad fallback.

    parseGridMap(text) reads sparse ref=midi pairs separated by commas, semicolons or whitespace, using refs such as a1 and z10. Empty, malformed or non-finite assignments are ignored without clearing an earlier mapping; numeric zero remains a valid value. Finite values are rounded, with the last valid assignment for a cell winning. This mapping helper does not clamp MIDI range; callers choose and validate the playable range for their input surface.

    renderScoreToBuffer plays the same score · sound · effect stack through an OfflineAudioContext — deterministically and faster than real time — and hands back an AudioBuffer. bufferToWav (or renderScoreToWav) encodes it as a downloadable 16-bit PCM WAV. This closes the loop alongside the notation/MIDI serializers: load/build → play → export audio.

    @webmusic/score/playInteractive API sandbox
    import { Duration, MeasureId, PartId, Pitch, Rational, ScoreBuilder, VoiceId } from '@webmusic/score';
    import { renderScoreToBuffer, bufferToWav } from '@webmusic/score/play';
    import { Sound, Effect } from '@webmusic/score/play/headless';
    
    const b = new ScoreBuilder();
    const part = PartId('piano');
    const voice = VoiceId('v1');
    const ts = { numerator: 4, denominator: 4 };
    b.setMetadata({ title: 'Render' })
      .addTempo({ atQuarters: Rational.ZERO, bpm: 120 })
      .addMeter({ atQuarters: Rational.ZERO, measureNumber: 1, timeSignature: ts });
    b.addPart({ id: part, name: 'Piano', staves: 1 });
    b.addMeasure({ id: MeasureId('m1'), number: 1, onsetQuarters: Rational.ZERO, durationQuarters: new Rational(4), timeSignature: ts });
    ['C4', 'E4', 'G4', 'C5'].forEach((p, i) =>
      b.addNote(part, { id: b.newNoteId(), pitch: Pitch.parse(p), onsetQuarters: new Rational(i), duration: Duration.quarter(), voice }));
    
    // Render the same score · sound · effect stack offline, then download a WAV.
    const btn = document.createElement('button');
    btn.textContent = 'Render and download WAV';
    btn.onclick = async () => {
      const buffer = await renderScoreToBuffer(b.build(), {
        synth: Sound.oscillator({ type: 'triangle' }),
        effect: Effect.reverb({ wet: 0.3 }),
      });
      const url = URL.createObjectURL(bufferToWav(buffer));
      const a = document.createElement('a');
      a.href = url;
      a.download = 'score.wav';
      a.click();
    };
    document.body.append(btn);
    Option Default Description
    synth oscillator Timbre — a Sound or HeadlessSynth that implements connect().
    effect / reverb — Effect chain (legacy reverb sugar).
    tempo score tempo Override BPM.
    sampleRate / channels 44100 / 2 Output format.
    tailSeconds 1.5 w/ effect, else 0.15 Extra silence for effect tails.

    Self-routing synths (Tone / Spessa) route to their own destination and aren’t captured — use a Sound or a HeadlessSynth with connect() (oscillator / soundfont / sfz).

    Players take a Score, not a file. Conversion lives in one place, so the same parsed Score feeds a player, a visualizer, and an analyzer. Each format has a parser and serializer (parseMIDI / serializeMIDI, parseMusicXML / serializeMusicXML, parseMXL / serializeMXL, parseABC / serializeABC).

    @webmusic/score/playInteractive API sandbox
    import { detectFormat, loadScore, loadScoreFromUrl } from '@webmusic/score/io';
    import { createScorePlayerFromUrl } from '@webmusic/score/play/headless';
    
    // Players take a Score, not a file. Parse once, reuse everywhere.
    // const a = await loadScore(bytes);                 // auto-detect midi/musicxml/mxl/abc
    // const b = await loadScore(xml, { format: 'musicxml' });
    // const c = await loadScoreFromUrl('/etude.mid');   // fetch + infer from extension
    // detectFormat(bytes); // 'midi' | 'mxl' | 'musicxml' | 'abc'
    
    // One-line sugar (parsers load lazily):
    // const player = await createScorePlayerFromUrl('song.mxl');
    console.log('loaders ready');
    Format Extensions Notes
    MIDI .mid .midi Standard MIDI File
    MusicXML .xml .musicxml
    MXL .mxl zip-compressed MusicXML
    ABC .abc

    Parsing large files blocks the main thread — measured ~160ms for a 20k-note MusicXML and ~360ms for a 100k-note MIDI. createParserWorker() moves the parse into a Web Worker: the worker posts back plain Score JSON and the main thread only pays the cheap scoreFromJSON rebuild (~50ms), so the UI stays responsive.

    import {createParserWorker} from '@webmusic/score/io/worker-client';
    const parser = createParserWorker();
    // Bytes / text you already have — caller-owned buffers are never detached.
    const score = await parser.parse(buffer); // format sniffed; or parse(data, 'midi')
    // Fetch happens on the main thread (page cookies, relative URLs), parse in the worker.
    const fromUrl = await parser.loadFromUrl('/scores/sonata.mxl');
    parser.dispose(); // terminate the worker when done

    After checking maxInputBytes, parse*() snapshots the caller’s ArrayBuffer or exact Uint8Array range once. A real Worker receives that private snapshot by transfer; the fallback parses the same snapshot. URL loading may transfer its own bounded response buffer directly. Every route preserves caller ownership and never mutates or detaches the original.

    By default the worker is spawned with the bundler-friendly pattern new Worker(new URL('./worker.js', import.meta.url), {type: 'module'}), which Vite / webpack / Rollup detect and bundle automatically. If your bundler can’t handle it, pass your own Worker instance or factory:

    const parser = createParserWorker(
    () => new Worker(new URL('@webmusic/score/io/worker', import.meta.url), {type: 'module'}),
    );

    Where Worker doesn’t exist (Node, SSR) — or constructing one throws — every call transparently falls back to in-process parsing behind the same Promise API, so the code is safe to run anywhere. The worker protocol itself (ParseRequest / ParseResponse and the pure handleParseRequest) is exported too, in case you want to host the handler in your own worker.


    The @webmusic/score/play/drivers entry maps continuous browser input to a borrowed transport. The live example uses bindValue to scrub a ScorePlayer from a numeric slider. These browser adapters are separate from InteractivePlayer’s clock and MIDI helpers.

    @webmusic/score/play/driversbindValue

    Drag the slider to drive a paused ScorePlayer. Reset releases the player and its polling binding.

    drag to scrub
    0.0s
    Parameters
    intervalPolling interval in milliseconds. Changing it replaces and releases the previous binding.
    // Configure bindValue below.
    // Each browser adapter returns a cleanup; retain every binding you install.
    import { bindScroll, bindPointer, bindOrientation } from '@webmusic/score/play/drivers';
    const offScroll = bindScroll(player, { mode: 'position' });
    const offPointer = bindPointer(player, surfaceEl, { axis: 'x' });
    const offOrientation = bindOrientation(player, { axis: 'gamma' });
    offScroll();
    offPointer();
    offOrientation();

    Here player is a compatible continuous transport such as ScorePlayer, not the InteractivePlayer in the earlier independent example. These browser adapters own their installed event bindings; the transport and target elements remain caller-owned.

    API Kind Use it for
    createScorePlayer / ScorePlayer / Player Engine Autonomous push-clock timeline engine.
    createScorePlayerFromUrl(url, opts?) Engine Load a file and build a ScorePlayer.
    playScore(score, opts?) / playScoreFromUrl(url, opts?) Headless action Create, start and return a caller-controlled ScorePlayer; failed startup disposes owned resources.
    InteractivePlayer / createInteractivePlayer Engine Event-driven pull engine: voices, sources, mixing, live score swap.
    Rack / createRack Ensemble Compose single-instrument players into a synced, individually-mixed group; .effect / setEffect() replace the summed master-bus chain.
    LoopPlayer / createLoopPlayer Transport A–B loop region over a ScorePlayer, with finite-repeat counting.
    AbPlayer / createAbPlayer Transport Live zero-latency switching between named arrangements (built on InteractivePlayer).
    LfoController / createLfoController, lfoWave Modulation DOM-free LFO state, scheduling and borrowed-target modulation; pure waveform helper.
    SynthPanelAudioGraph, SynthPanelAudioOptions, SynthPanelSection Headless class / types (@webmusic/score/play/headless) Effects/EQ graph with stable same-context ports; separate from the stateless Play entry.
    Sound Timbre Uniform wrapper: oscillator / samples / sfz / soundfont2 / from / custom.
    OscillatorSynth, SoundfontSynth, RangeSampler Synth Oscillator, per-note sampler, pitch-shifting range sampler.
    SpessaSynthSynth Synth adapter Adopt an existing spessasynth instance as a HeadlessSynth.
    createReverbNode, chooseZone, playbackRateFor, parseSfz Audio helpers Reverb node, zone selection, pitch math, .sfz parsing.
    renderScoreToBuffer, renderScoreToWav, bufferToWav Export Render a score offline to an AudioBuffer / downloadable WAV.
    InteractivePlayer.record / .stopRecording, notesToScore Record Capture a live performance back into a Score.
    Effect Effect reverb / delay / filter / distortion / compressor / gain / chain / custom.
    insertEffect, resolveEffect Effect helpers Wire an effect into a custom graph.
    bindScroll, bindPointer, bindOrientation, bindValue Driver (@webmusic/score/play/drivers) Bind scroll / pointer / orientation / value to a PlaybackTransport (e.g. a ScorePlayer).
    Metronome, bindClock, bindMidiInput Live input Drive the pull engine’s advance() from a clock or Web MIDI.
    defineScorePlayerElement, ScorePlayerElement Web Component Register / the class behind <score-player>.
    defineRackControlElement, defineScoreRecorderElement Web Component Register <score-rack-control> (Rack mixer with a master .effect proxy) / <score-recorder>.
    defineNoteInputElement, defineSynthPanelElement Web Component Register <score-note-input> / <score-synth-panel>.
    defineAllElements Web Component Register every element in the flat set at its default tag (the /auto entry calls this).
    loadScore, loadScoreFromUrl, detectFormat IO (@webmusic/score/io) Parse / fetch+parse / sniff format.
    createParserWorker, handleParseRequest IO (@webmusic/score/io/worker-client, /worker-protocol) Parse off the main thread in a Web Worker (with in-process fallback).
    parseMIDI / serializeMIDI, parseMusicXML / serializeMusicXML, parseMXL / serializeMXL, parseABC / serializeABC IO (@webmusic/score/io) Each format ⇄ Score.
    HeadlessSynth, ScorePlayerOptions, PlayerTimeUpdate, PlaybackTransport, InteractivePlayerOptions, RackOptions, RackAddOptions, LfoControllerOptions, LfoControllerState, LfoTarget, LfoShape, SynthPanelLfoErrorDetail, RenderOptions, RecordedNote, RecordToScoreOptions, EffectNode, ReverbOptions / DelayOptions / FilterOptions / DistortionOptions / CompressorOptions / LimiterOptions / AnalyserOptions / BitcrusherOptions / TremoloOptions / ChorusOptions, FmOptions / WavetableOptions / NoiseOptions / MidiOutOptions, ClockOptions / MidiInputOptions, ScoreFormat, … Types Option, event, and contract shapes.