musicxml-io
v0.8.1
Published
Parse and serialize MusicXML (.xml/.mxl) and ABC notation with high round-trip fidelity
Downloads
2,316
Maintainers
Readme
musicxml-io
TypeScript library for parsing and serializing MusicXML and ABC notation.
Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ MusicXML │ │ │ │ MusicXML │
│ .xml / .mxl │─────▶│ Score │─────▶│ .xml / .mxl │
└─────────────────┘ │ │ └─────────────────┘
parse │ ┌─────────┐ │ serialize
┌─────────────────┐ │ │ parts │ │ ┌─────────────────┐
│ ABC notation │ │ │ └─measures │ │ ABC notation │
│ .abc │─────▶│ │ └─entries│─────▶│ .abc │
└─────────────────┘ │ └─────────┘ │ └─────────────────┘
parseAbc │ │ serializeAbc
│ │ ┌─────────────────┐
│ │ │ MIDI │
│ │─────▶│ .mid │
│ │ └─────────────────┘
│ │ exportMidi
└────────┬────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ QUERIES │ │ OPERATIONS │ │ ACCESSORS │
│ │ │ │ │ │
│ Score-level │ │ Score mutation │ │ Entry-level │
│ read operations │ │ operations │ │ helpers │
│ │ │ │ │ │
│ getMeasure() │ │ transpose() │ │ isRest() │
│ findNotes() │ │ addNote() │ │ isPitchedNote() │
│ getAllNotes() │ │ changeKey() │ │ getPartName() │
│ getHarmonies() │ │ insertMeasure() │ │ hasTie() │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ VALIDATE │
│ │
│ validate() │
│ isValid() │
│ assertValid() │
└─────────────────┘Module Structure
| Module | File | Description |
|--------|------|-------------|
| Query | src/query/index.ts | Score-level read operations (get, find, iterate) |
| Operations | src/operations/index.ts | Score mutation operations (add, delete, modify) |
| Accessors | src/entry-accessors.ts | Entry-level helpers for notes, directions, parts |
Install
npm install musicxml-ioUsage
import { parse, serialize, transpose } from 'musicxml-io';
const score = parse(xmlString);
const transposed = transpose(score, 2); // up 2 semitones
const output = serialize(transposed);ABC Notation
import { parseAbc, serializeAbc } from 'musicxml-io';
// ABC → Score
const score = parseAbc(abcString);
// Score → ABC
const abc = serializeAbc(score, {
referenceNumber: 1,
includeChordSymbols: true,
includeDynamics: true,
includeLyrics: true,
});
// Auto-detect format (MusicXML, .mxl, or ABC)
import { parseAuto } from 'musicxml-io';
const score2 = parseAuto(input);⚠️ Warning: This library's API is not yet stable and may change between versions.
File I/O (Node.js)
import { parseFile, serializeToFile } from 'musicxml-io';
const score = await parseFile('input.mxl');
await serializeToFile(score, 'output.xml');Operations
import { addNote, changeKey, changeTime } from 'musicxml-io';
const updated = addNote(score, {
partIndex: 0,
measureNumber: 1,
pitch: { step: 'C', octave: 4 },
duration: 4,
type: 'quarter',
});
const inG = changeKey(score, { fifths: 1 }, 0, 1);
const waltz = changeTime(score, { beats: 3, beatType: 4 }, 0, 1);Query
import { findNotes, getAllNotes, getMeasureCount, getHarmonies } from 'musicxml-io';
const notes = getAllNotes(score);
const quarterNotes = findNotes(score, { noteType: 'quarter' });
const count = getMeasureCount(score);
const harmonies = getHarmonies(score); // chord symbols (C7, Dm, etc.)Accessors
Entry-level helpers for working with individual notes, directions, and parts:
import {
getAllNotes,
isRest, isPitchedNote, hasTie, isChordNote,
getPartName,
getDirectionOfKind, getSoundTempo
} from 'musicxml-io';
// NoteEntry helpers
for (const item of getAllNotes(score)) {
if (isRest(item.note)) continue;
if (isPitchedNote(item.note)) {
console.log(`${item.note.pitch!.step}${item.note.pitch!.octave}`);
}
if (hasTie(item.note)) console.log('Tied note');
if (isChordNote(item.note)) console.log('Part of chord');
}
// PartInfo helpers
const partName = getPartName(score, 'P1'); // 'Piano'
// DirectionEntry helpers
for (const entry of measure.entries) {
if (entry.type === 'direction') {
const dynamics = getDirectionOfKind(entry, 'dynamics');
if (dynamics) console.log(dynamics.value); // 'ff', 'pp', etc.
const tempo = getSoundTempo(entry);
if (tempo) console.log(`Tempo: ${tempo} BPM`);
}
}MIDI Export
import { exportMidi } from 'musicxml-io';
const midi = exportMidi(score, { tempo: 120 });Playback timeline (for audio alignment)
generatePlaybackTimeline returns a timing sidecar mapping playback time
(seconds) to conceptual musical positions (measure + beat), with
repeats/voltas/jumps expanded. It is a read-only analysis (under query) — the
sibling of generatePlaybackSequence, which gives the play order; this gives
the same expansion with absolute times.
import { generatePlaybackTimeline } from 'musicxml-io';
const sidecar = generatePlaybackTimeline(score);
// sidecar.breakpoints: [{ midiSec, quarterPos, measureNumber, beatInMeasure, repeatIteration }]The timeline is the one thing that cannot be recomputed from the MusicXML alone,
because the seconds depend on the tempo and repeat expansion. Its seconds equal
the playback time of exportMidi's output (they share the same computation).
Pair it with an audio aligner that returns audioSec ↔ midiSec to follow a
recording on the score:
audioSec ─[aligner]→ midiSec ─[timeline: interpolate quarterPos]→ (measure, beat)Breakpoints are emitted at every played-measure start and every tempo change,
sorted and monotone by midiSec. Between two consecutive breakpoints
midiSec ↔ quarterPos is linear (tempo is piecewise-constant), so any
intermediate time interpolates exactly. A repeated measure appears multiple
times with a rising repeatIteration. The conceptual position is
renderer-independent — resolving (measure, beat) to a rendered element is the
caller's responsibility.
When you need both the MIDI and its timeline, exportMidiWithTimingMap(score)
returns { midi, sidecar } in one call (just exportMidi +
generatePlaybackTimeline, guaranteed consistent).
Validation
import { validate, isValid } from 'musicxml-io';
const { valid, errors } = validate(score);API
Parse / Serialize
| Function | Description |
|----------|-------------|
| parse(xml) | Parse MusicXML string |
| parseFile(path) | Parse from file |
| parseCompressed(buffer) | Parse .mxl |
| parseAbc(abc) | Parse ABC notation string |
| parseAuto(data) | Auto-detect format (MusicXML / .mxl / ABC) |
| serialize(score) | To MusicXML string |
| serializeToFile(score, path) | To file |
| serializeCompressed(score) | To .mxl |
| serializeAbc(score, options?) | To ABC notation string |
| exportMidi(score) | To MIDI |
| exportMidiWithTimingMap(score) | To MIDI + a MIDI↔(measure, beat) timing sidecar for audio alignment |
Operations
| Function | Description |
|----------|-------------|
| transpose(score, semitones) | Transpose pitches |
| insertNote(score, options) | Insert note at position |
| removeNote(score, options) | Remove note (replace with rest) |
| addChord(score, options) | Add note to chord |
| setNotePitch(score, options) | Change note pitch |
| changeNoteDuration(score, options) | Change note duration |
| addVoice(score, options) | Add voice to measure |
| addPart(score, options) | Add part to score |
| removePart(score, options) | Remove part from score |
| setStaves(score, options) | Set staff count |
| changeKey(score, key, part, measure) | Change key signature |
| changeTime(score, time, part, measure) | Change time signature |
| insertMeasure(score, part, after) | Insert measure |
| deleteMeasure(score, part, measure) | Delete measure |
| addTie(score, options) | Add tie between notes |
| addSlur(score, options) | Add slur between notes |
| addArticulation(score, options) | Add staccato, accent, etc. |
| addDynamics(score, options) | Add dynamics (f, p, etc.) |
| modifyDynamics(score, options) | Modify dynamics |
| addTempo(score, options) | Add tempo marking |
| modifyTempo(score, options) | Modify tempo |
| addOrnament(score, options) | Add trill, turn, etc. |
| addText(score, options) | Add text direction |
| addLyric(score, options) | Add lyric to note |
| autoBeam(score, options) | Auto-beam notes |
| createTuplet(score, options) | Create tuplet |
| addChordSymbol(score, options) | Add chord symbol |
| changeClef(score, options) | Change clef |
| setBarline(score, options) | Change barline style |
| addRepeat(score, options) | Add repeat barline |
| addEnding(score, options) | Add first/second ending |
| addFermata(score, options) | Add fermata |
| addWedge(score, options) | Add crescendo/diminuendo |
| addPedal(score, options) | Add pedal marking |
| addGraceNote(score, options) | Add grace note |
See OPERATIONS.md for the complete list.
Query
| Function | Description |
|----------|-------------|
| getAllNotes(score) | All notes with context |
| findNotes(score, filter) | Filter notes by criteria |
| getMeasure(score, { part, measure }) | Get measure by number |
| getMeasureByIndex(score, { part, measureIndex }) | Get measure by index |
| getMeasureCount(score) | Total measure count |
| getChords(measure) | Chord groups in measure |
| countNotes(score) | Total note count |
| getHarmonies(score) | All chord symbols |
| getDynamics(score) | All dynamics markings |
| getTempoMarkings(score) | All tempo markings |
| generatePlaybackSequence(score) | Play order of measures (repeats/voltas/jumps expanded) |
| generatePlaybackTimeline(score) | Playback time (sec) ↔ (measure, beat) map for audio alignment |
Accessors
Entry-level helpers for individual notes, directions, and parts.
NoteEntry
| Function | Description |
|----------|-------------|
| isRest(note) | Check if rest |
| isPitchedNote(note) | Check if has pitch |
| isUnpitchedNote(note) | Check if percussion |
| isChordNote(note) | Check if part of chord |
| isGraceNote(note) | Check if grace note |
| isCueNote(note) | Check if cue note |
| hasTie(note) | Check if tied |
| hasTieStart(note) | Check if tie starts |
| hasTieStop(note) | Check if tie stops |
| hasBeam(note) | Check if beamed |
| hasLyrics(note) | Check if has lyrics |
| hasNotations(note) | Check if has notations |
| hasTuplet(note) | Check if in tuplet |
DirectionEntry
| Function | Description |
|----------|-------------|
| getDirectionOfKind(entry, kind) | Get first direction type |
| getDirectionsOfKind(entry, kind) | Get all direction types |
| hasDirectionOfKind(entry, kind) | Check if has type |
| getSoundTempo(entry) | Get tempo from sound |
| getSoundDynamics(entry) | Get dynamics (0-127) |
| getSoundDamperPedal(entry) | Get damper pedal state |
| getSoundSoftPedal(entry) | Get soft pedal state |
| getSoundSostenutoPedal(entry) | Get sostenuto pedal state |
PartInfo
| Function | Description |
|----------|-------------|
| getPartInfo(score, id) | Get part info by ID |
| getPartName(score, id) | Get part name |
| getPartAbbreviation(score, id) | Get part abbreviation |
| getAllPartInfos(score) | Get all part infos |
| getPartNameMap(score) | Get ID to name map |
| isPartInfo(entry) | Type guard for PartInfo |
Validate
| Function | Description |
|----------|-------------|
| validate(score) | Validation errors |
| isValid(score) | Boolean check |
| assertValid(score) | Throw if invalid |
Tree-shaking
import { transpose } from 'musicxml-io/operations';
import { findNotes } from 'musicxml-io/query';
import { isRest, getPartName } from 'musicxml-io/entry-accessors';Unique Element IDs
All elements in the Score structure have a unique _id property that is automatically generated when:
- MusicXML is parsed/imported
- New elements are created via operations
The ID format is "i" + nanoid(10) (11 characters total), where:
"i"prefix ensures XML ID compatibility (IDs must start with a letter or underscore)nanoid(10)generates a URL-safe unique identifier
import { parse, generateId } from 'musicxml-io';
const score = parse(xmlString);
console.log(score._id); // e.g., "iV1StGXR8_Z"
console.log(score.parts[0]._id); // e.g., "i2x4K9mL1Qp"
// Generate IDs manually for custom elements
const customId = generateId(); // e.g., "iAb3Cd5Ef7H"This feature enables:
- Tracking elements across transformations
- Building element references in external systems
- Implementing undo/redo functionality
- Diffing and merging scores
Round-trip Fidelity
MusicXML
| Metric | Score | |--------|------:| | Overall | 99.6% | | Node coverage | 99.9% | | Attribute coverage | 95.9% |
ABC Notation
| Path | Fidelity | |------|----------| | ABC → Score → ABC | High (42 fixtures passing) | | ABC → MusicXML → ABC | Musical content preserved |
Contributing
Contributions are welcome! Whether it's bug reports, feature requests, documentation improvements, or code contributions, we appreciate your help in making this library better.
Development Setup
# Clone the repository
git clone https://github.com/tan-z-tan/musicxml-io.git
cd musicxml-io
# Install dependencies
npm install
# Run tests
npm test
# Build
npm run build
# Type check
npm run typecheck
# Lint
npm run lintHow to Contribute
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Run tests to ensure everything works (
npm test) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Guidelines
- Write tests for new features
- Follow the existing code style
- Update documentation as needed
- Keep PRs focused on a single change
License
MIT
