music21_rs is music21's analysis
classes, Pitch, Interval, Chord, Note, Duration, Key, Scale,
RomanNumeral, TimeSignature, ToneRow and others, implemented in Rust
and packaged for Python. The classes have music21's names, arguments,
properties and repr, and give music21's answers. The Rust half is the
music21-rs crate; the wheel needs no
Rust toolchain and no music21 to run on its own. music21's stream
machinery, its converter and the corpus are not included; those remain
music21's. The package does read scores in seven formats into streams of its
own, and writes MusicXML: see Reading and writing
scores.
The reports page has the full numbers: every music21 method and whether it is ported, the doctest and test suite results, benchmarks and sizes, refreshed on every push.
pip install music21-rsimport music21_rs as m
chord = m.Chord("C4 E4 G4 B-4")
chord.commonName # 'dominant seventh chord'
chord.root() # <music21.pitch.Pitch C4>
chord.forteClass # '4-27B'
chord.transpose("M3") # <music21.chord.Chord E4 G#4 B4 D5>
m.RomanNumeral("viio7", m.Key("c")).pitches
# (<music21.pitch.Pitch B4>, <music21.pitch.Pitch D5>, <music21.pitch.Pitch F5>, <music21.pitch.Pitch A-5>)
m.TimeSignature("6/8").getAccentWeight(1.5) # 0.5import music21_rs as m
score = m.from_musicxml(open("tune.musicxml", encoding="utf-8").read())
for part in score.parts:
print(part.partName, len(part.getElementsByClass("Measure")))
text = m.to_musicxml(score, encoding_date="2026-01-01")
song = m.from_midi(open("song.mid", "rb").read())
line = m.from_tiny_notation("tinyNotation: 3/4 E4 r f# g trip{b-8 a g} c'2.")Every format music21-rs reads is read as music21's converter.parse reads
it, into this package's own streams:
| function | format | hands back |
|---|---|---|
from_musicxml(text) |
MusicXML, partwise | Score |
from_abc(text) |
ABC | Score, or an Opus of them for several tunes |
from_abc_number(text, number) |
one tune of an ABC file, by its X: |
Score |
from_midi(data) |
a standard MIDI file's bytes |
Score |
from_tiny_notation(text) |
TinyNotation | Part |
from_humdrum(text) |
Humdrum **kern |
Score, or an Opus for several tables |
from_mei(text) |
MEI | Score |
from_roman_text(text) |
RomanText | Score, or an Opus for several movements |
The streams hold measures of notes, chords, rests, clefs, keys, meters,
tempo marks, dynamics, chord symbols and instruments, and a voice apiece
where a measure has more than one. A RomanText score's chords carry their
numerals as lyrics. What the package has no class for -- words, barlines,
repeat marks, slurs and other spanners, unpitched percussion, metadata -- is
read and left out. Input a reader cannot read raises StreamException. A
compressed .mxl is a zip holding the document: unpack it first.
to_musicxml takes one of this package's streams or one of music21's and
writes what music21's exporter writes with makeNotation=False. With
make_notation=True it makes the notation first, as music21's exporter does
by default, so a part of loose notes is written in measures, with its ties,
rests, accidentals and beams.
install_into_music21() replaces the classes of an installed music21 with
these, so existing music21 code runs on the Rust implementation unchanged:
import music21_rs
music21_rs.install_into_music21() # before anything imports the classes
from music21 import chord, roman, key
chord.Chord("C4 E4 G4").commonName # 'major triad', out of Rust
roman.RomanNumeral("V7", key.Key("G")).pitchesIt patches the live modules for the whole process, so call it before any
from music21.chord import Chord; for a test suite, conftest.py is early
enough. The installed classes are Music21Object subclasses, so music21 can
hold them in streams, find them by class, pickle them and export them to
MusicXML.
- 94% of the public methods of the ported music21 classes are reachable from this wheel.
- All 40 music21 modules whose doctests run against the port pass every
example, 9,174 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 this wheel installed over music21 as with music21 alone.
- harte-library, a third-party chord parser built on music21, gives identical results for its 8,116 tests on both.
style.Style is provided but not installed over music21's, whose
version does more. Missing behaviour raises
rather than falling back to music21.
Times per call, from the benchmark against music21 11 on Python 3.13.
| music21 | music21_rs | speedup | |
|---|---|---|---|
Pitch('C#4') |
1.55 us | 0.37 us | 4x |
Note('C#4') |
4.4 us | 1.3 us | 3.4x |
Interval('P5') |
7.5 us | 0.47 us | 16x |
Chord('C4 E4 G4') |
16.2 us | 8.9 us | 1.8x |
Chord.commonName |
526 us | 60 us | 8.8x |
Chord.forteClass |
244 us | 53 us | 4.6x |
Pitch.transpose('M3') |
28 us | 1.0 us | 28x |
Pitch.getEnharmonic() |
22.7 us | 0.63 us | 36x |
ToneRow.zeroCenteredTransformation |
274 us | 0.37 us | 741x |
pcToToneRow(...).matrix() |
2.71 ms | 4.9 us | 547x |
Repeated queries on the same chord are cached in both and cost the same. Over music21's own test suite the median test runs at the same speed, since most of a music21 test is music21's own code.
The wheel is 1.7 MB, one compiled module with the chord tables and scale definitions inside it and no dependencies. music21 installs 105 MB.
uvx maturin build --release --manifest-path python/Cargo.toml
uvx maturin develop --manifest-path python/Cargo.toml # into the active venv
pytest python/tests -qThe wheel shares the crate's version number. Two further checks run it
against music21 itself, and need music21, lark and pytest installed
beside it:
# music21's own test suite, on music21, on the crate and on the wheel
cargo run --release -p xtask --features python -- music21-suite
# harte-library's test suite, on music21 and on the wheel
cargo run --release -p xtask -- downstreammusic21_rs is released under the
AGPL-3.0. It
ports behaviour from, and compiles in data from, the projects below, each
under its own licence.
The chord tables, scale definitions, time-signature behaviour and chord-symbol kinds are derived from music21, the Python library for computational musicology by Michael Scott Asato Cuthbert and contributors, licensed BSD-3-Clause. The wheel is built against music21 11.0.0b9, and its classes are checked against that version's own doctests and test suite. Thanks to Michael Scott Asato Cuthbert and all music21 contributors for the original library.
The harte crate beside this one, which xtask downstream runs
harte-library's own tests against, is a port of
harte-library by Andrea
Poltronieri, licensed
MIT.
Thirteen of the tuning tables compiled in are transcribed from the
Scala scale archive as distributed
with music21, which includes it by kind permission of Manuel Op de Coul. The
archive itself, 3,994 scales in the crate's scala-archive feature, and the
62 Plainsound Hexatone scales
beside it are not in the wheel.
The mapping, generators, commas and scales of 95 regular temperaments from the Xenharmonic Wiki, which is CC BY-SA, are compiled in. Only the numbers are used.
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 (open). - 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.