acorde
Platform-agnostic music score library for Rust and WebAssembly.
Overview
acorde is a pure-Rust music score library covering the full notation pipeline: score model · command engine · MusicXML/MIDI/ABC I/O · logical layout · WebAssembly bindings · CLI.
It has zero UI dependencies and no filesystem access in the core crates — renderers and host applications (desktop, web, server) consume the library and handle I/O at the boundary.
Architecture
Pipeline
flowchart LR
subgraph Input["Input formats"]
MXL[".musicxml / .mxl"]
MID[".mid"]
ABC[".abc"]
MSZ[".mscz / .mscx"]
end
subgraph IO["acorde-io"]
direction TB
pm["parse_musicxml\nparse_mxl"]
pmid["parse_midi"]
pabc["parse_abc"]
pmscz["parse_mscz\nparse_mscx"]
end
subgraph Core["acorde-core"]
direction TB
Score(["Score"])
SE["ScoreEngine\n(apply / undo / redo)"]
PE["to_playback_events"]
TX["transpose / validate / diff\nScale::best_fit · roman_numeral"]
end
subgraph Layout["acorde-layout"]
CL["compute_layout"]
LR(["LayoutResult\n(vis_slots · rows · spans\nbeam_groups · tuplet_groups)"])
end
subgraph Output["Output formats"]
OXL["serialize_musicxml"]
OMD["serialize_midi"]
OAB["serialize_abc"]
end
MXL --> pm --> Score
MID --> pmid --> Score
ABC --> pabc --> Score
MSZ --> pmscz --> Score
Score <--> SE
Score --> PE
Score --> TX
Score --> CL --> LR
Score --> OXL
Score --> OMD
Score --> OAB
Score data model
Score
├── metadata { title, composer, lyricist, copyright, … }
├── settings { tempo_bpm, time_signature, key_signature }
├── part_groups Vec<PartGroup>
└── parts Vec<Part>
├── midi_channel / midi_program
└── staves Vec<Staff>
├── clef
├── transpose_semitones (transposing instruments)
└── measures Vec<Measure>
├── time_sig / key_sig / clef / tempo
├── barline_left / barline_right
├── volta / rehearsal / navigation / expression_text
└── voices [Vec<Note>; 4]
└── Note
├── pitches Vec<Pitch> (chord = multiple pitches)
├── duration / dot_count / tuplet
├── is_rest / is_grace / is_cue
├── tie_start / slur_start / hairpin_start / ottava_start
├── dynamic / articulations / lyric / chord_symbol
├── stem_up / note_head / fingering / technique_text
└── guitar_technique / arpeggiate / trill_line_start
Command flow (undo / redo)
sequenceDiagram
participant App
participant ScoreEngine
participant CommandStack
participant Score
App->>ScoreEngine: apply(Command::AddNote(…))
ScoreEngine->>CommandStack: execute(cmd, &mut score)
CommandStack->>Score: mutate
CommandStack-->>ScoreEngine: ChangeHint { scope, layout_dirty, playback_dirty }
ScoreEngine-->>App: ChangeHint
App->>ScoreEngine: undo()
ScoreEngine->>CommandStack: undo(&mut score)
CommandStack->>Score: revert mutation
CommandStack-->>App: ChangeHint
Why acorde?
Edit history is a first-class citizen
Every mutation is a serializable Command enum stored in a CommandStack.
This means undo/redo works out of the box, but also that edit history can be persisted to disk,
replayed deterministically, or streamed over a network — without any extra infrastructure.
The same code runs natively and in the browser
acorde-core and acorde-io compile to WebAssembly without modification.
A server that parses MusicXML and a browser-based editor can share identical business logic.
Only import what you actually use
acorde-io features are independent flags — musicxml, midi, abc, mscz.
acorde-core pulls in no I/O crates at all, keeping it embeddable in constrained environments.
Compared to other tools
The music notation ecosystem has several mature players, each optimised for a different use-case. The table below maps the most commonly reached-for alternatives.
| acorde | music21 | VexFlow | OSMD | jFugue | |
|---|---|---|---|---|---|
| Primary language | Rust | Python | JavaScript | TypeScript | Java |
| Runs in browser | ✓ (WASM) | ✗ | ✓ | ✓ | ✗ |
| Score data model | ✓ | ✓ | partial¹ | ✗ | ✓ |
| MusicXML parse + emit | ✓ | ✓ | ✗ | parse only | ✗ |
| MIDI parse + emit | ✓ | ✓ | ✗ | ✗ | ✓ |
| ABC Notation | ✓ | ✓ | ✗ | ✗ | ✗ |
| Undo / redo built-in | ✓ | ✗ | ✗ | ✗ | ✗ |
| Serializable edit history | ✓ | ✗ | ✗ | ✗ | ✗ |
| Playback event generation | ✓ | ✗ | ✗ | ✗ | ✓ |
| Renderer-agnostic layout | ✓ | ✗ | ✗ | ✗ | ✗ |
| Music-theory analysis | ✓ | ✓✓✓ | ✗ | ✗ | basic |
| No garbage collector | ✓ | ✗ | ✗ | ✗ | ✗ |
| Embeddable / no runtime | ✓ | ✗ | ✗ | ✗ | ✗ |
¹ VexFlow's object model (StaveNote, Beam, etc.) is tightly coupled to its SVG/Canvas renderer.
It is not designed as a standalone data layer that can be serialized or mutated independently.
When to choose acorde
acorde is the right fit when you need:
- A Rust or WebAssembly target — native desktop, server-side processing, or a browser editor using the same binary.
- A full pipeline in one library — parse MusicXML, run commands, export MIDI, compute layout hints, generate playback events — without gluing together separate packages.
- Built-in undo/redo and crash recovery — every mutation is a serializable
Command. History can be persisted and replayed deterministically; no separate state-management framework required. - AI or batch mutation —
batch_apply_labeled()applies an arbitrary sequence of commands as a single undoable step, making AI-assisted score editing straightforward. - Renderer independence —
LayoutResultreturns logical coordinates (row/column slot indices, resolved span endpoints) rather than pixels, so you can drive VexFlow, a canvas renderer, or a native UI from the same data.
When to choose something else
- Deep music-theory analysis (Roman numerals, voice-leading, corpus research): reach for music21. Its analysis toolkit is unmatched and Python's ecosystem is ideal for research notebooks.
- Rendering only, no mutation: if you just need to display a static MusicXML file in a browser, OSMD has a polished out-of-the-box experience.
- JavaScript without a build step: VexFlow or abc.js can be dropped into a
<script>tag; acorde requires a WASM build pipeline. - JVM ecosystem: jFugue is the natural choice for Java/Kotlin projects.
Workspace
acorde/
Cargo.toml # workspace
crates/
core/ # Score model + ScoreEngine (no I/O, no layout)
io/ # MusicXML / MIDI / ABC parsers & serializers
layout/ # Logical layout engine
wasm/ # wasm-bindgen bindings
cli/ # Format-conversion CLI
tests/
fixtures/ # Sample .musicxml / .mid / .abc files
Crates
acorde
Umbrella crate — depend on this alone to get acorde-core, acorde-io, and acorde-layout
re-exported as acorde::core, acorde::io, acorde::layout.
acorde-core
Score data model and command engine. Zero I/O, zero layout.
use ;
let mut engine = new;
engine.apply?;
engine.undo?;
engine.redo?;
// Transpose the score up a perfect fifth (7 semitones)
let transposed = transpose;
// Mark staff as Bb instrument (written C4 → concert Bb3)
engine.apply?;
// Change MIDI channel and program for a part
engine.apply?;
// Set a tempo change starting at measure 4
engine.apply?;
// Merge two scores (append parts, pad shorter one with empty measures)
let combined = score_a.merge;
// Compute playback events for audio engines (Tone.js, Web Audio API, etc.)
use PlaybackOptions;
let events = to_playback_events;
// Each PlaybackEvent: time_beats, time_secs, pitch_midi, velocity, duration_beats, duration_secs, part_index
// pitch_midi includes Staff.transpose_semitones; time_secs is correct across tempo changes
// Copy and paste a voice (clipboard lives in ScoreEngine; paste is undo-able)
engine.copy_voice?; // copy part 0 / staff 0 / measure 0 / voice 0
engine.paste_voice?; // paste to measure 1
// General MIDI program and drum name lookup
assert_eq!;
assert_eq!;
assert_eq!;
// ChangeHint — skip redundant recomputes
let hint = engine.apply?;
// hint.scope == ChangeScope::Global
// hint.layout_dirty == false (tempo doesn't affect measure layout)
// hint.playback_dirty == true
Public types: Score · Part · Staff · Measure · Note · Pitch · Step ·
Duration · Clef · KeySignature · TimeSignature · Dynamic · Articulation ·
Barline · HairpinKind · OttavaKind · Lyric · ChordSymbol · NoteHead · GuitarTechnique ·
PartGroup · PartGroupSymbol · ScoreTemplate ·
ScoreEngine · Command · CommandStack · ScoreStats · PlaybackEvent ·
Interval · IntervalQuality · Scale · ScaleKind ·
ValidationError · ValidationWarning · ValidationReport
Commands (53): AddNote · AddPitch · DeleteNote · AddMeasure · DeleteMeasure ·
SetTempo · NewScore · AddHairpin · ToggleTie · SetDynamic · ToggleArticulation ·
SetKeySignature · SetTimeSignature · SetBarline · AddPart · DeletePart ·
SetMetadata · SetRehearsalMark · SetNavigationMark · SetChordSymbol · SetGrace ·
SetOttava · SetLyric · SetMultiRest · AddPedal · SetVolta · SetClef ·
SetPartName · SetMidiInstrument · SetTranspose · SetTempoAtMeasure · PasteVoice ·
PasteRange · SetSystemBreak · SetPageBreak · ToggleSlur · AddStaff · DeleteStaff ·
SetTuplet · RespellScore · RespellScoreToKey · SetStem · SetArpeggio ·
SetTechniqueText · SetFingering · SetStringNumber · SetNoteHead · SetCue ·
SetGuitarTechnique · SetExpressionText · ToggleTrillLine · SetPartGroup · Batch
Functions: transpose(score, semitones) · to_playback_events(score, options) ·
measure_sequence(score) · validate(score) · Score::statistics() ·
Score::extract_part(n) · Score::merge(other) · Score::diff(a, b) ·
program_name(n) · drum_name(n) · interval_between(p1, p2) ·
detect_chord(pitches) · roman_numeral(chord, key) · Scale::best_fit(pitches)
ChangeHint types: ChangeHint · ChangeScope (Global / Part(usize) / Measures{…})
acorde-io
Feature-gated parsers and serializers. Never touches the filesystem.
| Feature | Default | Content |
|---|---|---|
musicxml |
✓ | MusicXML + MXL parser, MusicXML serializer |
midi |
✓ | MIDI parser + serializer |
abc |
— | ABC Notation parser + serializer |
mscz |
— | MuseScore .mscz/.mscx parser |
use ;
use ; // requires feature = "abc"
use ; // requires feature = "mscz"
let score = parse_musicxml?;
let xml = serialize_musicxml?;
// <midi-instrument> channel/program and <transpose><chromatic> survive round-trips
let score = parse_midi?;
let midi = serialize_midi?;
// Part.midi_channel / Part.midi_program restored from ProgramChange events on import
// → Vec<u8> (SMF Type 1, PPQ = 480)
// Staff.transpose_semitones is applied to all MIDI note pitches
// Per-measure Tempo meta events are emitted when Measure.tempo is set
let score = parse_mscz?; // .mscz (compressed archive)
let score = parse_mscx?; // .mscx (raw XML)
// Imports: pitches (TPC), durations, rests, ties, slurs, dynamics, lyrics,
// repeat barlines, volta brackets, MuseScore 3.x and 4.x formats
acorde-layout
Logical layout computation — no pixel values, no CSS.
use ;
let config = LayoutConfig ;
let result = compute_layout;
// result.vis_slots — visual column → physical measure index (multi-rest aware)
// result.rows — measures per row/system
// result.spans — hairpin / pedal / ottava start+end indices resolved
// result.concert_key_overrides — per-staff key sig for transposing instruments in concert pitch
// result.beam_groups — note index groups for beam rendering
// result.tuplet_groups — note index groups with actual/normal note counts for tuplets
// result.courtesy_accidentals — accidentals that must be shown as a courtesy to the player
acorde-wasm
wasm-bindgen bindings. Build with wasm-pack build.
Exposes: parse_musicxml · parse_mxl · serialize_musicxml · parse_midi · serialize_midi ·
serialize_midi_region(score_json, start, end) · parse_mscz · parse_mscx ·
parse_abc · serialize_abc ·
to_playback_events(score_json, bpm, muted_parts_json) · to_playback_events_ex(score_json, options_json) ·
compute_playback_position(score_json, time_secs) ·
compute_layout(score_json, measures_per_row, concert_pitch) · compute_layout_ex(score_json, config_json) ·
gm_program_name(n) · gm_drum_name(n) ·
validate_score · transpose_score · extract_part · merge_scores · diff_scores ·
score_statistics · score_duration_secs · score_duration_secs_region ·
respell_score(score_json, prefer_flat) · respell_score_to_key ·
measure_beats_remaining · pitch_from_midi(midi, prefer_flat) · pitch_from_str ·
interval_between(pitch1_json, pitch2_json) ·
key_alter_for_step · key_contains_pitch · key_display_name ·
clef_middle_line_midi · suggested_stem_up(pitches_json, clef_json) ·
compute_beams(notes_json, time_sig_json) · command_key_from_json ·
detect_chord(pitches_json) · roman_numeral(chord_json, key_json) · best_fit_scale(pitches_json) ·
ScoreEngine JS class:
apply(cmd_json) → ChangeHint JSON · undo() / redo() → ChangeHint JSON ·
apply_batch(cmds_json) · apply_batch_labeled(cmds_json, label) ·
copy_voice / paste_voice · copy_range / paste_range ·
get_undo_label() / get_redo_label() · get_undo_key() / get_redo_key() ·
export_history() / restore_history(json) · replace_score · get_score · get_version
acorde-cli
Getting Started
For convenience, add the acorde umbrella crate — it re-exports acorde-core, acorde-io,
and acorde-layout under acorde::core, acorde::io, acorde::layout:
[]
= "0.1"
# ABC Notation support (opt-in)
= { = "0.1", = ["abc"] }
# MuseScore .mscz/.mscx support (opt-in)
= { = "0.1", = ["mscz"] }
Or depend on the individual crates directly:
[]
= "0.1"
For I/O support:
= "0.1"
# ABC Notation support (opt-in)
= { = "0.1", = ["abc"] }
# MuseScore .mscz/.mscx support (opt-in)
= { = "0.1", = ["mscz"] }
Building
Prerequisites: Rust 1.77+
For WebAssembly:
Design Constraints
| Rule | Applies to |
|---|---|
No async runtime (tokio) |
core · io · layout |
No std::fs |
core · io · layout |
| No pixel values, CSS, or renderer-specific types | all crates |
core must not depend on io or layout |
core |
No panic! / unwrap in public paths |
all crates |
Testing
Every parser has a round-trip integration test in crates/io/tests/roundtrip.rs.
Passing 0-byte or garbage data to any parser returns Err, never panics.
License
acorde is dual-licensed under MIT or Apache-2.0, at your option — see LICENSE-MIT and LICENSE-APACHE for details.