npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@webmusic/score

v0.2.0

Published

Symbolic music for the web: immutable score model, MIDI/MusicXML/MXL/ABC I/O, playback, analysis, views, Web Components and React bindings.

Readme

@webmusic/score

Symbolic music for the web. One package: an immutable Score model, MIDI / MusicXML / MXL / ABC import and export, two playback engines, staff / piano-roll / waterfall views, musical analysis, Web Components, and React bindings.

npm install @webmusic/score

Changing this package rather than using it? ARCHITECTURE.md explains the layering, the capability rules, the transport design and the reasoning behind them.

The command above is enough for the model, IO, API and Headless entries. Add the optional presenter peer when using Web Components:

npm install @webmusic/score @webmusic/ui

Start playback:

import {loadScoreFromUrl} from '@webmusic/score/io';
import {playScore} from '@webmusic/score/play/headless';

const score = await loadScoreFromUrl('song.musicxml'); // MIDI/MusicXML/MXL/ABC
const player = await playScore(score);                 // create and start playback
// Later, when playback is no longer needed: player.dispose();

Keep the returned player for the interaction lifetime: playScore() resolves once playback starts, not when the piece ends. Call player.dispose() during teardown to release its owned resources.

…or to pixels, with the same Score:

import {renderPianoRollVisualizer} from '@webmusic/score/view/render';
renderPianoRollVisualizer(score, svgElement, {pixelsPerSecond: 30, noteHeight: 6});

The parsed Score is the shared currency: parse once, then play, render, and analyze the same immutable object — or ship it to a Worker.

Documentation and contribution

Current usage and reference pages are maintained in the documentation site:

Historical package notes retain earlier technical material; they are not the current runnable reference. For development, start at the contribution guide and the Score architecture. Use the owning public reference and exported types to establish implemented behavior.

Which entry do I import?

Subpaths follow a two-level grammar. The first level is a capability — io, view, play, analyze, react; the package root is the core model. The second level is a form of that capability: headless is stateful code with no UI DOM (operations still have entry-specific environment and resource requirements); element is Web Components; render is imperative browser renderers; drivers binds browser input to a player; auto registers all elements as an import side effect; global is the CDN IIFE bundle; and worker / worker-client / worker-protocol are the Worker trio — the self-registering Worker runtime, the main-thread client, and the pure wire types. Each capability's root (@webmusic/score/io, …/view, …/play, …/analyze) is its stateless API: data in, data out, no DOM, no engine state.

| Entry | What it is | |---|---| | @webmusic/score | The core model: immutable Score, ScoreBuilder, Rational, Pitch, Duration, TimeMap, queries, events, JSON. Zero DOM. | | @webmusic/score/io | Stateless parse / serialize / load for MIDI, MusicXML, MXL, ABC. | | @webmusic/score/io/load | Just the loaders: loadScore, loadScoreFromUrl, detectFormat, limits. | | @webmusic/score/io/formats | Per-format parsers and serializers only. | | @webmusic/score/io/worker-client | Main-thread parser Worker client (createParserWorker). | | @webmusic/score/io/worker-protocol | Pure parser wire types and constants for custom hosts. | | @webmusic/score/io/worker | Self-registering parser Worker runtime. | | @webmusic/score/view | Stateless view API: layout geometry, note sequences, types. | | @webmusic/score/view/headless | Score windows, map navigation and held-pitch state with optional borrowed playback; no DOM. | | @webmusic/score/view/render | Imperative browser visualizers (SVG / canvas / OSMD). | | @webmusic/score/view/element | <score-view>, <score-pitch-view> and <score-sheet-view> Web Components. | | @webmusic/score/play | Stateless playback API: SFZ data helpers, offline render types. | | @webmusic/score/play/headless | Players, synths, Sound, Effect, Rack, LfoController, offline rendering. | | @webmusic/score/play/drivers | Pointer / scroll / orientation / value transport adapters. | | @webmusic/score/play/element | All playback Web Components (<score-player>, …). | | @webmusic/score/play/auto | Import for side effect: registers every play element. | | @webmusic/score/play/global | Browser IIFE bundle (window.WebMusicScorePlay). | | @webmusic/score/play/demos | Demo elements and sample scores. | | @webmusic/score/analyze | Stateless analysis: key, chords, Roman numerals, motifs, voice leading. | | @webmusic/score/analyze/headless | Sessions, playback followers, live trackers, worker clients and presentation prediction; no DOM. | | @webmusic/score/analyze/element | Interactive current-chord and chord-progression analysis components. | | @webmusic/score/analyze/worker-client | Main-thread analysis Worker client (createAnalysisWorker). | | @webmusic/score/analyze/worker-protocol | Pure analysis wire types and constants. | | @webmusic/score/analyze/worker | Self-registering analysis Worker runtime. | | @webmusic/score/react | React provider, hooks, views, and one-line presets. |

Module entries ship ESM + CJS with TypeScript declarations; /global is the browser IIFE form. Public subpaths let applications select capabilities. Bundle contents still depend on the selected entry, its imports, side effects, and the consumer's bundler configuration.

Core model (@webmusic/score)

The thin foundation: the immutable Score, musical primitives, TimeMap, note queries, a typed EventEmitter, and JSON serialization. It touches no browser APIs, so it is safe in Node, Workers, and SSR.

ScoreBuilder collects metadata, parts, measures, tempo, meter, and notes, then returns an immutable Score:

import {Duration, MeasureId, PartId, Pitch, Rational, ScoreBuilder, VoiceId} from '@webmusic/score';

const builder = new ScoreBuilder();
const partId = PartId('piano');
const timeSignature = {numerator: 4, denominator: 4};

builder
  .setMetadata({title: 'Etude', composer: 'WebMusic'})
  .addTempo({atQuarters: Rational.ZERO, bpm: 96})
  .addMeter({atQuarters: Rational.ZERO, measureNumber: 1, timeSignature});

builder.addPart({id: partId, name: 'Piano', midiProgram: 0, staves: 2});
builder.addMeasure({
  id: MeasureId('m1'),
  number: 1,
  onsetQuarters: Rational.ZERO,
  durationQuarters: new Rational(4),
  timeSignature,
  keySignature: {fifths: 0, mode: 'major'},
});

builder.addNote(partId, {
  id: builder.newNoteId(),
  pitch: Pitch.parse('C4'),
  onsetQuarters: Rational.ZERO,
  duration: Duration.quarter(),
  performed: {onsetSec: 0, durationSec: 0.5, velocity: 90},
  voice: VoiceId('piano-v1'),
  staff: 1,
});

const score = builder.build();

Share the result between a player, a renderer, and an analyzer without one consumer mutating another's data. score.withMetadata(...) returns a modified copy; scoreFromJSON(score.toJSON()) round-trips it, with a schema marker (v0.1), runtime validation, and coded ScoreJSONError rejections.

Time, queries, events:

import {Rational, TimeMap, notesAt, notesIn, notesOverlapping, EventEmitter} from '@webmusic/score';

const map = new TimeMap(score);
map.quartersToSeconds(new Rational(2)); // musical time → wall clock
map.quartersToMBS(new Rational(5));     // → {measure, beat, subbeat}

notesAt(score, new Rational(2));        // notes sounding at a position
notesIn(score, Rational.ZERO, new Rational(4));
notesOverlapping(score, new Rational(1), new Rational(3));

const events = new EventEmitter<{ready: {id: string}}>();
events.once('ready', ({id}) => console.log(id));

Compatibility helpers (pitchToMidi, quartersToTicks, noteMidi, tickToMeasureBeat, locateSeconds, …) expose MIDI-style 480 PPQ ticks and second-based readings for legacy integrations.

I/O (@webmusic/score/io)

