Skip to content

Latest commit

 

History

History
250 lines (203 loc) · 11.3 KB

File metadata and controls

250 lines (203 loc) · 11.3 KB

music21-rs for Python

PyPI music21 members in the wheel music21 doctests passing

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-rs
import 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.5

Reading and writing scores

import 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.

Using it inside music21

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")).pitches

It 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.

Coverage of music21

  • 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 of figuredBass: notation, possibility, realizerScale, rules, resolution, segment, harmony, checker, and the realizer and examples that 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.

Speed and size

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.

Building

uvx maturin build --release --manifest-path python/Cargo.toml
uvx maturin develop --manifest-path python/Cargo.toml   # into the active venv
pytest python/tests -q

The 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 -- downstream

Credits and third-party data

music21_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.

music21

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.

harte-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.

The Scala scale archive

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 Xenharmonic Wiki

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.

Contributed back

Fixes found while porting went upstream: