a rust port of the analysis half of music21: pitches, intervals, chords, keys, scales, roman numerals, chord symbols, figured bass and its realization, durations, meters, clefs, articulations, ornaments, tone rows, sieves, tempo marks and voice leading. it keeps music21's data model and answers, so a chord is named, a numeral realized and a scale spelled as music21 does it. the same code is also published as a Python package; see python/README.md.
the reports page has the full numbers: every music21 method and whether it is ported, the doctest and test suite results, coverage, benchmarks and sizes, refreshed on every push.
-
92% of the public methods of the ported music21 classes are in the crate.
-
the crate's own tests cover 95% of its lines and of its functions.
-
all 40 music21 modules whose doctests run against the port pass every example, 9,185 of them:
pitch,interval,chord,chord.tables,note,duration,key,scale,scale.scala,roman,harmony,serial,sieve,meter.base,meter.core,beam,tie,volume,dynamics,instrument,clef,articulations,expressions,tempo,voiceLeading,analysis.discrete,analysis.enharmonics,analysis.harmonicFunction,analysis.neoRiemannian,analysis.transposition, and ten offiguredBass:notation,possibility,realizerScale,rules,resolution,segment,harmony,checker, and therealizerandexamplesthat realize whole lines over them. -
music21's own test suite gives the same results with the crate's classes standing in for music21's as with music21 alone.
-
MusicXML export writes what music21's exporter writes for a notated score, and 41 scores from music21's corpus — chorales, lead sheets, piano music, whole string quartets — come out byte for byte the same.
-
MusicXML import reads a file as music21's reader reads it: those same scores, read by the crate and written back, give the text music21 gives after reading them itself.
-
with
ExportOptions::make_notationthe export works out the notation a score leaves unsaid first, as music21's does by default: measures, rests, ties, accidentals, beams, tuplet brackets. 244 subjects — MIDI files, ABC tunes with no barlines, loose notes, notated corpus scores — come out byte for byte as music21 writes them.
not ported: music21's sites and context search (a crate stream knows nothing outside itself, so what music21 finds in context is passed in), its plotting, recording from a microphone and following a score live, and the corpus.
times per call, from the benchmark against music21 11 on Python 3.13.
| music21 | music21-rs | speedup | |
|---|---|---|---|
Pitch::from_name("C#4") |
1.55 us | 0.22 us | 7x |
Interval::from_name("P5") |
7.5 us | 0.21 us | 35x |
Chord::new("C4 E4 G4") |
16.2 us | 0.79 us | 21x |
Chord::common_name |
526 us | 14.8 us | 36x |
Chord::forte_class |
244 us | 7.4 us | 33x |
Chord::root |
115 us | 6.4 us | 18x |
Pitch::transpose by a third |
28 us | 0.58 us | 49x |
Pitch::get_enharmonic |
22.7 us | 0.37 us | 62x |
Interval::new(p1, p2) |
9.7 us | 0.47 us | 21x |
the published crate is 0.7 MB of source, with the chord tables and scale definitions compiled in and five small dependencies. a release binary that names chords and transposes pitches, the benchmark example, is 441 KB. music21 installs 105 MB.
cargo add music21-rsdefault features are empty. serde adds Serialize and Deserialize to the
public types; scala-archive bundles the Scala scale archive described below;
language-detection bundles music21's excerpts in seven languages, about
1.9 MB, for its language feature TX1 and features::language;
musicxml reads and writes MusicXML; musescore converts through an installed
MuseScore, and turns musicxml on. ABC, MIDI, TinyNotation, Humdrum, MEI
and RomanText are read with no feature at all: abc::from_abc,
midi::from_midi, tinynotation::from_tiny_notation,
humdrum::from_humdrum, mei::from_mei, romantext::from_roman_text and
noteworthy::from_noteworthy and noteworthy::from_nwc (NoteWorthy
Composer's .nwctxt and, uncompressed, .nwc), musedata::from_musedata
and capella::from_capella (a .capx's score.xml) each read
what music21 reads into the score music21 makes of it. ABC is
written as well: abc::to_abc writes a score as a tune music21's reader and
the crate's both read back as the same score. so is MIDI: midi::to_midi
writes the file music21's streamToMidiFile writes, byte for byte, its
repeats played out first as Stream::expand_repeats plays them. chant
written in Volpiano is read and written by volpiano::from_volpiano and
volpiano::to_volpiano, as music21's volpiano module reads and writes it.
A RomanText analysis is written back by romantext::to_roman_text, as
music21's RnWriter writes one: every analysis of music21's corpus comes
out the same text on both sides. The two tabular analysis formats beside it
are read as well: romantext::tsv::from_tsv reads one of the Digital and
Cognitive Musicology Lab's tables in either of its standards and
tsv::to_tsv writes one back, column for column; and
romantext::clercq_temperley::from_clercq_temperley reads a de
Clercq-Temperley analysis of the rock corpus -- rules of chords a measure
at a time, played through by a song rule -- as music21's CTSong.toPart
reads it. lily::to_lilypond writes a score as
LilyPond, as music21's LilypondConverter writes it: every MusicXML score
of music21's corpus that music21 can write comes out the same text, space
for space.
music21's analyses of whole pieces are ported beside its chord and pitch
analysis, each held to music21 on its corpus: Stream::chordify, the
melodic intervals between consecutive notes, Patel's rhythmic and melodic
variability, metrical depth and melodic accent, windowed and floating key
analysis (analysis::windowed, analysis::floating_key), and the features
music21 extracts for machine learning, jSymbolic's and its own
(features::jsymbolic, features::native), and the correction of
scores read by optical music recognition (omr), which finds the measures
whose rhythm was misread and puts the likeliest rhythm in their place.
Other readings of a passage are kept beside it as music21's variants
(variant): merged in from another version of a score, and made the
score's reading by Stream::activate_variants.
A recording of one line is transcribed as music21's audioSearch does
it (audiosearch::monophonic_stream_from_wav): a WAV file's frequencies
found by autocorrelation, snapped to a scale, smoothed and joined into
notes and rests whose lengths are quantized, the same notes music21 finds.
MuseScore's own files are read with no feature and no MuseScore installed:
musescore::from_mscx takes the text of a .mscx, as MuseScore 3 and 4 save
it, and hands back the score, held to what MuseScore itself exports from the
same file. it opens and unpacks nothing, so a .mscz, which is a zip archive
holding a .mscx, is the caller's to unzip. percussion and tablature staves
and a few kinds of line are refused with an error rather than guessed at; the
function's documentation lists them.
name a chord and read its set class:
use music21_rs::Chord;
let chord: Chord = "C Eb G Bb".parse()?;
assert_eq!(chord.pitched_common_name(), "C-minor seventh chord");
assert_eq!(chord.forte_class().as_deref(), Some("4-26"));
assert_eq!(chord.chord_symbol().as_deref(), Some("Cm7"));
# Ok::<(), music21_rs::Error>(())spell and transpose pitches:
use music21_rs::{Interval, Pitch};
let mut pitch = Pitch::from_name("C#4")?;
pitch.get_higher_enharmonic_in_place()?;
assert_eq!(pitch.name_with_octave(), "Db4");
let fifth = Interval::from_name("P5")?;
assert_eq!(fifth.transpose_pitch(&pitch)?.name_with_octave(), "Ab4");
assert_eq!(fifth.pythagorean_ratio()?.to_string(), "3/2");
# Ok::<(), music21_rs::Error>(())realize a roman numeral in a key, and analyse a chord as one:
use music21_rs::{Chord, Key, RomanNumeral};
let key = Key::from_tonic("c")?;
let numeral = RomanNumeral::new("viio7", key.clone())?;
assert_eq!(numeral.to_chord()?.pitch_names(), ["B", "D", "F", "Ab"]);
let analysed = RomanNumeral::analyze(&Chord::new("F Ab C")?, key)?;
assert_eq!(analysed.map(|n| n.figure().to_string()), Some("iv".to_string()));
# Ok::<(), music21_rs::Error>(())beats and accent weights of a time signature:
use music21_rs::TimeSignature;
let six_eight = TimeSignature::new(6, 8)?;
assert_eq!(six_eight.beat_count(), 2);
assert_eq!(six_eight.accent_weight(1.5)?, 0.5);
# Ok::<(), music21_rs::Error>(())write a score as MusicXML, with the musicxml feature:
# #[cfg(feature = "musicxml")] {
use music21_rs::musicxml::{ExportOptions, to_musicxml};
use music21_rs::{Clef, Note, Stream, StreamKind, TimeSignature};
let mut measure = Stream::with_kind(StreamKind::Measure);
measure.set_number(1);
measure.insert(0.0, Clef::treble());
measure.insert(0.0, TimeSignature::new(3, 4)?);
for name in ["C4", "E4", "G4"] {
measure.push(Note::from_name(name)?);
}
let mut part = Stream::with_kind(StreamKind::Part);
part.push(measure);
let mut score = Stream::with_kind(StreamKind::Score);
score.push(part);
let xml = to_musicxml(&score, &ExportOptions::default())?;
assert!(xml.contains("<beats>3</beats>"));
// And read it back.
let read = music21_rs::musicxml::from_musicxml(&xml)?;
assert_eq!(read.parts()[0].measures()[0].pitches().len(), 3);
# }
# Ok::<(), music21_rs::Error>(())full API documentation is on docs.rs.
the tuning code has no counterpart in music21:
- 28 tuning systems as exact ratio tables: just intonation, meantone, Pythagorean, Bohlen-Pierce, the Carlos scales and 14 historical keyboard temperaments.
- the complete Scala scale archive, 3,994 scales, bundled and searchable with
the
scala-archivefeature. - regular temperament theory from the Xenharmonic Wiki: monzos, vals, 95 temperaments with their commas and mappings, moments of symmetry, and equal divisions of any interval.
- adaptive tuning, where a pitch's frequency depends on the chord around it.
- polyrhythms, including the chord a rhythm's ratios give as pitches.
- a check for the same scale filed under two names. scales are compared by their cents, so one written as ratios and one written as cents still match. The Scala archive has 44 such groups.
- Harte chord notation,
C:maj7/3orBb:(b3,5,b7,9), as thehartecrate beside this one: a port of harte-library that builds music21-rs chords, checked against every label in that library's 8,064-chord coverage set. it is not music21's, so it is not in this crate.
hilll.dev/music21-rs runs the crate in the browser via wasm, from examples/web/: a score editor that engraves ABC notation with abcjs, analyses its harmony and voice leading, shows guitar, bass, mandolin or fiddle tablature and plays it in any tuning system, and exports MIDI and MusicXML; a chord inspector, a chord browser, a polyrhythm lab, a Harte chord reader, a roman numeral realizer, a scale finder, a tone row matrix, a chord listener that names what a microphone hears, whether its notes sound together or one after another, and a tuning explorer that plays every tuning system, regular temperament, equal division and Scala scale from the computer keyboard or a MIDI device. examples/audio/ plays a polyrhythm through the default audio device.
use the toolchain pinned in rust-toolchain.toml.
cargo test runs the crate's tests and needs nothing else. the full check
is several commands, because the parity suite lives in a crate outside the
workspace:
# the reference submodule, needed by the parity suite and the Scala archive
git submodule update --init --recursive
# formatting, for each manifest; `--all` does not leave the workspace
cargo fmt --all -- --check
cargo fmt --manifest-path python/Cargo.toml --all -- --check
cargo fmt --manifest-path python-parity/Cargo.toml --all -- --check
# lints, and the generated files that must match their source data
cargo clippy --workspace --all-targets -- -D warnings
cargo run --release -p xtask -- verify-tables
cargo run --release -p xtask -- verify-tuning-tables
cargo run --release -p xtask -- verify-scala-archive
cargo run --release -p xtask -- verify-temperaments
cargo run --release -p xtask -- verify-report-script
cargo check --workspace --locked
# the library, the examples and xtask, under each feature set
cargo test --workspace --all-targets
for flags in "--no-default-features" "--no-default-features --features serde" "--all-features"; do
cargo clippy --workspace --all-targets $flags -- -D warnings
cargo test --workspace --all-targets $flags
done
# the parity suite: fixtures, the Scala archive, and music21's doctests
cargo test --manifest-path python-parity/Cargo.toml -- --test-threads=1
# music21's own test suite, on music21 and on the crate
cargo run --release -p xtask --features python -- music21-suite
# CI's site job; `report` fails when the feature map is stale
cargo doc --workspace --no-deps
cargo run --release -p xtask --features python -- report --features-onlythe parity suite runs music21's doctests against the crate through an embedded interpreter, so it needs music21's dependencies:
uv venv .m21venv --python 3.12
uv pip install --python .m21venv chardet joblib jsonpickle lark more_itertools numpy requests webcolorson Windows, pyo3 links against the first python on PATH. the Microsoft
Store build does not work; set PYO3_PYTHON to another interpreter and put
its directory first on PATH.
generated files are committed, so normal builds need only Rust. each has
regenerate-*, emit-* and verify-* commands in xtask;
regenerate-all rebuilds all of them after a submodule bump. nix develop
opens a shell with everything CI uses.
music21-rs is released under the AGPL-3.0. it ports behaviour
from, and bundles data from, the projects below, each under its own licence.
the chord tables, scale definitions, time-signature behaviour, chord-symbol
kinds and every expectation fixture are derived from
music21, the Python library for
computational musicology by Michael Scott Asato Cuthbert and contributors,
licensed BSD-3-Clause.
It is pinned as a git submodule, currently at 11.0.0b9, and is the source of
truth for data/chord_tables.toml, data/*_expectations.toml and the Rust
emitted from them. thanks to Michael Scott Asato Cuthbert and all music21
contributors for the original library.
the harte crate in this repository is a port of
harte-library by Andrea
Poltronieri, licensed
MIT, and
harte/data/harte_expectations.toml is generated from its test data.
data/scala_archive.toml bundles 3,994 scales. 3,932 of them come from the
Scala scale archive as distributed
with music21, which includes it by kind permission of Manuel Op de Coul.
music21 states that it "assumes no copyright or change in original licensing"
for those files, and that some may carry restrictions; the complete archive is
published by the
Huygens-Fokker Foundation.
Entries carry source = "music21".
the remaining 62 scales come from
Plainsound Hexatone, a microtonal
MIDI isomorphic keyboard designed and programmed by
Marc Sabat, licensed
GPL-3.0. they are curated
just intonation, harmonic-series, meantone and equal-division scales, several
credited in their own descriptions to Wilson, Fokker, Vicentino, Farabi and
others. entries carry source = "vendored". those files remain under GPL-3.0
within this work, as AGPL-3.0 section 13 provides for.
src/musescore/beams.rs, which works out how MuseScore beams the notes of a
.mscx that leaves beaming to it, is a port of
MuseScore's own beaming code,
copyright MuseScore BVBA and others, licensed
GPL-3.0. that file remains
under GPL-3.0 within this work, as AGPL-3.0 section 13 provides for.
data/temperaments.toml records the mapping, generators, commas and scales
of 95 regular temperaments from the Xenharmonic Wiki,
which is CC BY-SA. only the numbers are used.
src/features/language/ holds the seven excerpts music21's language
detector compares lyrics with (music21/languageExcerpts/trainingData),
public-domain texts from Project Gutenberg as
music21 ships them. they are compiled in only with the language-detection
feature.
fixes found while porting went upstream:
- cuthbertLab/music21#1746:
the
Pitchconstructor's type annotation admits aPitch. - cuthbertLab/music21#2003:
a malformed ratio in the Scala archive's
sparschuh-stanhope.scl. - cuthbertLab/music21#2004:
a
type: ignoreleft over from a closed mypy issue. - cuthbertLab/music21#2026: Scala files with text after a pitch value, which 23 archive files have.
- cuthbertLab/music21#2027:
removeRedundantPitchesconfusing a flat with a negative octave. - cuthbertLab/music21#2028:
getPitchFromNodeDegreehanding back a pitch owned by the scale's cache. - cuthbertLab/music21#2038:
OctaveRepeatingScaleappending to the list of steps it was given. - cuthbertLab/music21#2043: a descending realization checking its range against the unaltered pitch.
- cuthbertLab/music21#2044:
deriveleaving out a scale's altered degrees, so a harmonic minor derived as a natural minor. - cuthbertLab/music21#2045: an octave-repeating scale wider than an octave starting an octave off its tonic.
- cuthbertLab/music21#2046: timespan offsets added as floats, which gave a voice-leading quartet that is not in the music.
- cuthbertLab/music21#2048:
two deepcopies in
AbstractScaleof values that are already copies. - cuthbertLab/music21#2054:
a realization cached without its altered degrees, so a harmonic minor lost
its raised seventh after
nextPitch. - cuthbertLab/music21#2056:
getAllNamesForInstrumentfinding names only for an instrument whose name is spelled like its class. - cuthbertLab/music21#2057:
nextPitchon a cyclical scale answering below the pitch it was given: F#3 after C4 on a scale of major thirds from D3. - PLAINSOUND/hexatone#3: Scala headers in five Hexatone scale files (open).
- corrections to the Xenharmonic Wiki's temperament pages, found while checking its infoboxes: special:contributions/hill.