| Format | Extensions | Notes | |---|---|---| | MIDI | .mid .midi | Standard MIDI File | | MusicXML | .xml .musicxml | | | MXL | .mxl | zip-compressed MusicXML | | ABC | .abc | compact single-voice subset |

import {
  detectFormat,
  loadScore,
  loadScoreFromUrl,
  parseMIDI,
  parseMusicXML,
  parseMusicXMLDetailed,
  serializeMIDI,
  serializeMusicXML,
} from '@webmusic/score/io';

const score = await loadScoreFromUrl('song.mid');                  // infers format
const score2 = await loadScore(bytesOrText, {format: 'musicxml'}); // from data

parseMusicXML() returns just a Score. Use the *Detailed() variants at an import boundary when normalisation must be inspectable:

const {score, diagnostics} = parseMusicXMLDetailed(xml);
for (const d of diagnostics) {
  reportImportWarning(d.code, d.message, d.location); // stable code + severity + location
}

The same result shape comes from parseMXLDetailed(), loadScoreDetailed(), and loadScoreFromUrlDetailed(). ABC detailed parsing reports unsupported key / tempo / voice features; MIDI detailed parsing reports measure-grid and meta normalisations.

URL and Worker calls accept {signal, timeoutMs}; one cancellation scope covers fetch, bounded body reading, parsing, and Worker requests:

const controller = new AbortController();
const pending = loadScoreFromUrlDetailed(url, {signal: controller.signal, timeoutMs: 15_000});
controller.abort();
await pending; // rejects with AbortError; ScoreLoadTimeoutError marks a deadline instead

Parsing moves off the main thread with the explicit Worker client:

import {createParserWorker} from '@webmusic/score/io/worker-client';

const parser = createParserWorker();
const {score, diagnostics} = await parser.parseDetailed(xml, 'musicxml');
parser.dispose();

In an ESM browser bundle the default client resolves the published module Worker automatically and falls back in-process if a Worker cannot be constructed; the CJS entry uses the in-process Promise API by default. Pass a Worker instance or factory for a custom bundler setup — its lifecycle then belongs to the client. Supplied buffers are never detached or mutated. Known failures keep their class and stable code across the Worker boundary; other structured remote failures reject with ParserWorkerRemoteError. Wire types live at @webmusic/score/io/worker-protocol; the self-registering runtime at @webmusic/score/io/worker.

Views (@webmusic/score/view)

Three views — staff, piano roll, waterfall — at four altitudes:

// 1. Pure layout data (no DOM) — feed your own renderer
import {createPianoRollLayout, scoreToNoteSequence} from '@webmusic/score/view';
const layout = createPianoRollLayout(score, {pixelsPerSecond: 30, laneHeight: 6});
const sequence = scoreToNoteSequence(score);

// 2. Stateful behavior with no UI or DOM
import {createScoreView} from '@webmusic/score/view/headless';
const view = createScoreView(score, {type: 'piano-roll'});
view.setViewport(0, 8);
view.noteOn(60, 0);
console.log(view.state.activeNotes, view.state.visibleNotes);

// 3. WebMusic's imperative browser renderer
import {renderPianoRollVisualizer} from '@webmusic/score/view/render';
const viz = renderPianoRollVisualizer(score, svg, {pixelsPerSecond: 30, noteHeight: 6});
viz.redraw(sequence.notes[0]); // highlight one currently-sounding note

For application-owned navigation, createScoreMapView borrows a playback source and delegates quarter-note seeks with explicit command outcomes. createPitchView maintains held pitches for any representation; currentStaffMarks and currentFretMarks from @webmusic/score/view provide pure readout data. createAnalysisFollower combines that source's score, position and activity with an analysis session. These objects release only their own subscriptions and never own playback.

@webmusic/score/view/render also exports renderStaffVisualizer, renderWaterfallVisualizer, bindPlayerToVisualizer, and renderOSMDStaffVisualizer (engraving through the optional opensheetmusicdisplay peer).

The Web Component entry provides three composable tags:

import {defineAllViewElements} from '@webmusic/score/view/element';
defineAllViewElements();
<score-player id="p" src="song.mid"></score-player>
<score-view type="piano-roll" player="#p"></score-view>
<score-pitch-view type="keyboard" player="#p" low="48" high="84"></score-pitch-view>
  • <score-view> selects piano-roll (default), staff, waterfall, map or thumbnail. It resolves explicit .score, then src, then the selected player's score. Type changes reuse that data. Map provides density cells and rate-aware seeking; thumbnail is a static preview without active highlighting or interaction, even when borrowing a player's loaded score.
  • <score-pitch-view> selects keyboard (default), current-pitch staff or fretboard. It borrows held notes through player (or legacy source), owns no Score, and sends no note or transport commands. Type changes retain activity.
  • <score-sheet-view> engraves an explicit .score or src through the optional opensheetmusicdisplay peer. Its optional player connection follows note events; it does not borrow the player's score or native snapshot.

Each instance renders one surface. Configuration belongs to attributes or application-owned controls; documentation demos put it in external Parameters.

Playback (@webmusic/score/play)

Two engines at opposite ends of one question: who owns time?

ScorePlayer — the push engine. Owns its own clock; a lookahead scheduler advances on the AudioContext clock. For concerts, fixed playback, audio-synced views:

import {ScorePlayer, playScore, createScorePlayerFromUrl} from '@webmusic/score/play/headless';

const player = new ScorePlayer(score, {reverb: {wet: 0.25}});
player.on('cursor', (pos) => updatePlayhead(pos));
player.on('end', () => console.log('done'));
await player.play();
// player.pause(); player.seek(12); player.setTempo(140);

const playing = await playScore(score);              // one-call: create + start
const p = await createScorePlayerFromUrl('song.mxl'); // load + build (parsers load lazily)
// Dispose each player you own when its interaction ends:
// player.dispose(); playing.dispose(); p.dispose();

InteractivePlayer — the pull engine. Owns no clock; it sounds only when you feed it a signal — a gesture, a metronome, an OSC message, a game loop. Built for installations, conducting, and creative coding:

import {InteractivePlayer, Sound} from '@webmusic/score/play/headless';

const player = new InteractivePlayer();
player.addVoice('piano', Sound.samples({60: 'c4.m4a', 72: 'c5.m4a'}));
player.addVoice('strings', Sound.oscillator({type: 'sawtooth'}));
await player.addSourceFromUrl('major', 'major.mid', {route: {Piano: 'piano'}});
await player.addSourceFromUrl('minor', 'minor.mid');
await player.preload();

// Every external beat advances one beat and returns its notes
onConductorBeat((dtSeconds) => player.advance({secondsPerBeat: dtSeconds}));

player.setMix({piano: 1.0, strings: 0.3});  // real-time per-voice mixing
player.select('minor');                      // O(1) score swap
player.play([{midi: 67, velocity: 90, voice: 'piano'}]); // free triggering

It runs fully headless too: with no AudioContext, advance() still returns the beat's notes, so the same pull signal can drive lights or robotics.

| You want | Use | |---|---| | Autonomous timeline playback (pure Web Audio) | ScorePlayer | | External-signal, beat-at-a-time interaction, live score swap | InteractivePlayer | | Multiple instruments, each its own score / sound / effect | Rack | | Progress bar / scrub / rate control | ScorePlayer + @webmusic/score/play/drivers | | Drop a player widget on the page | <score-player> | | A project that already owns a Tone.js clock | TonePlayer (optional tone peer) |

Sounds

A player is conceptually <player · score · sound · effect>. Sound wraps any timbre backend behind one contract, adds a private gain bus (per-voice volume / mute / solo), and loads lazily:

import {ScorePlayer, Sound} from '@webmusic/score/play/headless';

Sound.oscillator({type: 'triangle'})         // built-in subtractive synth
Sound.samples({60: 'c4.m4a', 72: 'c5.m4a'})  // one file per exact MIDI note
Sound.sfz('piano.sfz')                       // key ranges, velocity layers, round-robin
Sound.soundfont2('strings.sf2')              // .sf2/.sf3 via optional spessasynth_lib
Sound.from(toneInstrument)                   // adopt a Tone.js instrument
Sound.custom((ctx) => myHeadlessSynth)       // your own WebAudio graph

const player = new ScorePlayer(score, {synth: Sound.sfz('piano.sfz')});
await player.preload(); // optional warm-up; play() waits for loading anyway
await player.play();

Also exported: Sound.fm, Sound.wavetable, Sound.noise, Sound.layer, Sound.midiOut, plus the lower-level OscillatorSynth, SoundfontSynth, RangeSampler, and SpessaSynthSynth.

SoundFont playback and the workletUrl recipe

Sound.soundfont2() plays .sf2 / .sf3 files through the optional spessasynth_lib peer, which runs as an AudioWorklet. The package does not guess the worklet module URL (a literal ?url magic import resolves only under Vite and breaks webpack / esbuild consumers), so you supply it once via Soundfont2Options.workletUrl:

// Vite
import workletUrl from 'spessasynth_lib/dist/spessasynth_processor.min.js?url';
import {Sound} from '@webmusic/score/play/headless';

const sf2 = Sound.soundfont2('strings.sf2', {workletUrl});

With any other bundler, pass a URL your build emits or your app hosts:

const sf2 = Sound.soundfont2('strings.sf2', {
  workletUrl: '/assets/spessasynth_processor.min.js',
});

Or, when your application owns AudioWorklet registration itself, use the escape hatch — called once per AudioContext instead of loading workletUrl:

const sf2 = Sound.soundfont2('strings.sf2', {
  registerWorklet: (ctx) => ctx.audioWorklet.addModule(myWorkletUrl),
});

Without either option, Sound.soundfont2 fails with an error repeating this recipe.

Effects and racks

Effect mirrors Sound: a lazy, composable recipe inserted between the output bus and the destination:

import {Effect, ScorePlayer, Sound} from '@webmusic/score/play/headless';

const room = Effect.chain(
  Effect.filter({type: 'lowpass', frequency: 6000}),
  Effect.delay({delaySeconds: 0.3, feedback: 0.35, wet: 0.25}),
  Effect.reverb({wet: 0.3}),
);
new ScorePlayer(score, {synth: Sound.sfz('piano.sfz'), effect: room});

Built-ins: reverb, delay, filter, distortion, compressor, limiter, gain, panner, bitcrusher, tremolo, chorus, analyser, chain, and custom.

For an ensemble, give each instrument its own player and group them in a Rack — one shared AudioContext, one master bus, per-member mix strips:

import {Rack, Effect, Sound} from '@webmusic/score/play/headless';

const rack = new Rack({effect: Effect.reverb({wet: 0.25})}); // shared room
rack.add({score: violinScore, sound: Sound.sfz('violin.sfz')});
rack.add({score: celloScore, sound: Sound.sfz('cello.sfz'), effect: Effect.filter({frequency: 4000})});

await rack.play();           // every member starts together on one clock
rack.setVolume('player-0', 0.6);
rack.mute('player-1');
rack.setEffect(room);         // hot-swap the summed master bus; members keep playing
rack.effect = undefined;      // the property form restores a dry master output
rack.on('end', () => {});    // fires once all members finish

Built-in effects and Effect.custom(factory) are lazy recipes rather than realised AudioNodes, so the same recipe can be reused by a ScorePlayer, an InteractivePlayer, and one or more racks; every owner builds its own nodes in its own AudioContext. Effect.custom(existingNodePair) is the deliberate escape hatch: it returns those exact nodes, so do not mount that form in multiple owners or across contexts. Rack has two deliberately separate effect tiers:

  • new Rack({effect}), rack.effect, and rack.setEffect() address the one post-processing chain after the summed master bus. Replacing it on a live rack swaps only that route; member players, mix state, and transport position are not rebuilt.
  • rack.add({effect}) (RackAddOptions.effect) belongs to that member alone, before its channel reaches the rack's master bus.

LFO modulation

LfoController is the reusable DOM-free owner for shape, rate, depth, phase, the run scheduler and a borrowed LfoTarget. Assigning or replacing the target does not apply a value eagerly:

import {LfoController} from '@webmusic/score/play/headless';

const lfo = new LfoController({
  shape: 'sine',
  rate: 1,
  depth: 0.6,
  target: {
    min: 300,
    max: 9000,
    unit: 'Hz',
    apply: (value) => synth.setParam('cutoff', value),
  },
});

lfo.start();                 // scheduled modulation begins
lfo.configure({rate: 2});    // hot configuration; phase is retained
lfo.stop();                  // reusable stop; target/configuration remain
lfo.dispose();               // final cleanup; the borrowed target is untouched

createLfoController() is the equivalent factory. The controller also exposes snapshot(), subscribe(), setRunning(), setTarget() and the pure lfoWave() helper. Defaults are sine, 1 Hz, depth 0.6, phase 0 and stopped; rate is normalized to 0.05…12 Hz and depth to 0…1.

Drivers

Drivers bind external input to any PlaybackTransport (ScorePlayer satisfies it directly, with scrub, seekFraction, setRate, progress, and a timeupdate event):

import {ScorePlayer} from '@webmusic/score/play/headless';
import {bindPointer, bindScroll, bindOrientation, bindValue} from '@webmusic/score/play/drivers';

const player = new ScorePlayer(score);
player.on('timeupdate', ({progress}) => renderBar(progress));
bindPointer(player, element);                  // drag to scrub
bindScroll(player, {mode: 'position'});        // scroll position → playhead
bindOrientation(player, {mode: 'rate'});       // device tilt → tempo
bindValue(player, {getValue: () => sensor()}); // any number → seek/rate

Offline rendering is code-only too: renderScoreToBuffer / renderScoreToWav / bufferToWav from @webmusic/score/play/headless.

Playback Web Components

Registration is explicit, so the headless entries stay DOM-free:

import {defineScorePlayerElement, defineAllElements} from '@webmusic/score/play/element';
defineScorePlayerElement(); // just <score-player>
defineAllElements();              // …or the whole set (what './play/auto' calls)
<score-player src="song.mid"></score-player>
<score-player src="song.musicxml" sound-font="/samples/{midi}.m4a"></score-player>
<score-rack-control></score-rack-control>
<score-note-input layout="piano" keyboard start="48" end="71"></score-note-input>

| Element | Purpose | |---|---| | <score-player> | One player: load a src or assign .score; transport plus live rate / volume / pan / loop. Assign .controller to present a headless PlayerController you already own. | | <score-rack-control> | Mixer for a shared Rack. | | <score-recorder> | Capture a live performance into a Score; replay + MIDI / MusicXML export. | | <score-note-input> | Discrete note surface — layout="piano\|grid\|chords", keyboard for QWERTY. | | <score-synth-panel> | Control panel — sections="sound,effects,envelope,eq,lfo,macros". |

Assign .score, .sound, .effect, or .rack as properties to override attribute-driven defaults.

The former <simple-score-player> compatibility tag is retired; use <score-player> for transport.

<score-synth-panel> directly composes reusable Score Headless controllers with structural presenters from @webmusic/ui, with no standalone Element Adapter module between them. <score-player> reaches the same transport presenter through its existing public imperative mounter, whose signature remains a compatibility surface. Every <score-synth-panel> section uses a published presenter, and its LFO section binds the same LfoController. Hiding the LFO section removes only its presenter, so an already-running modulation continues; disconnecting the panel disposes the controller and stops it. The panel's Headless boundary remains Partial, and callback-owned boundaries remain Partial rather than inventing a Headless controller solely to satisfy the diagram. Score depends on UI for presenters across its complete visible element catalog, while UI does not depend back on Score, Audio or kernel.

Analysis (@webmusic/score/analyze)

Headless analysis that reads a Score and returns plain serializable data — no DOM, so it runs in Node, Workers, tests, and batch jobs:

import {
  summarizeScore,
  chordTimeline,
  detectKey,
  segmentChords,
  romanNumerals,
  findMotifs,
  rhythmPatterns,
  voiceLeading,
} from '@webmusic/score/analyze';

const summary = summarizeScore(score);           // metadata, counts, duration, tempo, key, range
const chords = chordTimeline(score, {windowQuarters: 1});
const key = detectKey(score);                    // {tonic, mode, confidence, scores}
const segments = segmentChords(score, {windowQuarters: 4});
const roman = romanNumerals(score, key);         // I, IV, V7 … relative to the key
const motifs = findMotifs(score, {length: 4, minOccurrences: 2});
const rhythms = rhythmPatterns(score, 4);
const issues = voiceLeading(score);              // large leaps, parallel fifths/octaves

@webmusic/score/analyze/headless adds stateful code-only components: createAnalysisSession, createLiveChordTracker, createLiveKeyTracker. The pure createScoreReport helper returns formatted summary rows. Whole-score summary, distribution and rhythm reports use these code-only inputs rather than Element wrappers; see the report workflow.

Heavy analysis moves off the main thread with the Worker trio:

import {createAnalysisWorker} from '@webmusic/score/analyze/worker-client';

const client = createAnalysisWorker();
const initial = await client.analyze(score, {windowQuarters: 2});
const updated = await client.update(editedScore); // latest-wins coalescing
client.dispose();

ESM browser consumers get the adjacent module Worker automatically; CJS (or a blocked Worker) takes the same in-process Promise API. A ready-made Worker instance is borrowed; one created by the default path or a factory is owned and terminated on dispose(). Structured remote failures reject with AnalysisWorkerRemoteError (code, operation, retryable). Wire types live at @webmusic/score/analyze/worker-protocol; the runtime at @webmusic/score/analyze/worker.

Analysis Web Components expose two focused inspection tools:

import {defineAllAnalysisElements} from '@webmusic/score/analyze/element';
defineAllAnalysisElements();
<score-player id="p" src="song.mid"></score-player>
<score-chord-analysis player="#p"></score-chord-analysis>
<score-pitch-view type="keyboard" player="#p" low="48" high="84"></score-pitch-view>
<score-pitch-view type="staff" player="#p"></score-pitch-view>

The analysis tool and pitch views are explicit siblings. Register the View tags with their define*Element() helpers from @webmusic/score/view/element. Each Analyze tag renders one core surface; display configuration belongs to its attributes, and supporting keyboards, staffs or fretboards remain reusable View components. They borrow the player's existing data or held-note state and own no playback graph.

The two tags cover current-chord interpretation and passage harmony. The Analyze overview selects and documents them. Each live demo contains its player and the current tag, with external Parameters.

Specialist Roman/voice-leading views and retained chord updates remain application-owned compositions using the API and Headless trackers and projections. Motif search remains an API/Headless result. Choose a fixed musical surface for live display; disconnecting it releases its resources and leaves playback with its owner.

React (@webmusic/score/react)

Two levels of API behind the optional react >= 18 peer.

Ready-made presets for direct JSX use:

import {SimpleScoreWorkspace} from '@webmusic/score/react';

export function Page({score}) {
  return (
    <SimpleScoreWorkspace
      score={score}
      analysis
      pianoRoll={{width: 720, height: 240}}
      staff={{width: 720, height: 220}}
      waterfall={{width: 720, height: 320}}
    />
  );
}

Pass analysis={false} / staff={false} / waterfall={false} to disable parts, or use a single preset (<SimpleScorePlayer score={score} />, <SimpleStaff />, <SimplePianoRoll />, <SimpleWaterfall />).

Provider + hooks + views for custom application design:

import {ScoreProvider, useCursor, usePlayer, useScore, PlayerControls, PianoRollView} from '@webmusic/score/react';

function App({score}) {
  return (
    <ScoreProvider score={score} playerOptions={{tempo: 96}}>
      <PlayerControls />
      <PianoRollView width={720} height={240} />
      <CursorReadout />
    </ScoreProvider>
  );
}

function CursorReadout() {
  const cursor = useCursor();
  return <span>m.{cursor?.measure} b.{cursor?.beat}</span>;
}

useScoreAnalysis({windowQuarters: 4}) exposes analysis results as React state; <AnalysisSummary /> renders them. For large scores, useScoreAnalysisAsync() returns {result, pending, error} and works through the analysis Worker client. Your app provides the Score (parse one with @webmusic/score/io); the bindings do not own an app shell.

CDN / <script> usage

A browser IIFE with built-in playback and UI ships at dist/play/auto.global.js. It registers every playback element and exposes the WebMusicScorePlay global:

<script src="https://unpkg.com/@webmusic/score/dist/play/auto.global.js"></script>

<score-player src="song.mid"></score-player>
<score-note-input layout="piano" keyboard></score-note-input>

<script>
  // Element classes and define* helpers are on the global:
  console.log(WebMusicScorePlay);
</script>

The same file resolves from the @webmusic/score/play/global subpath for tooling that wants it by name. Bundler users should prefer import '@webmusic/score/play/auto' (side-effect registration) or explicit define* calls from @webmusic/score/play/element instead.

The optional SoundFont backend stays outside the IIFE and loads only when an SF2/SF3 route connects. Direct browser applications using this backend must provide an import map before loading scripts. For example, these pinned mappings resolve Spessa and its transitive module imports:

<script type="importmap">
{
  "imports": {
    "spessasynth_lib": "https://unpkg.com/[email protected]/dist/index.js",
    "spessasynth_core": "https://unpkg.com/[email protected]/dist/index.js",
    "stb-vorbis": "https://unpkg.com/[email protected]/dist/index.js"
  }
}
</script>

Applications may host these modules themselves, or map spessasynth_lib to a browser-ready bundle. The AudioWorklet remains a separate resource: supply workletUrl or registerWorklet as shown in the SoundFont recipe above. Normal module bundlers resolve an installed Spessa peer through the lazy ESM/CJS import; the IIFE-only external setting does not change that integration.

Optional peer dependencies

The core install stays lean; each optional peer unlocks one capability:

| Peer | Version | Unlocks | |---|---|---| | opensheetmusicdisplay | >=1.8.0 | OSMD staff engraving in ./view/render (renderOSMDStaffVisualizer). | | spessasynth_lib | ^4.0.0 | Sound.soundfont2() — .sf2 / .sf3 SoundFont playback (see the workletUrl recipe above). | | tone | >=14.0.0 | TonePlayer — playback on an injected Tone.Transport, and Sound.from(toneInstrument). | | react | >=18 | @webmusic/score/react hooks and components. | | @webmusic/ui | ^0.2.0 | /element, /auto, visible analysis elements and default Web Component presenters. |

Everything else — parsing, the built-in oscillator / FM / wavetable / sampler sounds, Headless views and analysis — works without optional peers.

Ecosystem

@webmusic/score is the symbolic-music layer of the WebMusic family:

  • @webmusic/kernel — the shared primitives this package builds on (a required peer).
  • @webmusic/ui — domain-neutral styles and presenters used by all visible Score Web Components (an optional peer for browser UI entries).
  • @webmusic/audio — audio-signal capabilities in the shared five-package release.
  • @webmusic/bridge — score ↔ audio interop in the shared five-package release.

License

MIT

For source contributions and release verification, see the contribution guide.